From 1fe05bd5c7b6b5e7768f15b9c8938ef5c9535572 Mon Sep 17 00:00:00 2001 From: Denis Shilovich Date: Mon, 6 Nov 2023 09:14:33 +0000 Subject: [PATCH] Release 1.4.5 (184) --- .../Sources/AdProviderMock.swift | 13 + .../NGLottie/Sources/LottieViewImpl.swift | 2 +- .../Sources/NicegramSettingsController.swift | 12 + Package.resolved | 8 +- README.md | 23 +- Telegram/BUILD | 21 +- .../Sources/NotificationService.swift | 35 +- Telegram/Share/ShareRootController.swift | 22 +- .../Telegram-iOS/en.lproj/Localizable.strings | 325 ++ build-system/Make/Make.py | 13 + build-system/Make/ProjectGeneration.py | 142 +- ci/fastlane/Fastfile | 2 +- .../Sources/AccountContext.swift | 167 +- .../Sources/AttachmentMainButtonState.swift | 50 + .../Sources/ChatController.swift | 160 +- .../Sources/ChatHistoryLocation.swift | 24 +- .../ContactMultiselectionController.swift | 1 + .../Sources/PeerSelectionController.swift | 42 - .../Sources/AnimatedAvatarSetNode.swift | 12 +- .../Sources/AnimatedCountLabelNode.swift | 20 +- .../Sources/AnimatedStickerNode.swift | 4 +- .../Sources/DirectAnimatedStickerNode.swift | 2 +- submodules/AppLock/Sources/AppLock.swift | 26 +- .../Source/ASTextKitComponents.mm | 45 +- .../AsyncDisplayKit/Source/ASTextKitContext.h | 2 + .../Source/ASTextKitContext.mm | 2 +- submodules/AttachmentTextInputPanelNode/BUILD | 1 + .../AttachmentTextInputPanelNode.swift | 111 +- submodules/AttachmentUI/BUILD | 2 + .../Sources/AttachmentController.swift | 18 +- .../Sources/AttachmentPanel.swift | 64 +- ...ationSequenceCodeEntryControllerNode.swift | 6 +- .../AuthorizationSequenceController.swift | 80 +- ...tionSequencePhoneEntryControllerNode.swift | 12 +- ...uthorizationSequenceSignUpController.swift | 6 +- .../AvatarNode/Sources/AvatarNode.swift | 32 +- .../AvatarNode/Sources/PeerAvatar.swift | 10 +- .../Sources/AvatarVideoNode.swift | 20 +- .../Sources/BrowserInstantPageContent.swift | 4 +- .../Sources/CalendarMessageScreen.swift | 8 +- .../Sources/CallListController.swift | 12 +- .../Sources/CallListControllerNode.swift | 14 +- submodules/Camera/Sources/Camera.swift | 34 +- submodules/Camera/Sources/CameraDevice.swift | 4 + submodules/Camera/Sources/CameraMetrics.swift | 47 +- submodules/Camera/Sources/CameraOutput.swift | 2 +- .../Camera/Sources/CameraPreviewNode.swift | 69 - submodules/ChatImportUI/BUILD | 1 + .../Sources/ChatImportActivityScreen.swift | 54 +- .../Sources/ChatInterfaceState.swift | 168 +- .../ChatListSearchRecentPeersNode.swift | 4 +- .../ChatListUI/Sources/ChatContextMenus.swift | 8 +- .../Sources/ChatListController.swift | 8 +- .../Sources/ChatListControllerNode.swift | 2 + .../Sources/ChatListSearchContainerNode.swift | 4 +- .../Sources/ChatListSearchListPaneNode.swift | 6 +- .../Sources/ChatListShimmerNode.swift | 2 +- .../Sources/Node/ChatListItem.swift | 62 + .../Sources/Node/ChatListItemStrings.swift | 6 + .../Sources/ChatMessageBackground.swift | 111 +- .../ChatPanelInterfaceInteraction.swift | 12 +- .../ChatPresentationInterfaceState.swift | 191 +- .../Sources/ChatTextFormat.swift | 102 +- submodules/ChatSendMessageActionUI/BUILD | 1 + ...ChatSendMessageActionSheetController.swift | 10 +- ...SendMessageActionSheetControllerNode.swift | 148 +- .../Source/Components/HStack.swift | 3 +- .../Source/Components/Image.swift | 3 +- .../Source/Components/List.swift | 29 +- .../Sources/LottieAnimationComponent.swift | 3 + .../Sources/ReactionButtonListComponent.swift | 47 + .../ReactionListContextMenuContent.swift | 65 + .../Sources/SheetComponent.swift | 81 +- .../Sources/CreatePollTextInputItem.swift | 14 +- .../Sources/ContactListNode.swift | 64 +- .../Sources/ContactsSearchContainerNode.swift | 27 + .../InviteContactsControllerNode.swift | 2 +- submodules/ContextUI/BUILD | 4 + .../Sources/ContextActionsContainerNode.swift | 15 +- .../ContextUI/Sources/ContextController.swift | 617 ++- .../ContextControllerActionsStackNode.swift | 231 +- ...tControllerExtractedPresentationNode.swift | 340 +- .../ContextControllerPresentationNode.swift | 4 +- .../Sources/ContextSourceContainer.swift | 672 +++ .../ContextUI/Sources/PeekController.swift | 6 +- ...quenceCountrySelectionControllerNode.swift | 4 +- .../Sources/CountryList.swift | 8 +- submodules/Crc32/Package.swift | 2 +- submodules/CryptoUtils/Package.swift | 2 +- .../Sources/DatePickerNode.swift | 14 +- .../Sources/DebugController.swift | 74 +- .../ContainedViewLayoutTransition.swift | 61 +- .../Source/ContextControllerSourceNode.swift | 32 +- .../Source/ContextMenuContainerNode.swift | 31 +- .../Source/ContextMenuController.swift | 123 +- .../Display/Source/ContextMenuNode.swift | 275 -- .../Display/Source/EditableTextNode.swift | 16 + .../Display/Source/LinkHighlightingNode.swift | 23 +- submodules/Display/Source/ListView.swift | 37 +- .../Source/ListViewIntermediateState.swift | 61 +- .../Display/Source/ListViewItemHeader.swift | 4 +- .../Navigation/NavigationContainer.swift | 6 + .../Source/Navigation/NavigationLayout.swift | 5 +- .../Display/Source/PresentationContext.swift | 2 +- submodules/Display/Source/TextNode.swift | 1801 +++++--- .../Source/TooltipControllerNode.swift | 10 +- .../Display/Source/ViewController.swift | 6 +- .../DrawingUI/Sources/DrawingScreen.swift | 6 +- .../Sources/DrawingStickerEntity.swift | 4 +- .../DrawingUI/Sources/DrawingTextEntity.swift | 18 +- submodules/Emoji/Package.swift | 2 +- submodules/EncryptionProvider/Package.swift | 2 +- submodules/FFMpegBinding/BUILD | 3 + submodules/FFMpegBinding/Package.swift | 2 +- .../Public/FFMpegBinding/FFMpegAVIOContext.h | 2 + .../FFMpegBinding/Sources/FFMpegAVCodec.m | 8 +- .../Sources/FFMpegAVCodecContext.m | 4 + .../Sources/FFMpegAVFormatContext.m | 5 +- .../FFMpegBinding/Sources/FFMpegAVFrame.m | 6 +- .../FFMpegBinding/Sources/FFMpegAVIOContext.m | 2 + .../FFMpegBinding/Sources/FFMpegGlobals.m | 1 - .../FFMpegBinding/Sources/FFMpegPacket.m | 27 +- .../FFMpegBinding/Sources/FFMpegRemuxer.m | 14 +- .../FFMpegBinding/Sources/FFMpegSWResample.m | 9 +- .../ChatItemGalleryFooterContentNode.swift | 32 +- .../GalleryUI/Sources/GalleryController.swift | 2 +- .../Sources/Items/ChatImageGalleryItem.swift | 2 +- .../Items/UniversalVideoGalleryItem.swift | 6 +- .../Sources/RecognizedTextSelectionNode.swift | 2 +- .../SecretMediaPreviewController.swift | 2 +- submodules/GraphCore/Package.swift | 2 +- .../Sources/HashtagSearchController.swift | 2 +- .../Sources/ImageCompression.swift | 4 +- .../Sources/InAppPurchaseManager.swift | 312 +- .../Sources/CachedInternalInstantPages.swift | 8 +- .../Sources/InstantPageController.swift | 2 +- .../Sources/InstantPageControllerNode.swift | 6 +- .../Sources/InstantPageFeedbackNode.swift | 9 +- .../Sources/InstantPageLayout.swift | 2 +- .../InstantPagePeerReferenceNode.swift | 5 +- .../InstantPageReferenceControllerNode.swift | 2 +- .../InstantPageTextSelectionNode.swift | 2 +- submodules/InviteLinksUI/BUILD | 1 + .../FolderInviteLinkListController.swift | 6 +- .../Sources/InviteLinkEditController.swift | 1 + .../Sources/InviteLinkViewController.swift | 4 +- submodules/ItemListPeerItem/BUILD | 2 + .../Sources/ItemListPeerItem.swift | 305 +- .../Sources/ItemListController.swift | 5 +- .../ItemListControllerFooterItem.swift | 16 + .../Sources/ItemListControllerNode.swift | 155 +- .../Items/ItemListDisclosureItem.swift | 29 +- .../PublicHeaders/LegacyComponents/PGCamera.h | 2 +- .../LegacyComponents/PGCameraCaptureSession.h | 7 +- .../TGMediaAssetsController.h | 3 +- .../TGMediaPickerGalleryInterfaceView.h | 2 +- .../TGMediaPickerGalleryModel.h | 2 +- .../LegacyComponents/TGPhotoToolbarView.h | 3 +- .../TGVideoCameraGLRenderer.h | 6 +- .../LegacyComponents/TGVideoCameraGLView.h | 4 +- .../TGVideoCameraMovieRecorder.h | 3 +- .../LegacyComponents/Sources/PGCamera.m | 94 +- .../Sources/PGCameraCaptureSession.m | 10 +- .../Sources/TGCameraController.m | 22 +- .../LegacyComponents/Sources/TGImageBlur.m | 12 + .../Sources/TGMediaAssetsController.m | 3 +- .../TGMediaPickerGalleryInterfaceView.m | 8 +- .../Sources/TGMediaPickerGalleryModel.m | 6 +- .../TGMediaPickerGalleryVideoItemView.m | 2 + .../TGMediaPickerGalleryVideoScrubber.m | 117 +- .../Sources/TGMediaPickerModernGalleryMixin.m | 2 +- .../Sources/TGMediaPickerPhotoStripCell.m | 6 + .../Sources/TGPhotoToolbarView.m | 3 + .../Sources/TGPhotoVideoEditor.m | 2 +- .../Sources/TGVideoCameraGLRenderer.m | 156 +- .../Sources/TGVideoCameraGLView.m | 9 +- .../Sources/TGVideoCameraMovieRecorder.m | 6 +- .../Sources/TGVideoCameraPipeline.h | 9 +- .../Sources/TGVideoCameraPipeline.m | 60 +- .../Sources/TGVideoMessageCaptureController.m | 2 +- submodules/LegacyMediaPickerUI/BUILD | 1 - .../Sources/LegacyMediaPickers.swift | 85 +- submodules/LegacyUI/BUILD | 1 - .../LegacyUI/Sources/LegacyController.swift | 18 +- .../TelegramInitializeLegacyComponents.swift | 2 +- .../FetchPhotoLibraryImageResource.swift | 6 +- .../Sources/LocationActionListItem.swift | 9 +- .../LocationUI/Sources/LocationUtils.swift | 7 +- submodules/ManagedFile/Package.swift | 2 +- .../Sources/LegacyMediaPickerGallery.swift | 2 +- .../Sources/MediaPickerSelectedListNode.swift | 2 +- .../Sources/FFMpegAudioFrameDecoder.swift | 10 +- .../FFMpegMediaFrameSourceContext.swift | 10 +- .../Sources/SoftwareVideoSource.swift | 8 +- .../UniversalSoftwareVideoSource.swift | 2 +- .../Public/MozjpegBinding/MozjpegBinding.h | 2 +- .../MozjpegBinding/Sources/MozjpegBinding.mm | 18 +- submodules/MtProtoKit/Package.swift | 2 +- .../PublicHeaders/MtProtoKit/MTSignal.h | 5 + .../MtProtoKit/Sources/MTApiEnvironment.m | 8 + submodules/MtProtoKit/Sources/MTContext.m | 18 +- submodules/MtProtoKit/Sources/MTDNS.m | 4 +- submodules/MtProtoKit/Sources/MTDisposable.m | 235 +- .../Sources/MTNetworkAvailability.m | 14 +- submodules/MtProtoKit/Sources/MTProto.m | 7 +- submodules/MtProtoKit/Sources/MTSignal.m | 163 +- submodules/MtProtoKit/Sources/MTSubscriber.m | 30 +- .../MtProtoKit/Sources/MTTcpConnection.m | 4 +- submodules/MurMurHash32/Package.swift | 2 +- submodules/NetworkLogging/Package.swift | 2 +- .../Sources/NotificationSoundSelection.swift | 4 +- .../OpenSSLEncryptionProvider/Package.swift | 2 +- submodules/OpusBinding/Package.swift | 2 +- .../OpusBinding/Sources/opusenc/opusenc.m | 4 + .../LegacySecureIdAttachmentMenu.swift | 6 +- .../Pasteboard/Sources/Pasteboard.swift | 28 + .../Sources/ChannelAdminsController.swift | 2 +- .../ChannelPermissionsController.swift | 9 +- .../Sources/DeviceContactInfoController.swift | 4 +- .../GroupStickerPackSetupController.swift | 9 +- .../Sources/PhoneInputNode.swift | 9 + .../Sources/PhotoResources.swift | 73 +- submodules/Postbox/Package.swift | 2 +- .../Sources/MediaBoxFileContextV2Impl.swift | 4 + .../Postbox/Sources/MessageHistoryView.swift | 5 + .../Sources/MessageHistoryViewState.swift | 37 + submodules/Postbox/Sources/Postbox.swift | 21 +- .../Postbox/Sources/PostboxLogging.swift | 8 +- .../Postbox/Sources/SqliteValueBox.swift | 11 + submodules/Postbox/Sources/ViewTracker.swift | 3 + submodules/PremiumUI/BUILD | 13 +- .../Sources/CreateGiveawayController.swift | 1142 +++++ .../Sources/CreateGiveawayFooterItem.swift | 168 + .../Sources/CreateGiveawayHeaderItem.swift | 229 + .../PremiumUI/Sources/GiftOptionItem.swift | 662 +++ .../Sources/GiveawayInfoController.swift | 398 ++ .../Sources/PremiumBoostScreen.swift | 240 + .../PremiumUI/Sources/PremiumDemoScreen.swift | 6 + .../Sources/PremiumGiftCodeScreen.swift | 1181 +++++ .../PremiumUI/Sources/PremiumGiftScreen.swift | 64 +- .../Sources/PremiumIntroScreen.swift | 52 +- .../Sources/PremiumLimitScreen.swift | 651 ++- .../ReplaceBoostConfirmationController.swift | 135 +- .../Sources/ReplaceBoostScreen.swift | 1282 ++++++ .../Sources/SubscriptionsCountItem.swift | 372 ++ .../Sources/OpenUrl.swift | 69 +- .../QrCodeUI/Sources/QrCodeScanScreen.swift | 33 +- .../LegacyReachability/Package.swift | 2 +- submodules/Reachability/Package.swift | 2 +- .../Sources/ReactionContextNode.swift | 2 +- submodules/SSignalKit/Package.swift | 2 +- submodules/SSignalKit/SSignalKit/BUILD | 1 - submodules/SettingsUI/BUILD | 1 + .../BubbleSettingsController.swift | 6 +- .../Sources/DeleteAccountDataController.swift | 33 +- .../DeleteAccountOptionsController.swift | 12 + .../Sources/DeleteAccountPhoneItem.swift | 10 +- .../LocalizationListControllerNode.swift | 4 +- .../Sources/LogoutOptionsController.swift | 6 + .../DataPrivacySettingsController.swift | 6 +- .../ForwardPrivacyChatPreviewItem.swift | 17 +- .../PrivacyAndSecurityController.swift | 2 +- .../RecentSessionScreen.swift | 2 +- .../QuickReactionSetupController.swift | 5 +- .../Reactions/ReactionChatPreviewItem.swift | 13 +- .../InstalledStickerPacksController.swift | 9 +- .../TextSizeSelectionController.swift | 22 +- .../Sources/Themes/EditThemeController.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 20 +- .../Themes/ThemeGridSearchContentNode.swift | 6 + .../Themes/ThemePreviewControllerNode.swift | 26 +- .../Themes/ThemeSettingsChatPreviewItem.swift | 4 +- .../Themes/ThemeSettingsController.swift | 107 +- .../Sources/Themes/WallpaperGalleryItem.swift | 4 +- .../Sources/ShareController.swift | 38 +- .../StatisticsUI/Sources/BoostsTabsItem.swift | 267 ++ .../Sources/ChannelStatsController.swift | 367 +- .../Sources/MessageStatsController.swift | 2 +- .../Sources/StickerPackEmojisItem.swift | 2 + .../StickerPackPreviewController.swift | 6 + .../Sources/StickerPackScreen.swift | 8 +- .../StringTransliteration/Package.swift | 2 +- submodules/TelegramApi/Package.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 73 +- submodules/TelegramApi/Sources/Api1.swift | 52 +- submodules/TelegramApi/Sources/Api10.swift | 194 +- submodules/TelegramApi/Sources/Api11.swift | 162 +- submodules/TelegramApi/Sources/Api12.swift | 154 + submodules/TelegramApi/Sources/Api13.swift | 74 +- submodules/TelegramApi/Sources/Api14.swift | 112 +- submodules/TelegramApi/Sources/Api17.swift | 108 + submodules/TelegramApi/Sources/Api2.swift | 92 + submodules/TelegramApi/Sources/Api22.swift | 78 +- submodules/TelegramApi/Sources/Api28.swift | 546 +-- submodules/TelegramApi/Sources/Api29.swift | 798 ++-- submodules/TelegramApi/Sources/Api3.swift | 40 +- submodules/TelegramApi/Sources/Api30.swift | 88 + submodules/TelegramApi/Sources/Api31.swift | 280 +- submodules/TelegramApi/Sources/Api4.swift | 2 +- submodules/TelegramApi/Sources/Api5.swift | 38 +- submodules/TelegramApi/Sources/Api6.swift | 46 +- submodules/TelegramApi/Sources/Api8.swift | 52 + .../Sources/TelegramBaseController.swift | 2 +- .../Sources/CallRatingController.swift | 4 +- .../Components/MediaStreamComponent.swift | 78 +- .../Sources/PresentationCallManager.swift | 14 +- .../Sources/VoiceChatController.swift | 18 +- submodules/TelegramCore/Package.swift | 2 +- .../Sources/Account/Account.swift | 14 +- .../Account/AccountIntermediateState.swift | 4 +- .../Sources/Account/AccountManager.swift | 3 + .../AccountManager/AccountManagerImpl.swift | 11 + .../Sources/ApiUtils/ApiGroupOrChannel.swift | 12 +- .../Sources/ApiUtils/ChatContextResult.swift | 340 +- .../ApiUtils/StoreMessage_Telegram.swift | 213 +- .../ApiUtils/TelegramMediaAction.swift | 4 + .../ApiUtils/TelegramMediaWebpage.swift | 61 +- .../Sources/ApiUtils/TelegramUser.swift | 12 +- .../ApiUtils/TelegramUserPresence.swift | 2 +- .../TelegramCore/Sources/ForumChannels.swift | 23 +- .../Sources/MacOS/MacInternalUpdater.swift | 18 +- .../Network/FetchedMediaResource.swift | 6 +- .../Sources/Network/MultipartUpload.swift | 22 +- .../PendingMessages/EnqueueMessage.swift | 246 +- .../PendingMessageUploadedContent.swift | 27 + .../PendingUpdateMessageManager.swift | 8 +- .../PendingMessages/RequestEditMessage.swift | 24 +- .../StandaloneSendMessage.swift | 18 +- ...essageAutoremoveTimeoutInteractively.swift | 4 +- .../State/AccountStateManagementUtils.swift | 76 +- .../Sources/State/AccountStateManager.swift | 5 +- .../Sources/State/AccountTaskManager.swift | 1 + .../Sources/State/AccountViewTracker.swift | 5 +- .../Sources/State/ApplyUpdateMessage.swift | 2 +- .../Sources/State/ChannelBoost.swift | 433 +- .../State/ManagedAccountPresence.swift | 10 +- .../Sources/State/ManagedRecentStickers.swift | 28 + ...dSynchronizeChatInputStateOperations.swift | 63 +- .../Sources/State/PendingMessageManager.swift | 166 +- ...ecretChatIncomingDecryptedOperations.swift | 8 +- .../Sources/State/Serialization.swift | 2 +- .../Sources/State/UpdatesApiUtils.swift | 7 +- .../State/UserLimitsConfiguration.swift | 24 +- .../SyncCore/SyncCore_CachedUserData.swift | 53 + .../SyncCore/SyncCore_LoggingSettings.swift | 2 +- .../SyncCore/SyncCore_Namespaces.swift | 4 +- ...ingChatContextResultMessageAttribute.swift | 10 +- .../SyncCore_ReplyMessageAttribute.swift | 80 +- ...ncCore_SynchronizeableChatInputState.swift | 30 +- .../SyncCore/SyncCore_TelegramChannel.swift | 73 +- .../SyncCore_TelegramMediaAction.swift | 21 + .../SyncCore_TelegramMediaGiveaway.swift | 91 + .../SyncCore_TelegramMediaWebpage.swift | 33 +- .../SyncCore/SyncCore_TelegramUser.swift | 82 +- ...yncCore_TextEntitiesMessageAttribute.swift | 59 + .../TelegramEngineAccountData.swift | 4 + .../AccountData/UpdateAccountPeerName.swift | 52 +- .../TelegramEngine/Calls/GroupCalls.swift | 2 +- .../Data/ConfigurationData.swift | 20 +- .../Data/TelegramEngineData.swift | 3 + .../TelegramEngine/Messages/AdMessages.swift | 48 +- .../TelegramEngine/Messages/BotWebView.swift | 4 +- .../Messages/ClearCloudDrafts.swift | 14 +- .../Messages/LoadMessagesIfNecessary.swift | 21 +- .../TelegramEngine/Messages/Media.swift | 7 + ...OutgoingMessageWithChatContextResult.swift | 282 +- .../Messages/RequestStartBot.swift | 2 +- .../TelegramEngine/Messages/SendAsPeers.swift | 2 +- .../TelegramEngine/Messages/Stories.swift | 20 +- .../Messages/TelegramEngineMessages.swift | 20 +- .../TelegramEngine/Payments/AppStore.swift | 91 +- .../Payments/BotPaymentForm.swift | 94 +- .../TelegramEngine/Payments/GiftCodes.swift | 285 ++ .../Payments/TelegramEnginePayments.swift | 20 + .../TelegramEngine/Peers/AddressNames.swift | 2 +- .../Peers/ChannelAdminEventLogs.swift | 7 +- .../TelegramEngine/Peers/Communities.swift | 6 +- .../Peers/InactiveChannels.swift | 2 +- .../TelegramEngine/Peers/JoinLink.swift | 5 +- .../Sources/TelegramEngine/Peers/Peer.swift | 8 + .../Peers/RecentlySearchedPeerIds.swift | 22 +- .../Peers/ResolvePeerByName.swift | 32 +- .../TelegramEngine/Peers/SearchPeers.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 39 +- .../TelegramEngine/Peers/UpdatePeerInfo.swift | 39 + .../UpdatedAccountPrivacySettings.swift | 4 +- .../Stickers/SearchStickers.swift | 7 +- .../TelegramEngine/Utils/TempBox.swift | 1 + .../TelegramCore/Sources/UpdatePeers.swift | 8 +- .../TelegramCore/Sources/Utils/Log.swift | 10 + .../Sources/Utils/MessageUtils.swift | 11 + .../Sources/Utils/PeerUtils.swift | 30 + .../TelegramCore/Sources/WebpagePreview.swift | 88 +- .../Sources/TelegramIntents.swift | 2 +- .../TelegramNotices/Sources/Notices.swift | 113 + .../Sources/ChatPresentationData.swift | 19 + .../DefaultDarkPresentationTheme.swift | 4 +- .../PresentationThemeEssentialGraphics.swift | 24 +- .../Resources/PresentationResourceKey.swift | 8 + .../Resources/PresentationResourcesChat.swift | 60 + .../PresentationResourcesItemList.swift | 12 + .../Sources/Geo.swift | 12 + .../Sources/MessageContentKind.swift | 20 + .../Sources/ServiceMessageStrings.swift | 5 + submodules/TelegramUI/BUILD | 69 + .../Components/AudioWaveformNode/BUILD | 20 + .../Sources/AudioWaveformNode.swift | 14 +- .../Sources/AvatarEditorScreen.swift | 6 +- .../Components/ButtonComponent/BUILD | 1 + .../Sources/ButtonComponent.swift | 75 +- .../Sources/CameraButtonComponent.swift | 37 +- .../TelegramUI/Components/CameraScreen/BUILD | 1 + .../MetalResources/cameraScreen.metal | 5 +- .../CameraScreen/Sources/CameraScreen.swift | 420 +- .../Sources/CaptureControlsComponent.swift | 55 +- .../Sources/FlashTintControlComponent.swift | 466 ++ .../CameraScreen/Sources/ModeComponent.swift | 31 +- .../Sources/ShutterBlobView.swift | 16 +- .../Components/Chat/ChatBotInfoItem/BUILD | 32 + .../Sources/ChatBotInfoItem.swift | 157 +- .../Chat/ChatBotStartInputPanelNode/BUILD | 27 + .../Sources/ChatBotStartInputPanelNode.swift | 17 +- .../Chat/ChatButtonKeyboardInputNode/BUILD | 28 + .../Sources/ChatButtonKeyboardInputNode.swift | 14 +- .../ChatChannelSubscriberInputPanelNode/BUILD | 29 + .../ChatChannelSubscriberInputPanelNode.swift | 15 +- .../Chat/ChatContextResultPeekContent/BUILD | 27 + .../ChatContextResultPeekContent.swift} | 24 +- .../Components/Chat/ChatHistoryEntry/BUILD | 23 + .../Sources/ChatHistoryEntry.swift | 40 +- .../Chat/ChatInputContextPanelNode/BUILD | 25 + .../Sources/ChatInputContextPanelNode.swift | 42 + .../Components/Chat/ChatInputPanelNode/BUILD | 23 + .../Sources/ChatInputPanelNode.swift | 43 + .../Components/Chat/ChatInputTextNode/BUILD | 22 + .../ChatInputTextViewImpl/BUILD | 23 + .../ChatInputTextViewImpl.h | 28 + .../Sources/ChatInputTextViewImpl.m | 143 + .../Sources/ChatInputTextNode.swift | 833 ++++ .../ChatInstantVideoMessageDurationNode/BUILD | 21 + .../ChatInstantVideoMessageDurationNode.swift | 18 +- .../Components/Chat/ChatLoadingNode/BUILD | 34 + .../Sources/ChatLoadingNode.swift | 72 +- .../ChatMessageActionBubbleContentNode/BUILD | 38 + .../ChatMessageActionBubbleContentNode.swift} | 63 +- .../Chat/ChatMessageActionButtonsNode/BUILD | 24 + .../ChatMessageActionButtonsNode.swift | 64 +- .../ChatMessageAnimatedStickerItemNode/BUILD | 60 + .../ChatMessageAnimatedStickerItemNode.swift | 663 +-- .../BUILD | 23 + ...ChatMessageAttachedContentButtonNode.swift | 181 + .../Chat/ChatMessageAttachedContentNode/BUILD | 47 + .../ChatMessageAttachedContentNode.swift | 2289 +++++++++ .../Chat/ChatMessageBubbleContentNode/BUILD | 29 + ...eBubbleContentCalclulateImageCorners.swift | 3 +- .../ChatMessageBubbleContentNode.swift | 305 ++ .../Chat/ChatMessageBubbleItemNode/BUILD | 86 + .../Sources/ChatMessageBubbleItemNode.swift | 1177 +++-- .../ChatMessageCallBubbleContentNode/BUILD | 26 + .../ChatMessageCallBubbleContentNode.swift | 31 +- .../ChatMessageCommentFooterContentNode/BUILD | 28 + .../ChatMessageCommentFooterContentNode.swift | 26 +- .../ChatMessageContactBubbleContentNode/BUILD | 30 + .../ChatMessageContactBubbleContentNode.swift | 57 +- .../Chat/ChatMessageDateAndStatusNode/BUILD | 32 + .../ChatMessageDateAndStatusNode.swift | 65 +- .../StringForMessageTimestampStatus.swift | 6 +- .../Chat/ChatMessageDeliveryFailedNode/BUILD | 20 + .../ChatMessageDeliveryFailedNode.swift | 8 +- .../BUILD | 25 + ...entLogPreviousDescriptionContentNode.swift | 29 +- .../BUILD | 25 + ...ssageEventLogPreviousLinkContentNode.swift | 32 +- .../BUILD | 25 + ...geEventLogPreviousMessageContentNode.swift | 31 +- .../ChatMessageFileBubbleContentNode/BUILD | 29 + .../ChatMessageFileBubbleContentNode.swift | 46 +- .../Sources/ChatMessageForwardInfoNode.swift | 15 +- .../ChatMessageGameBubbleContentNode/BUILD | 25 + .../ChatMessageGameBubbleContentNode.swift | 37 +- .../ChatMessageGiftBubbleContentNode/BUILD | 37 + .../ChatMessageGiftBubbleContentNode.swift} | 177 +- .../BUILD | 35 + ...ChatMessageGiveawayBubbleContentNode.swift | 831 ++++ .../BUILD | 30 + ...MessageInstantVideoBubbleContentNode.swift | 77 +- .../ChatMessageInstantVideoItemNode/BUILD | 45 + .../ChatMessageInstantVideoItemNode.swift | 223 +- .../Chat/ChatMessageInteractiveFileNode/BUILD | 57 + .../ChatMessageInteractiveFileNode.swift | 211 +- .../BUILD | 45 + ...atMessageInteractiveInstantVideoNode.swift | 255 +- .../ChatMessageInteractiveMediaNode/BUILD | 46 + .../ChatMessageInteractiveMediaNode.swift | 98 +- .../ChatMessageInvoiceBubbleContentNode/BUILD | 27 + .../ChatMessageInvoiceBubbleContentNode.swift | 35 +- .../Components/Chat/ChatMessageItem/BUILD | 27 + .../Sources/ChatMessageItem.swift | 170 + .../Chat/ChatMessageItemCommon/BUILD | 22 + .../Sources/ChatMessageItemCommon.swift | 298 ++ .../Components/Chat/ChatMessageItemImpl/BUILD | 42 + .../Sources/ChatMessageDateHeader.swift | 145 +- .../Sources/ChatMessageItemImpl.swift} | 175 +- .../Sources/ChatReplyCountItem.swift | 37 +- .../Sources/ChatUnreadItem.swift | 37 +- .../Components/Chat/ChatMessageItemView/BUILD | 31 + .../Sources/ChatMessageItemView.swift | 242 +- .../ChatMessageMapBubbleContentNode/BUILD | 33 + .../ChatMessageLiveLocationTextNode.swift | 0 .../ChatMessageMapBubbleContentNode.swift | 37 +- .../ChatMessageMediaBubbleContentNode/BUILD | 31 + .../ChatMessageMediaBubbleContentNode.swift | 47 +- .../ChatMessagePollBubbleContentNode/BUILD | 32 + .../ChatMessagePollBubbleContentNode.swift | 175 +- .../BUILD | 37 + ...ageProfilePhotoSuggestionContentNode.swift | 22 +- .../BUILD | 32 + ...hatMessageReactionsFooterContentNode.swift | 144 +- .../Chat/ChatMessageReplyInfoNode/BUILD | 34 + .../Sources/ChatMessageReplyInfoNode.swift | 474 +- .../BUILD | 28 + ...atMessageRestrictedBubbleContentNode.swift | 17 +- .../Chat/ChatMessageSelectionNode/BUILD | 21 + .../Sources/ChatMessageSelectionNode.swift | 14 +- .../Chat/ChatMessageShareButton/BUILD | 26 + .../Sources/ChatMessageShareButton.swift | 180 + .../Chat/ChatMessageStickerItemNode/BUILD | 45 + .../Sources/ChatMessageStickerItemNode.swift | 416 +- .../ChatMessageStoryMentionContentNode/BUILD | 40 + .../ChatMessageStoryMentionContentNode.swift | 22 +- .../Chat/ChatMessageSwipeToReplyNode/BUILD | 22 + .../Sources/ChatMessageSwipeToReplyNode.swift | 16 +- .../ChatMessageTextBubbleContentNode/BUILD | 44 + .../ChatMessageTextBubbleContentNode.swift | 699 ++- .../Chat/ChatMessageThreadInfoNode/BUILD | 36 + .../Sources/ChatMessageThreadInfoNode.swift | 36 +- .../Chat/ChatMessageTransitionNode/BUILD | 19 + .../Sources/ChatMessageTransitionNode.swift | 15 + .../BUILD | 26 + ...tMessageUnsupportedBubbleContentNode.swift | 46 +- .../BUILD | 38 + ...hatMessageWallpaperBubbleContentNode.swift | 27 +- .../ChatMessageWebpageBubbleContentNode/BUILD | 37 + .../ChatMessageWebpageBubbleContentNode.swift | 211 +- .../Chat/ChatNavigationButton/BUILD | 17 + .../Sources/ChatNavigationButton.swift | 27 + .../Chat/ChatOverscrollControl/BUILD | 27 + .../Sources/ChatOverscrollControl.swift | 18 +- .../Chat/ChatRecentActionsController/BUILD | 54 + .../Sources/ChatRecentActionsController.swift | 19 +- .../ChatRecentActionsControllerNode.swift | 54 +- .../Sources/ChatRecentActionsEmptyNode.swift | 10 +- .../ChatRecentActionsFilterController.swift | 0 .../ChatRecentActionsHistoryTransition.swift | 178 +- .../ChatRecentActionsInteraction.swift | 0 ...ntActionsSearchNavigationContentNode.swift | 0 .../Sources/ChatRecentActionsTitleView.swift | 0 .../Chat/ChatSwipeToReplyRecognizer/BUILD | 17 + .../Sources/ChatSwipeToReplyRecognizer.swift | 25 +- .../Chat/EditableTokenListNode/BUILD | 23 + .../Sources/EditableTokenListNode.swift | 63 +- .../Sources/ForwardAccessoryPanelNode.swift | 22 +- .../Chat/InstantVideoRadialStatusNode/BUILD | 23 + .../InstantVideoRadialStatusNode.swift | 28 +- .../Chat/ManagedDiceAnimationNode/BUILD | 25 + .../Sources/ManagedDiceAnimationNode.swift | 20 +- .../Components/Chat/MessageHaptics/BUILD | 19 + .../Sources/CoffinHaptic.swift | 11 +- .../Sources/HeartbeatHaptic.swift | 13 +- .../MessageHaptics}/Sources/PeachHaptic.swift | 11 +- .../MessageInlineBlockBackgroundView/BUILD | 26 + .../MessageInlineBlockBackgroundView.swift | 836 ++++ .../Chat/MessageQuoteComponent/BUILD | 21 + .../Sources/MessageQuoteComponent.swift | 67 + .../Components/Chat/PollBubbleTimerNode/BUILD | 19 + .../Sources/PollBubbleTimerNode.swift | 8 +- .../Chat/ReplyAccessoryPanelNode/BUILD | 37 + .../Sources/ReplyAccessoryPanelNode.swift | 233 +- .../Components/Chat/ShimmeringLinkNode/BUILD | 20 + .../Sources/ShimmeringLinkNode.swift | 18 +- .../Sources/ChatControllerInteraction.swift | 67 +- .../ChatEntityKeyboardInputNode/BUILD | 1 + .../Sources/ChatEntityKeyboardInputNode.swift | 52 +- .../Sources/LockView.swift | 2 +- .../Components/CompositeTextNode/BUILD | 19 + .../Sources/CompositeTextNode.swift | 117 + .../Components/ContextMenuScreen/BUILD | 24 + .../Sources}/ContextMenuActionNode.swift | 53 +- .../Sources/ContextMenuController.swift | 111 + .../Sources/ContextMenuNode.swift | 320 ++ .../EmojiStatusSelectionComponent.swift | 2 + .../Sources/EmojiTextAttachmentView.swift | 4 +- .../Sources/EmojiPagerContentComponent.swift | 254 +- .../Sources/EntityKeyboard.swift | 17 +- .../EntityKeyboardTopPanelComponent.swift | 47 +- .../Sources/GifContext.swift | 6 + .../Sources/ForumCreateTopicScreen.swift | 13 +- .../Components/ItemListDatePickerItem/BUILD | 23 + .../Sources/ItemListDatePickerItem.swift | 16 +- .../LegacyInstantVideoController.swift | 8 +- .../Sources/LegacyMessageInputPanel.swift | 6 + .../Sources/LottieComponent.swift | 4 +- .../Sources/MediaEditorScreen.swift | 29 +- .../MessageInputActionButtonComponent.swift | 1 + .../NotificationExceptionsScreen.swift | 2 +- .../Sources/PeerInfoStoryGridScreen.swift | 6 +- .../Sources/PeerInfoVisualMediaPaneNode.swift | 4 +- .../Sources/PeerSelectionControllerNode.swift | 8 +- .../PremiumGiftAttachmentScreen/BUILD | 25 + .../Sources/PremiumGiftAttachmentScreen.swift | 58 + .../Sources/SendInviteLinkScreen.swift | 2 +- .../Settings/PeerNameColorScreen/BUILD | 33 + .../Sources/ApplyColorFooterItem.swift | 148 + .../Sources/EmojiPickerItem.swift | 388 ++ .../PeerNameColorChatPreviewItem.swift | 391 ++ .../Sources/PeerNameColorItem.swift | 562 +++ .../Sources/PeerNameColorScreen.swift | 642 +++ .../Components/ShareExtensionContext/BUILD | 1 + .../Sources/ShareExtensionContext.swift | 1007 ++-- .../Components/ShareWithPeersScreen/BUILD | 2 + .../CountriesMultiselectionScreen.swift | 1196 +++++ .../Sources/CountryListItemComponent.swift | 225 + .../Sources/ShareWithPeersScreen.swift | 906 +--- .../Sources/ShareWithPeersScreenState.swift | 723 +++ .../Sources/StorageUsageScreen.swift | 6 +- .../Sources/PeerListItemComponent.swift | 38 +- .../Stories/StoryContainerScreen/BUILD | 1 + .../MediaNavigationStripComponent.swift | 39 +- .../Sources/StoryContainerScreen.swift | 154 +- .../StoryContentCaptionComponent.swift | 5 +- .../StoryInteractionGuideComponent.swift | 450 ++ .../Sources/StoryItemContentComponent.swift | 52 +- .../Sources/StoryItemLoadingEffectView.swift | 24 - .../StoryItemSetContainerComponent.swift | 123 +- ...StoryItemSetContainerViewSendMessage.swift | 59 +- .../Sources/StoryFooterPanelComponent.swift | 5 +- .../Sources/TabSelectorComponent.swift | 25 +- .../Sources/TextFieldComponent.swift | 21 +- .../Components/TextLoadingEffect/BUILD | 20 + .../Sources/TextLoadingEffect.swift | 149 + .../Sources/TextNodeWithEntities.swift | 4 +- .../Sources/EditableTokenListNode.swift | 30 +- .../Sources/TokenListTextField.swift | 9 + .../Utils/RoundedRectWithTailPath/BUILD | 18 + .../Sources/RoundedRectWithTailPath.swift | 130 + .../Components/WallpaperPreviewMedia/BUILD | 19 + .../Sources/WallpaperPreviewMedia.swift | 24 +- .../AddRoundIcon.imageset/AddPlus.pdf | Bin 0 -> 1966 bytes .../AddRoundIcon.imageset/Contents.json | 12 + .../CaptionHide.imageset/Contents.json | 12 + .../CaptionHide.imageset/hidecaption_24.pdf | Bin 0 -> 5259 bytes .../CaptionShow.imageset/Contents.json | 12 + .../CaptionShow.imageset/showcaption_24.pdf | Bin 0 -> 5347 bytes .../ImageEnlarge.imageset/Contents.json | 12 + .../ImageEnlarge.imageset/enlarge_24.pdf | Bin 0 -> 4978 bytes .../ImageShrink.imageset/Contents.json | 12 + .../ImageShrink.imageset/shrink_24.pdf | Bin 0 -> 4968 bytes .../MoveDown.imageset/Contents.json | 12 + .../MoveDown.imageset/movedown_24.pdf | Bin 0 -> 2218 bytes .../MoveUp.imageset/Contents.json | 12 + .../MoveUp.imageset/moveup_24.pdf | Bin 0 -> 2237 bytes .../Context Menu/Quote.imageset/Contents.json | 12 + .../Context Menu/Quote.imageset/IconQuote.svg | 3 + .../QuoteRemove.imageset/Contents.json | 12 + .../QuoteRemove.imageset/IconQuoteRemove.svg | 3 + .../QuoteSelected.imageset/Contents.json | 12 + .../IconQuoteSelected.svg | 3 + .../UserCrossed.imageset/Contents.json | 12 + .../UserCrossed.imageset/hideforward_24.pdf | Bin 0 -> 2746 bytes .../Info/NameColorIcon.imageset/Contents.json | 12 + .../Info/NameColorIcon.imageset/brush_30.pdf | Bin 0 -> 4628 bytes .../Contents.json | 12 + .../forwardtools_30.pdf | Bin 0 -> 5030 bytes .../LinkSettingsIcon.imageset/Contents.json | 12 + .../linktools_30.pdf | Bin 0 -> 3815 bytes .../Contents.json | 12 + .../PanelTextChannelIcon.imageset/channel.pdf | Bin 0 -> 2267 bytes .../PanelTextGroupIcon.imageset/Contents.json | 12 + .../PanelTextGroupIcon.imageset/group.pdf | Bin 0 -> 3052 bytes .../ReplyQuoteIcon.imageset/Contents.json | 12 + .../ReplyQuoteIcon.imageset/replyquote_30.pdf | Bin 0 -> 4683 bytes .../ReplySettingsIcon.imageset/Contents.json | 12 + .../replytools_30.pdf | Bin 0 -> 5029 bytes .../Contents.json | 20 +- ...sationInstantPageButtonIconIncoming@2x.png | Bin 367 -> 0 bytes ...sationInstantPageButtonIconIncoming@3x.png | Bin 447 -> 0 bytes .../InstantIcon.svg | 3 + .../ReplyQuoteIcon.imageset/Contents.json | 12 + .../ReplyQuoteIcon.imageset/quotemini.pdf | Bin 0 -> 2878 bytes .../Premium/AddBoosts.imageset/Contents.json | 12 + .../AddBoosts.imageset/addboosts_30.pdf | Bin 0 -> 3623 bytes .../AvatarBoost.imageset/Contents.json | 2 +- .../{AvatarBoost.pdf => Reassign.pdf} | Bin 4192 -> 3622 bytes .../BoostButtonIcon.imageset/Contents.json | 12 + .../BoostButtonIcon.imageset/Union.pdf | Bin 0 -> 1249 bytes .../BoostReplaceIcon.imageset/Contents.json | 12 + .../replacedboost_30.pdf | Bin 0 -> 6171 bytes .../Premium/Giveaway.imageset/Contents.json | 12 + .../Premium/Giveaway.imageset/giveaway_30.pdf | Bin 0 -> 5593 bytes .../Premium/NoIcon.imageset/Contents.json | 12 + .../Premium/NoIcon.imageset/noicon_30.pdf | Bin 0 -> 1820 bytes .../ToBeDistributed.imageset/Contents.json | 12 + .../avatar_tobedistributed.pdf | Bin 0 -> 5365 bytes .../Premium/Unclaimed.imageset/Contents.json | 12 + .../Unclaimed.imageset/avatar_unclaimed.pdf | Bin 0 -> 2196 bytes .../Resources/Animations/flash_onToOff.json | 1 + .../message_preview_caption_off.json | 1 + .../message_preview_caption_on.json | 1 + .../message_preview_media_large.json | 1 + .../message_preview_media_small.json | 1 + .../message_preview_person_off.json | 1 + .../Animations/message_preview_person_on.json | 1 + .../message_preview_sort_above.json | 1 + .../message_preview_sort_below.json | 1 + .../Resources/Animations/story_back.tgs | Bin 0 -> 3576 bytes .../Resources/Animations/story_forward.tgs | Bin 0 -> 3374 bytes .../Resources/Animations/story_move.tgs | Bin 0 -> 2563 bytes .../Resources/Animations/story_pause.tgs | Bin 0 -> 2168 bytes .../TelegramUI/Sources/AccountContext.swift | 23 +- .../TelegramUI/Sources/AppDelegate.swift | 43 +- .../Sources/ApplicationContext.swift | 13 +- .../Sources/AttachmentFileController.swift | 6 + .../Chat/ChatMessageActionOptions.swift | 914 ++++ ...ChatMessageDisplaySendMessageOptions.swift | 130 + ...UpdateChatPresentationInterfaceState.swift | 516 +++ .../TelegramUI/Sources/ChatController.swift | 2754 +++++------ .../Sources/ChatControllerNode.swift | 370 +- .../Sources/ChatDateSelectionSheet.swift | 122 - .../Sources/ChatEditMessageMediaContext.swift | 31 - .../TelegramUI/Sources/ChatEmptyNode.swift | 1 + .../ChatFeedNavigationInputPanelNode.swift | 75 - .../Sources/ChatHistoryEntriesForView.swift | 2 + .../Sources/ChatHistoryListNode.swift | 103 +- .../ChatHistorySearchContainerNode.swift | 7 +- .../Sources/ChatHistoryViewForLocation.swift | 25 +- .../TelegramUI/Sources/ChatHoleItem.swift | 118 - .../Sources/ChatInputContextPanelNode.swift | 42 - .../Sources/ChatInputPanelNode.swift | 43 - .../ChatInterfaceInputContextPanels.swift | 1 + .../Sources/ChatInterfaceInputNodes.swift | 2 + .../ChatInterfaceStateAccessoryPanels.swift | 34 +- .../ChatInterfaceStateContextMenus.swift | 84 +- .../ChatInterfaceStateContextQueries.swift | 47 +- .../ChatInterfaceStateInputPanels.swift | 9 +- .../ChatInterfaceStateNavigationButtons.swift | 27 +- .../ChatInterfaceTitlePanelNodes.swift | 2 +- .../ChatMessageAttachedContentNode.swift | 1328 ------ .../ChatMessageBubbleContentNode.swift | 264 -- ...essageContextControllerContentSource.swift | 2 +- .../ChatMessageReportInputPanelNode.swift | 1 + .../ChatMessageSelectionInputPanelNode.swift | 1 + .../Sources/ChatMessageTransitionNode.swift | 191 +- .../ChatPinnedMessageTitlePanelNode.swift | 3 +- .../ChatRecordingPreviewInputPanelNode.swift | 3 +- .../ChatRestrictedInputPanelNode.swift | 1 + .../Sources/ChatSearchInputPanelNode.swift | 1 + .../ChatSearchResultsContollerNode.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 291 +- .../TelegramUI/Sources/ChatThemeScreen.swift | 4 + .../Sources/ChatThemeScreen.swift.orig | 1238 ----- .../Sources/ChatUnblockInputPanelNode.swift | 1 + .../CommandChatInputContextPanelNode.swift | 1 + ...CommandMenuChatInputContextPanelNode.swift | 1 + .../ContactMultiselectionController.swift | 1 + .../ContactMultiselectionControllerNode.swift | 1 + .../Sources/ContactSelectionController.swift | 5 +- .../ContactSelectionControllerNode.swift | 13 +- .../Sources/DeleteChatInputPanelNode.swift | 1 + ...textResultsChatInputContextPanelNode.swift | 1 + .../TelegramUI/Sources/EmojiResources.swift | 39 - .../EmojisChatInputContextPanelNode.swift | 1 + .../TelegramUI/Sources/GridHoleItem.swift | 36 - .../TelegramUI/Sources/GridMessageItem.swift | 471 -- .../HashtagChatInputContextPanelNode.swift | 1 + ...textResultsChatInputContextPanelNode.swift | 2 + ...rizontalStickersChatContextPanelNode.swift | 1 + .../Sources/InChatPrefetchManager.swift | 1 + .../Sources/InlineReactionSearchPanel.swift | 1 + .../Sources/MakeTempAccountContext.swift | 56 + .../MentionChatInputContextPanelNode.swift | 1 + .../Sources/NavigateToChatController.swift | 7 +- .../TelegramUI/Sources/OpenChatMessage.swift | 6 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 216 +- .../OverlayAudioPlayerControllerNode.swift | 19 +- .../OverlayInstantVideoDecoration.swift | 1 + .../ListItems/PeerInfoScreenActionItem.swift | 2 +- .../PeerInfoScreenDisclosureItem.swift | 51 +- .../ListItems/PeerInfoScreenMemberItem.swift | 8 +- .../PeerInfoGroupsInCommonPaneNode.swift | 2 +- .../PeerInfo/Panes/PeerInfoListPaneNode.swift | 11 +- .../PeerInfo/Panes/PeerInfoMembersPane.swift | 3 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoMembers.swift | 6 +- .../PeerInfo/PeerInfoPaneContainerNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 467 +- .../Sources/PollResultsController.swift | 4 +- .../TelegramUI/Sources/PrefetchManager.swift | 2 + .../PreparedChatHistoryViewTransition.swift | 26 +- ...retChatHandshakeStatusInputPanelNode.swift | 1 + .../Sources/SharedAccountContext.swift | 52 +- .../Sources/TelegramRootController.swift | 17 +- .../TelegramUI/Sources/TextLinkHandling.swift | 17 +- .../TransformOutgoingMessageMedia.swift | 6 +- ...textResultsChatInputContextPanelNode.swift | 1 + .../WebpagePreviewAccessoryPanelNode.swift | 51 +- .../Sources/module.private.modulemap | 3 - .../Sources/ExperimentalUISettings.swift | 10 +- submodules/TelegramVoip/Package.swift | 2 +- .../Sources/GroupCallContext.swift | 23 +- ...annelMemberCategoriesContextsManager.swift | 4 +- .../Sources/ChatTextInputAttributes.swift | 260 +- .../TextFormat/Sources/CountNicePercent.swift | 77 + .../Sources/GenerateTextEntities.swift | 13 +- .../Sources/StringWithAppliedEntities.swift | 138 +- .../Sources/TelegramAttributes.swift | 1 + submodules/TextFormat/Sources/TextFormat.h | 19 - .../Sources/TextSelectionNode.swift | 214 +- submodules/TgVoipWebrtc/tgcalls | 2 +- .../TranslateUI/Sources/ChatTranslation.swift | 4 +- .../Sources/UndoOverlayController.swift | 4 +- .../Sources/UndoOverlayControllerNode.swift | 23 +- .../UrlHandling/Sources/UrlHandling.swift | 420 +- submodules/Utils/DarwinDirStat/Package.swift | 2 +- submodules/Utils/RangeSet/Package.swift | 2 +- .../Sources/WallpaperBackgroundNode.swift | 1 + .../Sources/WatchRequestHandlers.swift | 16 +- .../Sources/LegacyWebSearchGallery.swift | 2 +- .../Sources/WebSearchController.swift | 7 +- .../WebUI/Sources/WebAppController.swift | 110 +- submodules/WebUI/Sources/WebAppWebView.swift | 6 +- submodules/YuvConversion/Package.swift | 2 +- submodules/libphonenumber/Package.swift | 2 +- submodules/sqlcipher/Package.swift | 2 +- swift_deps.bzl | 6 +- swift_deps_index.json | 8 +- third-party/libjxl/build-libjxl-bazel.sh | 10 +- .../mozjpeg/.github/pull_request_template.md | 8 + third-party/mozjpeg/mozjpeg/BUILDING.md | 296 +- third-party/mozjpeg/mozjpeg/CMakeLists.txt | 505 +- third-party/mozjpeg/mozjpeg/ChangeLog.md | 569 ++- third-party/mozjpeg/mozjpeg/LICENSE.md | 2 +- .../mozjpeg/mozjpeg/README-mozilla.txt | 4 +- third-party/mozjpeg/mozjpeg/README.ijg | 37 +- third-party/mozjpeg/mozjpeg/README.md | 12 +- third-party/mozjpeg/mozjpeg/appveyor.yml | 26 - third-party/mozjpeg/mozjpeg/cderror.h | 28 +- third-party/mozjpeg/mozjpeg/cdjpeg.c | 49 +- third-party/mozjpeg/mozjpeg/cdjpeg.h | 27 +- third-party/mozjpeg/mozjpeg/change.log | 31 +- third-party/mozjpeg/mozjpeg/cjpeg.1 | 73 +- third-party/mozjpeg/mozjpeg/cjpeg.c | 163 +- .../mozjpeg/cmakescripts/BuildPackages.cmake | 110 +- .../mozjpeg/cmakescripts/GNUInstallDirs.cmake | 3 + .../mozjpeg/cmakescripts/PackageInfo.cmake | 13 + third-party/mozjpeg/mozjpeg/cmyk.h | 1 - third-party/mozjpeg/mozjpeg/croptest.in | 95 + third-party/mozjpeg/mozjpeg/djpeg.1 | 110 +- third-party/mozjpeg/mozjpeg/djpeg.c | 155 +- .../mozjpeg/mozjpeg/doc/html/annotated.html | 69 +- .../mozjpeg/mozjpeg/doc/html/classes.html | 79 +- .../mozjpeg/doc/html/{ftv2doc.png => doc.png} | Bin .../mozjpeg/mozjpeg/doc/html/doxygen.css | 708 ++- .../mozjpeg/mozjpeg/doc/html/doxygen.png | Bin 3779 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/doxygen.svg | 26 + .../mozjpeg/mozjpeg/doc/html/dynsections.js | 82 +- ...{ftv2folderclosed.png => folderclosed.png} | Bin .../{ftv2folderopen.png => folderopen.png} | Bin .../mozjpeg/mozjpeg/doc/html/ftv2blank.png | Bin 86 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2cl.png | Bin 453 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2lastnode.png | Bin 86 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2link.png | Bin 746 -> 0 bytes .../mozjpeg/doc/html/ftv2mlastnode.png | Bin 246 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2mnode.png | Bin 246 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2mo.png | Bin 403 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2node.png | Bin 86 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2ns.png | Bin 388 -> 0 bytes .../mozjpeg/doc/html/ftv2plastnode.png | Bin 229 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2pnode.png | Bin 229 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/ftv2vertline.png | Bin 86 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/functions.html | 71 +- .../mozjpeg/doc/html/functions_vars.html | 71 +- .../doc/html/group___turbo_j_p_e_g.html | 928 ++-- .../mozjpeg/mozjpeg/doc/html/index.html | 56 +- .../mozjpeg/mozjpeg/doc/html/jquery.js | 43 +- third-party/mozjpeg/mozjpeg/doc/html/menu.js | 51 + .../mozjpeg/mozjpeg/doc/html/menudata.js | 33 + .../mozjpeg/mozjpeg/doc/html/modules.html | 58 +- .../html/search/{all_63.html => all_0.html} | 16 +- .../mozjpeg/mozjpeg/doc/html/search/all_0.js | 4 + .../html/search/{all_64.html => all_1.html} | 16 +- .../mozjpeg/mozjpeg/doc/html/search/all_1.js | 5 + .../html/search/{all_68.html => all_2.html} | 16 +- .../mozjpeg/mozjpeg/doc/html/search/all_2.js | 4 + .../html/search/{all_6e.html => all_3.html} | 16 +- .../mozjpeg/mozjpeg/doc/html/search/all_3.js | 4 + .../mozjpeg/doc/html/search/all_4.html | 36 + .../mozjpeg/mozjpeg/doc/html/search/all_4.js | 5 + .../mozjpeg/doc/html/search/all_5.html | 36 + .../mozjpeg/mozjpeg/doc/html/search/all_5.js | 4 + .../mozjpeg/doc/html/search/all_6.html | 36 + .../mozjpeg/mozjpeg/doc/html/search/all_6.js | 103 + .../mozjpeg/mozjpeg/doc/html/search/all_63.js | 4 - .../mozjpeg/mozjpeg/doc/html/search/all_64.js | 5 - .../mozjpeg/mozjpeg/doc/html/search/all_68.js | 4 - .../mozjpeg/mozjpeg/doc/html/search/all_6e.js | 4 - .../mozjpeg/doc/html/search/all_6f.html | 26 - .../mozjpeg/mozjpeg/doc/html/search/all_6f.js | 5 - .../mozjpeg/doc/html/search/all_7.html | 36 + .../mozjpeg/mozjpeg/doc/html/search/all_7.js | 4 + .../mozjpeg/doc/html/search/all_72.html | 26 - .../mozjpeg/mozjpeg/doc/html/search/all_72.js | 4 - .../mozjpeg/doc/html/search/all_74.html | 26 - .../mozjpeg/mozjpeg/doc/html/search/all_74.js | 102 - .../mozjpeg/doc/html/search/all_77.html | 26 - .../mozjpeg/mozjpeg/doc/html/search/all_77.js | 4 - .../mozjpeg/doc/html/search/all_78.html | 26 - .../mozjpeg/mozjpeg/doc/html/search/all_78.js | 4 - .../mozjpeg/doc/html/search/all_79.html | 26 - .../mozjpeg/mozjpeg/doc/html/search/all_79.js | 4 - .../mozjpeg/doc/html/search/all_8.html | 36 + .../mozjpeg/mozjpeg/doc/html/search/all_8.js | 4 + .../mozjpeg/doc/html/search/all_9.html | 36 + .../mozjpeg/mozjpeg/doc/html/search/all_9.js | 4 + .../mozjpeg/doc/html/search/classes_0.html | 36 + .../mozjpeg/doc/html/search/classes_0.js | 6 + .../mozjpeg/doc/html/search/classes_74.html | 26 - .../mozjpeg/doc/html/search/classes_74.js | 6 - .../mozjpeg/mozjpeg/doc/html/search/close.png | Bin 273 -> 0 bytes .../mozjpeg/mozjpeg/doc/html/search/close.svg | 31 + .../mozjpeg/doc/html/search/enums_0.html | 36 + .../mozjpeg/doc/html/search/enums_0.js | 8 + .../mozjpeg/doc/html/search/enums_74.html | 26 - .../mozjpeg/doc/html/search/enums_74.js | 8 - .../mozjpeg/doc/html/search/enumvalues_0.html | 36 + .../mozjpeg/doc/html/search/enumvalues_0.js | 37 + .../doc/html/search/enumvalues_74.html | 26 - .../mozjpeg/doc/html/search/enumvalues_74.js | 37 - .../mozjpeg/doc/html/search/functions_0.html | 36 + .../mozjpeg/doc/html/search/functions_0.js | 31 + .../mozjpeg/doc/html/search/functions_74.html | 26 - .../mozjpeg/doc/html/search/functions_74.js | 31 - .../mozjpeg/doc/html/search/groups_0.html | 36 + .../mozjpeg/doc/html/search/groups_0.js | 4 + .../mozjpeg/doc/html/search/groups_74.html | 26 - .../mozjpeg/doc/html/search/groups_74.js | 4 - .../mozjpeg/doc/html/search/mag_sel.png | Bin 563 -> 0 bytes .../mozjpeg/doc/html/search/mag_sel.svg | 74 + .../mozjpeg/doc/html/search/nomatches.html | 2 +- .../mozjpeg/doc/html/search/search.css | 106 +- .../mozjpeg/mozjpeg/doc/html/search/search.js | 137 +- .../mozjpeg/doc/html/search/search_l.png | Bin 604 -> 567 bytes .../mozjpeg/doc/html/search/search_r.png | Bin 612 -> 553 bytes .../mozjpeg/doc/html/search/searchdata.js | 36 + .../mozjpeg/doc/html/search/typedefs_0.html | 36 + .../mozjpeg/doc/html/search/typedefs_0.js | 5 + .../mozjpeg/doc/html/search/typedefs_74.html | 26 - .../mozjpeg/doc/html/search/typedefs_74.js | 5 - .../mozjpeg/doc/html/search/variables_0.html | 36 + .../mozjpeg/doc/html/search/variables_0.js | 4 + .../mozjpeg/doc/html/search/variables_1.html | 36 + .../mozjpeg/doc/html/search/variables_1.js | 5 + .../mozjpeg/doc/html/search/variables_2.html | 36 + .../mozjpeg/doc/html/search/variables_2.js | 4 + .../mozjpeg/doc/html/search/variables_3.html | 36 + .../mozjpeg/doc/html/search/variables_3.js | 4 + .../mozjpeg/doc/html/search/variables_4.html | 36 + .../mozjpeg/doc/html/search/variables_4.js | 5 + .../mozjpeg/doc/html/search/variables_5.html | 36 + .../mozjpeg/doc/html/search/variables_5.js | 4 + .../mozjpeg/doc/html/search/variables_6.html | 36 + .../mozjpeg/doc/html/search/variables_6.js | 10 + .../mozjpeg/doc/html/search/variables_63.html | 26 - .../mozjpeg/doc/html/search/variables_63.js | 4 - .../mozjpeg/doc/html/search/variables_64.html | 26 - .../mozjpeg/doc/html/search/variables_64.js | 5 - .../mozjpeg/doc/html/search/variables_68.html | 26 - .../mozjpeg/doc/html/search/variables_68.js | 4 - .../mozjpeg/doc/html/search/variables_6e.html | 26 - .../mozjpeg/doc/html/search/variables_6e.js | 4 - .../mozjpeg/doc/html/search/variables_6f.html | 26 - .../mozjpeg/doc/html/search/variables_6f.js | 5 - .../mozjpeg/doc/html/search/variables_7.html | 36 + .../mozjpeg/doc/html/search/variables_7.js | 4 + .../mozjpeg/doc/html/search/variables_72.html | 26 - .../mozjpeg/doc/html/search/variables_72.js | 4 - .../mozjpeg/doc/html/search/variables_74.html | 26 - .../mozjpeg/doc/html/search/variables_74.js | 10 - .../mozjpeg/doc/html/search/variables_77.html | 26 - .../mozjpeg/doc/html/search/variables_77.js | 4 - .../mozjpeg/doc/html/search/variables_78.html | 26 - .../mozjpeg/doc/html/search/variables_78.js | 4 - .../mozjpeg/doc/html/search/variables_79.html | 26 - .../mozjpeg/doc/html/search/variables_79.js | 4 - .../mozjpeg/doc/html/search/variables_8.html | 36 + .../mozjpeg/doc/html/search/variables_8.js | 4 + .../mozjpeg/doc/html/search/variables_9.html | 36 + .../mozjpeg/doc/html/search/variables_9.js | 4 + .../html/{ftv2splitbar.png => splitbar.png} | Bin .../mozjpeg/doc/html/structtjregion.html | 87 +- .../doc/html/structtjscalingfactor.html | 75 +- .../mozjpeg/doc/html/structtjtransform.html | 103 +- third-party/mozjpeg/mozjpeg/doc/html/tabs.css | 61 +- third-party/mozjpeg/mozjpeg/doxygen.config | 2 +- third-party/mozjpeg/mozjpeg/example.txt | 2 +- .../mozjpeg/mozjpeg/fuzz/CMakeLists.txt | 55 + third-party/mozjpeg/mozjpeg/fuzz/build.sh | 25 + third-party/mozjpeg/mozjpeg/fuzz/cjpeg.cc | 89 + third-party/mozjpeg/mozjpeg/fuzz/compress.cc | 133 + .../mozjpeg/mozjpeg/fuzz/compress_yuv.cc | 148 + .../mozjpeg/mozjpeg/fuzz/decompress.cc | 107 + .../mozjpeg/mozjpeg/fuzz/decompress_yuv.cc | 112 + third-party/mozjpeg/mozjpeg/fuzz/transform.cc | 162 + third-party/mozjpeg/mozjpeg/java/README | 2 +- third-party/mozjpeg/mozjpeg/java/TJBench.java | 121 +- .../mozjpeg/mozjpeg/java/TJExample.java | 13 +- .../mozjpeg/mozjpeg/java/TJUnitTest.java | 76 +- .../mozjpeg/java/doc/constant-values.html | 53 +- .../mozjpeg/mozjpeg/java/doc/index-all.html | 184 +- .../doc/org/libjpegturbo/turbojpeg/TJ.html | 249 +- .../libjpegturbo/turbojpeg/TJCompressor.html | 169 +- .../turbojpeg/TJCustomFilter.html | 2 +- .../turbojpeg/TJDecompressor.html | 360 +- .../libjpegturbo/turbojpeg/TJTransform.html | 39 +- .../libjpegturbo/turbojpeg/TJTransformer.html | 81 +- .../org/libjpegturbo/turbojpeg/YUVImage.html | 166 +- .../turbojpeg/package-summary.html | 2 +- .../mozjpeg/java/doc/serialized-form.html | 5 +- .../java/org/libjpegturbo/turbojpeg/TJ.java | 163 +- .../libjpegturbo/turbojpeg/TJCompressor.java | 152 +- .../turbojpeg/TJCustomFilter.java | 4 +- .../turbojpeg/TJDecompressor.java | 326 +- .../turbojpeg/TJLoader-unix.java.in | 6 +- .../libjpegturbo/turbojpeg/TJTransform.java | 28 +- .../libjpegturbo/turbojpeg/TJTransformer.java | 66 +- .../org/libjpegturbo/turbojpeg/YUVImage.java | 161 +- .../java/org_libjpegturbo_turbojpeg_TJ.h | 20 + third-party/mozjpeg/mozjpeg/jcapimin.c | 9 +- third-party/mozjpeg/mozjpeg/jcarith.c | 12 +- third-party/mozjpeg/mozjpeg/jccolext.c | 20 +- third-party/mozjpeg/mozjpeg/jccolor.c | 27 +- third-party/mozjpeg/mozjpeg/jcdctmgr.c | 37 +- third-party/mozjpeg/mozjpeg/jchuff.c | 418 +- third-party/mozjpeg/mozjpeg/jchuff.h | 15 +- third-party/mozjpeg/mozjpeg/jcinit.c | 5 +- third-party/mozjpeg/mozjpeg/jcmaster.c | 6 +- third-party/mozjpeg/mozjpeg/jconfig.h.in | 36 - third-party/mozjpeg/mozjpeg/jconfig.txt | 51 - third-party/mozjpeg/mozjpeg/jconfigint.h.in | 13 + third-party/mozjpeg/mozjpeg/jcparam.c | 6 +- third-party/mozjpeg/mozjpeg/jcphuff.c | 99 +- third-party/mozjpeg/mozjpeg/jcprepct.c | 8 +- third-party/mozjpeg/mozjpeg/jcsample.c | 63 +- third-party/mozjpeg/mozjpeg/jctrans.c | 11 +- third-party/mozjpeg/mozjpeg/jdapimin.c | 10 +- third-party/mozjpeg/mozjpeg/jdapistd.c | 80 +- third-party/mozjpeg/mozjpeg/jdarith.c | 35 +- third-party/mozjpeg/mozjpeg/jdatadst-tj.c | 8 +- third-party/mozjpeg/mozjpeg/jdatadst.c | 15 +- third-party/mozjpeg/mozjpeg/jdatasrc.c | 4 +- third-party/mozjpeg/mozjpeg/jdcoefct.c | 290 +- third-party/mozjpeg/mozjpeg/jdcoefct.h | 3 +- third-party/mozjpeg/mozjpeg/jdcol565.c | 96 +- third-party/mozjpeg/mozjpeg/jdcolext.c | 22 +- third-party/mozjpeg/mozjpeg/jdcolor.c | 24 +- third-party/mozjpeg/mozjpeg/jddctmgr.c | 4 +- third-party/mozjpeg/mozjpeg/jdhuff.c | 69 +- third-party/mozjpeg/mozjpeg/jdhuff.h | 13 +- third-party/mozjpeg/mozjpeg/jdicc.c | 36 +- third-party/mozjpeg/mozjpeg/jdinput.c | 4 +- third-party/mozjpeg/mozjpeg/jdmainct.c | 4 +- third-party/mozjpeg/mozjpeg/jdmarker.c | 75 +- third-party/mozjpeg/mozjpeg/jdmaster.c | 28 +- third-party/mozjpeg/mozjpeg/jdmerge.c | 56 +- third-party/mozjpeg/mozjpeg/jdmerge.h | 47 + third-party/mozjpeg/mozjpeg/jdmrg565.c | 78 +- third-party/mozjpeg/mozjpeg/jdmrgext.c | 58 +- third-party/mozjpeg/mozjpeg/jdphuff.c | 56 +- third-party/mozjpeg/mozjpeg/jdsample.c | 38 +- third-party/mozjpeg/mozjpeg/jdtrans.c | 5 +- third-party/mozjpeg/mozjpeg/jerror.c | 16 +- third-party/mozjpeg/mozjpeg/jerror.h | 37 +- third-party/mozjpeg/mozjpeg/jfdctint.c | 4 +- third-party/mozjpeg/mozjpeg/jidctint.c | 12 +- third-party/mozjpeg/mozjpeg/jinclude.h | 145 +- third-party/mozjpeg/mozjpeg/jmemmgr.c | 33 +- third-party/mozjpeg/mozjpeg/jmemnobs.c | 5 - third-party/mozjpeg/mozjpeg/jmorecfg.h | 47 +- third-party/mozjpeg/mozjpeg/jpegcomp.h | 3 +- third-party/mozjpeg/mozjpeg/jpegint.h | 83 +- third-party/mozjpeg/mozjpeg/jpeglib.h | 8 +- third-party/mozjpeg/mozjpeg/jpegtran.1 | 83 +- third-party/mozjpeg/mozjpeg/jpegtran.c | 227 +- third-party/mozjpeg/mozjpeg/jquant1.c | 27 +- third-party/mozjpeg/mozjpeg/jquant2.c | 55 +- third-party/mozjpeg/mozjpeg/jsimd.h | 14 +- third-party/mozjpeg/mozjpeg/jsimd_none.c | 21 +- third-party/mozjpeg/mozjpeg/jstdhuff.c | 9 +- third-party/mozjpeg/mozjpeg/jutils.c | 10 +- .../mozjpeg/{jversion.h => jversion.h.in} | 26 +- third-party/mozjpeg/mozjpeg/libjpeg.txt | 106 +- third-party/mozjpeg/mozjpeg/md5/md5hl.c | 6 +- third-party/mozjpeg/mozjpeg/rdbmp.c | 77 +- third-party/mozjpeg/mozjpeg/rdcolmap.c | 5 +- third-party/mozjpeg/mozjpeg/rdgif.c | 709 ++- third-party/mozjpeg/mozjpeg/rdjpgcom.c | 31 +- third-party/mozjpeg/mozjpeg/rdppm.c | 111 +- third-party/mozjpeg/mozjpeg/rdrle.c | 389 -- third-party/mozjpeg/mozjpeg/rdswitch.c | 22 +- third-party/mozjpeg/mozjpeg/rdtarga.c | 27 +- .../mozjpeg/mozjpeg/release/Config.cmake.in | 4 + .../mozjpeg/mozjpeg/release/License.rtf | 0 .../mozjpeg/mozjpeg/release/ReadMe.txt | 4 +- .../mozjpeg/mozjpeg/release/Welcome.rtf | 18 - .../mozjpeg/mozjpeg/release/Welcome.rtf.in | 17 + .../mozjpeg/mozjpeg/release/installer.nsi.in | 11 + .../mozjpeg/mozjpeg/release/makecygwinpkg.in | 66 - .../mozjpeg/mozjpeg/release/makedpkg.in | 14 +- .../mozjpeg/mozjpeg/release/makemacpkg.in | 113 +- .../mozjpeg/mozjpeg/release/makerpm.in | 0 .../mozjpeg/mozjpeg/release/makesrpm.in | 0 .../mozjpeg/mozjpeg/release/maketarball.in | 2 +- .../mozjpeg/mozjpeg/release/rpm.spec.in | 92 +- .../mozjpeg/mozjpeg/release/uninstall.in | 11 +- .../mozjpeg/mozjpeg/sharedlib/CMakeLists.txt | 31 +- .../mozjpeg/mozjpeg/simd/CMakeLists.txt | 266 +- .../mozjpeg/simd/arm/aarch32/jccolext-neon.c | 148 + .../mozjpeg/simd/arm/aarch32/jchuff-neon.c | 334 ++ .../mozjpeg/simd/arm/{ => aarch32}/jsimd.c | 305 +- .../mozjpeg/simd/arm/aarch32/jsimd_neon.S | 1200 +++++ .../mozjpeg/simd/arm/aarch64/jccolext-neon.c | 316 ++ .../mozjpeg/simd/arm/aarch64/jchuff-neon.c | 411 ++ .../simd/{arm64 => arm/aarch64}/jsimd.c | 323 +- .../simd/{arm64 => arm/aarch64}/jsimd_neon.S | 1285 +----- third-party/mozjpeg/mozjpeg/simd/arm/align.h | 28 + .../mozjpeg/mozjpeg/simd/arm/jccolor-neon.c | 160 + .../mozjpeg/mozjpeg/simd/arm/jcgray-neon.c | 120 + .../mozjpeg/mozjpeg/simd/arm/jcgryext-neon.c | 106 + third-party/mozjpeg/mozjpeg/simd/arm/jchuff.h | 131 + .../mozjpeg/mozjpeg/simd/arm/jcphuff-neon.c | 623 +++ .../mozjpeg/mozjpeg/simd/arm/jcsample-neon.c | 192 + .../mozjpeg/mozjpeg/simd/arm/jdcolext-neon.c | 374 ++ .../mozjpeg/mozjpeg/simd/arm/jdcolor-neon.c | 141 + .../mozjpeg/mozjpeg/simd/arm/jdmerge-neon.c | 144 + .../mozjpeg/mozjpeg/simd/arm/jdmrgext-neon.c | 723 +++ .../mozjpeg/mozjpeg/simd/arm/jdsample-neon.c | 569 +++ .../mozjpeg/mozjpeg/simd/arm/jfdctfst-neon.c | 214 + .../mozjpeg/mozjpeg/simd/arm/jfdctint-neon.c | 376 ++ .../mozjpeg/mozjpeg/simd/arm/jidctfst-neon.c | 472 ++ .../mozjpeg/mozjpeg/simd/arm/jidctint-neon.c | 801 ++++ .../mozjpeg/mozjpeg/simd/arm/jidctred-neon.c | 486 ++ .../mozjpeg/mozjpeg/simd/arm/jquanti-neon.c | 193 + .../mozjpeg/mozjpeg/simd/arm/jsimd_neon.S | 2878 ------------ .../mozjpeg/mozjpeg/simd/arm/neon-compat.h.in | 37 + .../mozjpeg/mozjpeg/simd/gas-preprocessor.in | 1 - .../mozjpeg/mozjpeg/simd/i386/jchuff-sse2.asm | 1063 +++-- .../mozjpeg/simd/i386/jcphuff-sse2.asm | 2 + .../mozjpeg/simd/i386/jfdctint-avx2.asm | 6 +- .../mozjpeg/simd/i386/jfdctint-mmx.asm | 4 +- .../mozjpeg/simd/i386/jfdctint-sse2.asm | 4 +- .../mozjpeg/simd/i386/jidctint-avx2.asm | 6 +- .../mozjpeg/simd/i386/jidctint-mmx.asm | 4 +- .../mozjpeg/simd/i386/jidctint-sse2.asm | 4 +- third-party/mozjpeg/mozjpeg/simd/i386/jsimd.c | 107 +- third-party/mozjpeg/mozjpeg/simd/jsimd.h | 189 +- .../mozjpeg/simd/loongson/jccolext-mmi.c | 483 -- .../mozjpeg/simd/loongson/jdcolext-mmi.c | 424 -- .../mozjpeg/simd/loongson/jdsample-mmi.c | 245 - .../mozjpeg/simd/loongson/jquanti-mmi.c | 130 - third-party/mozjpeg/mozjpeg/simd/mips/jsimd.c | 52 +- .../mozjpeg/mozjpeg/simd/mips/jsimd_dspr2.S | 2226 ++++----- .../mozjpeg/simd/mips64/jccolext-mmi.c | 455 ++ .../simd/{loongson => mips64}/jccolor-mmi.c | 0 .../mozjpeg/mozjpeg/simd/mips64/jcgray-mmi.c | 132 + .../mozjpeg/simd/mips64/jcgryext-mmi.c | 374 ++ .../simd/{loongson => mips64}/jcsample-mmi.c | 68 +- .../simd/{loongson => mips64}/jcsample.h | 2 +- .../mozjpeg/simd/mips64/jdcolext-mmi.c | 415 ++ .../simd/{loongson => mips64}/jdcolor-mmi.c | 0 .../mozjpeg/mozjpeg/simd/mips64/jdmerge-mmi.c | 149 + .../mozjpeg/simd/mips64/jdmrgext-mmi.c | 615 +++ .../mozjpeg/simd/mips64/jdsample-mmi.c | 304 ++ .../mozjpeg/simd/mips64/jfdctfst-mmi.c | 255 + .../simd/{loongson => mips64}/jfdctint-mmi.c | 4 +- .../mozjpeg/simd/mips64/jidctfst-mmi.c | 395 ++ .../simd/{loongson => mips64}/jidctint-mmi.c | 4 +- .../mozjpeg/mozjpeg/simd/mips64/jquanti-mmi.c | 124 + .../mozjpeg/simd/{loongson => mips64}/jsimd.c | 276 +- .../simd/{loongson => mips64}/jsimd_mmi.h | 26 +- .../{loongson => mips64}/loongson-mmintrin.h | 14 +- .../mozjpeg/simd/nasm/jpeg_nbits_table.inc | 4097 ----------------- .../mozjpeg/mozjpeg/simd/nasm/jsimdcfg.inc.h | 12 +- .../mozjpeg/mozjpeg/simd/nasm/jsimdext.inc | 45 +- .../mozjpeg/mozjpeg/simd/powerpc/jcsample.h | 2 +- .../mozjpeg/simd/powerpc/jfdctint-altivec.c | 4 +- .../mozjpeg/simd/powerpc/jidctint-altivec.c | 4 +- .../mozjpeg/mozjpeg/simd/powerpc/jsimd.c | 36 +- .../mozjpeg/simd/x86_64/jccolext-avx2.asm | 15 +- .../mozjpeg/simd/x86_64/jccolext-sse2.asm | 15 +- .../mozjpeg/simd/x86_64/jcgryext-avx2.asm | 7 +- .../mozjpeg/simd/x86_64/jcgryext-sse2.asm | 7 +- .../mozjpeg/simd/x86_64/jchuff-sse2.asm | 807 ++-- .../mozjpeg/simd/x86_64/jcphuff-sse2.asm | 2 + .../mozjpeg/simd/x86_64/jcsample-avx2.asm | 15 +- .../mozjpeg/simd/x86_64/jcsample-sse2.asm | 15 +- .../mozjpeg/simd/x86_64/jdcolext-avx2.asm | 15 +- .../mozjpeg/simd/x86_64/jdcolext-sse2.asm | 15 +- .../mozjpeg/simd/x86_64/jdmrgext-avx2.asm | 47 +- .../mozjpeg/simd/x86_64/jdmrgext-sse2.asm | 47 +- .../mozjpeg/simd/x86_64/jdsample-avx2.asm | 33 +- .../mozjpeg/simd/x86_64/jdsample-sse2.asm | 33 +- .../mozjpeg/simd/x86_64/jfdctint-avx2.asm | 6 +- .../mozjpeg/simd/x86_64/jfdctint-sse2.asm | 4 +- .../mozjpeg/simd/x86_64/jidctflt-sse2.asm | 9 +- .../mozjpeg/simd/x86_64/jidctfst-sse2.asm | 17 +- .../mozjpeg/simd/x86_64/jidctint-avx2.asm | 23 +- .../mozjpeg/simd/x86_64/jidctint-sse2.asm | 21 +- .../mozjpeg/simd/x86_64/jidctred-sse2.asm | 13 +- .../mozjpeg/simd/x86_64/jquantf-sse2.asm | 5 +- .../mozjpeg/simd/x86_64/jquanti-avx2.asm | 17 +- .../mozjpeg/simd/x86_64/jquanti-sse2.asm | 9 +- .../mozjpeg/mozjpeg/simd/x86_64/jsimd.c | 78 +- third-party/mozjpeg/mozjpeg/src/jconfig.h | 73 - third-party/mozjpeg/mozjpeg/src/jconfigint.h | 31 - third-party/mozjpeg/mozjpeg/strtest.c | 170 + third-party/mozjpeg/mozjpeg/structure.txt | 12 +- .../mozjpeg/mozjpeg/testimages/test.scan | 7 +- third-party/mozjpeg/mozjpeg/tjbench.c | 185 +- third-party/mozjpeg/mozjpeg/tjexample.c | 32 +- third-party/mozjpeg/mozjpeg/tjunittest.c | 86 +- third-party/mozjpeg/mozjpeg/tjutil.h | 18 +- third-party/mozjpeg/mozjpeg/transupp.c | 953 +++- third-party/mozjpeg/mozjpeg/transupp.h | 45 +- third-party/mozjpeg/mozjpeg/turbojpeg-jni.c | 329 +- third-party/mozjpeg/mozjpeg/turbojpeg-mapfile | 8 +- .../mozjpeg/mozjpeg/turbojpeg-mapfile.jni | 8 +- third-party/mozjpeg/mozjpeg/turbojpeg.c | 448 +- third-party/mozjpeg/mozjpeg/turbojpeg.h | 991 ++-- third-party/mozjpeg/mozjpeg/usage.txt | 241 +- .../win/gcc/projectTargets-release.cmake.in | 49 + third-party/mozjpeg/mozjpeg/win/jconfig.h.in | 9 - third-party/mozjpeg/mozjpeg/win/jpeg.rc.in | 35 + .../mozjpeg/win/projectTargets.cmake.in | 115 + .../mozjpeg/mozjpeg/win/turbojpeg.rc.in | 35 + .../win/vc/projectTargets-release.cmake.in | 49 + third-party/mozjpeg/mozjpeg/wizard.txt | 10 +- third-party/mozjpeg/mozjpeg/wrbmp.c | 46 +- third-party/mozjpeg/mozjpeg/wrgif.c | 329 +- third-party/mozjpeg/mozjpeg/wrjpgcom.c | 24 +- third-party/mozjpeg/mozjpeg/wrppm.c | 39 +- third-party/mozjpeg/mozjpeg/wrrle.c | 309 -- third-party/mozjpeg/mozjpeg/wrtarga.c | 31 +- third-party/rnnoise/Package.swift | 2 +- versions.json | 4 +- 1253 files changed, 75068 insertions(+), 35649 deletions(-) create mode 100644 Nicegram/AppLovinAdProvider/Sources/AdProviderMock.swift create mode 100644 submodules/AccountContext/Sources/AttachmentMainButtonState.swift delete mode 100644 submodules/Camera/Sources/CameraPreviewNode.swift create mode 100644 submodules/ContextUI/Sources/ContextSourceContainer.swift delete mode 100644 submodules/Display/Source/ContextMenuNode.swift create mode 100644 submodules/PremiumUI/Sources/CreateGiveawayController.swift create mode 100644 submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift create mode 100644 submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift create mode 100644 submodules/PremiumUI/Sources/GiftOptionItem.swift create mode 100644 submodules/PremiumUI/Sources/GiveawayInfoController.swift create mode 100644 submodules/PremiumUI/Sources/PremiumBoostScreen.swift create mode 100644 submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift rename submodules/{TelegramUI => PremiumUI}/Sources/ReplaceBoostConfirmationController.swift (65%) create mode 100644 submodules/PremiumUI/Sources/ReplaceBoostScreen.swift create mode 100644 submodules/PremiumUI/Sources/SubscriptionsCountItem.swift create mode 100644 submodules/StatisticsUI/Sources/BoostsTabsItem.swift create mode 100644 submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift create mode 100644 submodules/TelegramUI/Components/AudioWaveformNode/BUILD rename submodules/TelegramUI/{ => Components/AudioWaveformNode}/Sources/AudioWaveformNode.swift (95%) create mode 100644 submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatBotInfoItem/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatBotInfoItem}/Sources/ChatBotInfoItem.swift (82%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatBotStartInputPanelNode}/Sources/ChatBotStartInputPanelNode.swift (85%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatButtonKeyboardInputNode}/Sources/ChatButtonKeyboardInputNode.swift (95%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatChannelSubscriberInputPanelNode}/Sources/ChatChannelSubscriberInputPanelNode.swift (96%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/BUILD rename submodules/TelegramUI/{Sources/ChatContextResultPeekContentNode.swift => Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift} (94%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatHistoryEntry}/Sources/ChatHistoryEntry.swift (89%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/Sources/ChatInputContextPanelNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD create mode 100755 submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h create mode 100755 submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatInstantVideoMessageDurationNode}/Sources/ChatInstantVideoMessageDurationNode.swift (94%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatLoadingNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatLoadingNode}/Sources/ChatLoadingNode.swift (88%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/BUILD rename submodules/TelegramUI/{Sources/ChatMessageActionItemNode.swift => Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift} (91%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageActionButtonsNode}/Sources/ChatMessageActionButtonsNode.swift (88%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageAnimatedStickerItemNode}/Sources/ChatMessageAnimatedStickerItemNode.swift (87%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageBubbleContentNode}/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift (96%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageBubbleItemNode}/Sources/ChatMessageBubbleItemNode.swift (82%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageCallBubbleContentNode}/Sources/ChatMessageCallBubbleContentNode.swift (89%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageCommentFooterContentNode}/Sources/ChatMessageCommentFooterContentNode.swift (94%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageContactBubbleContentNode}/Sources/ChatMessageContactBubbleContentNode.swift (88%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageDateAndStatusNode}/Sources/ChatMessageDateAndStatusNode.swift (97%) rename submodules/TelegramUI/{ => Components/Chat/ChatMessageDateAndStatusNode}/Sources/StringForMessageTimestampStatus.swift (88%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageDeliveryFailedNode}/Sources/ChatMessageDeliveryFailedNode.swift (81%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode}/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift (71%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageEventLogPreviousLinkContentNode}/Sources/ChatMessageEventLogPreviousLinkContentNode.swift (71%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageEventLogPreviousMessageContentNode}/Sources/ChatMessageEventLogPreviousMessageContentNode.swift (72%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageFileBubbleContentNode}/Sources/ChatMessageFileBubbleContentNode.swift (81%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageGameBubbleContentNode}/Sources/ChatMessageGameBubbleContentNode.swift (73%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD rename submodules/TelegramUI/{Sources/ChatMessageGiftItemNode.swift => Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift} (72%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageInstantVideoBubbleContentNode}/Sources/ChatMessageInstantVideoBubbleContentNode.swift (88%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageInstantVideoItemNode}/Sources/ChatMessageInstantVideoItemNode.swift (88%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageInteractiveFileNode}/Sources/ChatMessageInteractiveFileNode.swift (94%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageInteractiveInstantVideoNode}/Sources/ChatMessageInteractiveInstantVideoNode.swift (89%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageInteractiveMediaNode}/Sources/ChatMessageInteractiveMediaNode.swift (97%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageInvoiceBubbleContentNode}/Sources/ChatMessageInvoiceBubbleContentNode.swift (74%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageItem/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageItemImpl}/Sources/ChatMessageDateHeader.swift (82%) rename submodules/TelegramUI/{Sources/ChatMessageItem.swift => Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift} (82%) rename submodules/TelegramUI/{ => Components/Chat/ChatMessageItemImpl}/Sources/ChatReplyCountItem.swift (84%) rename submodules/TelegramUI/{ => Components/Chat/ChatMessageItemImpl}/Sources/ChatUnreadItem.swift (84%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageItemView/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageItemView}/Sources/ChatMessageItemView.swift (81%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageMapBubbleContentNode}/Sources/ChatMessageLiveLocationTextNode.swift (100%) rename submodules/TelegramUI/{ => Components/Chat/ChatMessageMapBubbleContentNode}/Sources/ChatMessageMapBubbleContentNode.swift (94%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageMediaBubbleContentNode}/Sources/ChatMessageMediaBubbleContentNode.swift (93%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessagePollBubbleContentNode}/Sources/ChatMessagePollBubbleContentNode.swift (95%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageProfilePhotoSuggestionContentNode}/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift (93%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageReactionsFooterContentNode}/Sources/ChatMessageReactionsFooterContentNode.swift (85%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageReplyInfoNode}/Sources/ChatMessageReplyInfoNode.swift (55%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageRestrictedBubbleContentNode}/Sources/ChatMessageRestrictedBubbleContentNode.swift (92%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageSelectionNode}/Sources/ChatMessageSelectionNode.swift (74%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageShareButton/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageStickerItemNode}/Sources/ChatMessageStickerItemNode.swift (84%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageStoryMentionContentNode}/Sources/ChatMessageStoryMentionContentNode.swift (94%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageSwipeToReplyNode}/Sources/ChatMessageSwipeToReplyNode.swift (95%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageTextBubbleContentNode}/Sources/ChatMessageTextBubbleContentNode.swift (56%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageThreadInfoNode}/Sources/ChatMessageThreadInfoNode.swift (96%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageUnsupportedBubbleContentNode}/Sources/ChatMessageUnsupportedBubbleContentNode.swift (56%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageWallpaperBubbleContentNode}/Sources/ChatMessageWallpaperBubbleContentNode.swift (94%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatMessageWebpageBubbleContentNode}/Sources/ChatMessageWebpageBubbleContentNode.swift (70%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatNavigationButton/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatOverscrollControl}/Sources/ChatOverscrollControl.swift (98%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsController.swift (96%) rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsControllerNode.swift (97%) rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsEmptyNode.swift (94%) rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsFilterController.swift (100%) rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsHistoryTransition.swift (82%) rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsInteraction.swift (100%) rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsSearchNavigationContentNode.swift (100%) rename submodules/TelegramUI/{ => Components/Chat/ChatRecentActionsController}/Sources/ChatRecentActionsTitleView.swift (100%) create mode 100644 submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatSwipeToReplyRecognizer}/Sources/ChatSwipeToReplyRecognizer.swift (62%) create mode 100644 submodules/TelegramUI/Components/Chat/EditableTokenListNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/EditableTokenListNode}/Sources/EditableTokenListNode.swift (92%) create mode 100644 submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/InstantVideoRadialStatusNode}/Sources/InstantVideoRadialStatusNode.swift (95%) create mode 100644 submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ManagedDiceAnimationNode}/Sources/ManagedDiceAnimationNode.swift (93%) create mode 100644 submodules/TelegramUI/Components/Chat/MessageHaptics/BUILD rename submodules/TelegramUI/{ => Components/Chat/MessageHaptics}/Sources/CoffinHaptic.swift (91%) rename submodules/TelegramUI/{ => Components/Chat/MessageHaptics}/Sources/HeartbeatHaptic.swift (92%) rename submodules/TelegramUI/{ => Components/Chat/MessageHaptics}/Sources/PeachHaptic.swift (91%) create mode 100644 submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift create mode 100644 submodules/TelegramUI/Components/Chat/MessageQuoteComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/MessageQuoteComponent/Sources/MessageQuoteComponent.swift create mode 100644 submodules/TelegramUI/Components/Chat/PollBubbleTimerNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/PollBubbleTimerNode}/Sources/PollBubbleTimerNode.swift (97%) create mode 100644 submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ReplyAccessoryPanelNode}/Sources/ReplyAccessoryPanelNode.swift (55%) create mode 100644 submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/BUILD rename submodules/TelegramUI/{ => Components/Chat/ShimmeringLinkNode}/Sources/ShimmeringLinkNode.swift (89%) create mode 100644 submodules/TelegramUI/Components/CompositeTextNode/BUILD create mode 100644 submodules/TelegramUI/Components/CompositeTextNode/Sources/CompositeTextNode.swift create mode 100644 submodules/TelegramUI/Components/ContextMenuScreen/BUILD rename submodules/{Display/Source => TelegramUI/Components/ContextMenuScreen/Sources}/ContextMenuActionNode.swift (76%) create mode 100644 submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuController.swift create mode 100644 submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift create mode 100644 submodules/TelegramUI/Components/ItemListDatePickerItem/BUILD rename submodules/{InviteLinksUI => TelegramUI/Components/ItemListDatePickerItem}/Sources/ItemListDatePickerItem.swift (95%) create mode 100644 submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD create mode 100644 submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift create mode 100644 submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift create mode 100644 submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift create mode 100644 submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift create mode 100644 submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift create mode 100644 submodules/TelegramUI/Components/TextLoadingEffect/BUILD create mode 100644 submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift create mode 100644 submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/BUILD create mode 100644 submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/Sources/RoundedRectWithTailPath.swift create mode 100644 submodules/TelegramUI/Components/WallpaperPreviewMedia/BUILD rename submodules/TelegramUI/{ => Components/WallpaperPreviewMedia}/Sources/WallpaperPreviewMedia.swift (70%) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageEnlarge.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageEnlarge.imageset/enlarge_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/shrink_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MoveDown.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MoveDown.imageset/movedown_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MoveUp.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MoveUp.imageset/moveup_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Quote.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Quote.imageset/IconQuote.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/IconQuoteRemove.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/IconQuoteSelected.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/NameColorIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/NameColorIcon.imageset/brush_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ForwardSettingsIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ForwardSettingsIcon.imageset/forwardtools_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/LinkSettingsIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/LinkSettingsIcon.imageset/linktools_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextChannelIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextChannelIcon.imageset/channel.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextGroupIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextGroupIcon.imageset/group.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ReplyQuoteIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ReplyQuoteIcon.imageset/replyquote_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ReplySettingsIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ReplySettingsIcon.imageset/replytools_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/ConversationInstantPageButtonIconIncoming@2x.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/ConversationInstantPageButtonIconIncoming@3x.png create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/InstantIcon.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/AddBoosts.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/AddBoosts.imageset/addboosts_30.pdf rename submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/{AvatarBoost.pdf => Reassign.pdf} (52%) create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostButtonIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostButtonIcon.imageset/Union.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/NoIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/NoIcon.imageset/noicon_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/ToBeDistributed.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/ToBeDistributed.imageset/avatar_tobedistributed.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Unclaimed.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Unclaimed.imageset/avatar_unclaimed.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/flash_onToOff.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_caption_off.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_caption_on.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_media_large.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_media_small.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_person_off.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_person_on.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_sort_above.json create mode 100644 submodules/TelegramUI/Resources/Animations/message_preview_sort_below.json create mode 100644 submodules/TelegramUI/Resources/Animations/story_back.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/story_forward.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/story_move.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/story_pause.tgs create mode 100644 submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift create mode 100644 submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift create mode 100644 submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift delete mode 100644 submodules/TelegramUI/Sources/ChatDateSelectionSheet.swift delete mode 100644 submodules/TelegramUI/Sources/ChatEditMessageMediaContext.swift delete mode 100644 submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift delete mode 100644 submodules/TelegramUI/Sources/ChatHoleItem.swift delete mode 100644 submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift delete mode 100644 submodules/TelegramUI/Sources/ChatInputPanelNode.swift delete mode 100644 submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift delete mode 100644 submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift delete mode 100644 submodules/TelegramUI/Sources/ChatThemeScreen.swift.orig delete mode 100644 submodules/TelegramUI/Sources/EmojiResources.swift delete mode 100644 submodules/TelegramUI/Sources/GridHoleItem.swift delete mode 100644 submodules/TelegramUI/Sources/GridMessageItem.swift create mode 100644 submodules/TelegramUI/Sources/MakeTempAccountContext.swift delete mode 100644 submodules/TelegramUI/Sources/module.private.modulemap create mode 100644 submodules/TextFormat/Sources/CountNicePercent.swift delete mode 100644 submodules/TextFormat/Sources/TextFormat.h create mode 100644 third-party/mozjpeg/mozjpeg/.github/pull_request_template.md delete mode 100644 third-party/mozjpeg/mozjpeg/appveyor.yml create mode 100644 third-party/mozjpeg/mozjpeg/cmakescripts/PackageInfo.cmake create mode 100755 third-party/mozjpeg/mozjpeg/croptest.in rename third-party/mozjpeg/mozjpeg/doc/html/{ftv2doc.png => doc.png} (100%) delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/doxygen.png create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/doxygen.svg rename third-party/mozjpeg/mozjpeg/doc/html/{ftv2folderclosed.png => folderclosed.png} (100%) rename third-party/mozjpeg/mozjpeg/doc/html/{ftv2folderopen.png => folderopen.png} (100%) delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2blank.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2cl.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2lastnode.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2link.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2mlastnode.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2mnode.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2mo.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2node.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2ns.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2plastnode.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2pnode.png delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/ftv2vertline.png create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/menu.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/menudata.js rename third-party/mozjpeg/mozjpeg/doc/html/search/{all_63.html => all_0.html} (59%) create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_0.js rename third-party/mozjpeg/mozjpeg/doc/html/search/{all_64.html => all_1.html} (59%) create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_1.js rename third-party/mozjpeg/mozjpeg/doc/html/search/{all_68.html => all_2.html} (59%) create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_2.js rename third-party/mozjpeg/mozjpeg/doc/html/search/{all_6e.html => all_3.html} (59%) create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_3.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_4.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_4.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_5.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_5.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_6.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_6.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_63.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_64.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_68.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_6e.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_7.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_7.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_72.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_72.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_74.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_77.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_77.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_78.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_78.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_79.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_79.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_8.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_8.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_9.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/all_9.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/close.png create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/close.svg create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/mag_sel.png create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/mag_sel.svg create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/searchdata.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.js delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.html delete mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.js create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.html create mode 100644 third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.js rename third-party/mozjpeg/mozjpeg/doc/html/{ftv2splitbar.png => splitbar.png} (100%) create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/CMakeLists.txt create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/build.sh create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/cjpeg.cc create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/compress.cc create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/compress_yuv.cc create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/decompress.cc create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/decompress_yuv.cc create mode 100644 third-party/mozjpeg/mozjpeg/fuzz/transform.cc create mode 100644 third-party/mozjpeg/mozjpeg/jdmerge.h rename third-party/mozjpeg/mozjpeg/{jversion.h => jversion.h.in} (67%) delete mode 100644 third-party/mozjpeg/mozjpeg/rdrle.c create mode 100644 third-party/mozjpeg/mozjpeg/release/Config.cmake.in mode change 100755 => 100644 third-party/mozjpeg/mozjpeg/release/License.rtf delete mode 100755 third-party/mozjpeg/mozjpeg/release/Welcome.rtf create mode 100644 third-party/mozjpeg/mozjpeg/release/Welcome.rtf.in mode change 100755 => 100644 third-party/mozjpeg/mozjpeg/release/installer.nsi.in delete mode 100755 third-party/mozjpeg/mozjpeg/release/makecygwinpkg.in mode change 100644 => 100755 third-party/mozjpeg/mozjpeg/release/makedpkg.in mode change 100644 => 100755 third-party/mozjpeg/mozjpeg/release/makemacpkg.in mode change 100644 => 100755 third-party/mozjpeg/mozjpeg/release/makerpm.in mode change 100644 => 100755 third-party/mozjpeg/mozjpeg/release/makesrpm.in mode change 100644 => 100755 third-party/mozjpeg/mozjpeg/release/maketarball.in mode change 100755 => 100644 third-party/mozjpeg/mozjpeg/sharedlib/CMakeLists.txt mode change 100755 => 100644 third-party/mozjpeg/mozjpeg/simd/CMakeLists.txt create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jccolext-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jchuff-neon.c rename third-party/mozjpeg/mozjpeg/simd/arm/{ => aarch32}/jsimd.c (65%) create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jsimd_neon.S create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jccolext-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jchuff-neon.c rename third-party/mozjpeg/mozjpeg/simd/{arm64 => arm/aarch64}/jsimd.c (67%) rename third-party/mozjpeg/mozjpeg/simd/{arm64 => arm/aarch64}/jsimd_neon.S (65%) create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/align.h create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jccolor-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jcgray-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jcgryext-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jchuff.h create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jcphuff-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jcsample-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jdcolext-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jdcolor-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jdmerge-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jdmrgext-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jdsample-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jfdctfst-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jfdctint-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jidctfst-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jidctint-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jidctred-neon.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jquanti-neon.c delete mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/jsimd_neon.S create mode 100644 third-party/mozjpeg/mozjpeg/simd/arm/neon-compat.h.in delete mode 100755 third-party/mozjpeg/mozjpeg/simd/gas-preprocessor.in delete mode 100644 third-party/mozjpeg/mozjpeg/simd/loongson/jccolext-mmi.c delete mode 100644 third-party/mozjpeg/mozjpeg/simd/loongson/jdcolext-mmi.c delete mode 100644 third-party/mozjpeg/mozjpeg/simd/loongson/jdsample-mmi.c delete mode 100644 third-party/mozjpeg/mozjpeg/simd/loongson/jquanti-mmi.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jccolext-mmi.c rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jccolor-mmi.c (100%) create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jcgray-mmi.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jcgryext-mmi.c rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jcsample-mmi.c (56%) rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jcsample.h (90%) create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jdcolext-mmi.c rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jdcolor-mmi.c (100%) create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jdmerge-mmi.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jdmrgext-mmi.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jdsample-mmi.c create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jfdctfst-mmi.c rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jfdctint-mmi.c (99%) create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jidctfst-mmi.c rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jidctint-mmi.c (99%) create mode 100644 third-party/mozjpeg/mozjpeg/simd/mips64/jquanti-mmi.c rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jsimd.c (64%) rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/jsimd_mmi.h (72%) rename third-party/mozjpeg/mozjpeg/simd/{loongson => mips64}/loongson-mmintrin.h (98%) delete mode 100644 third-party/mozjpeg/mozjpeg/simd/nasm/jpeg_nbits_table.inc delete mode 100644 third-party/mozjpeg/mozjpeg/src/jconfig.h delete mode 100644 third-party/mozjpeg/mozjpeg/src/jconfigint.h create mode 100644 third-party/mozjpeg/mozjpeg/strtest.c mode change 100755 => 100644 third-party/mozjpeg/mozjpeg/turbojpeg-mapfile mode change 100755 => 100644 third-party/mozjpeg/mozjpeg/turbojpeg-mapfile.jni create mode 100644 third-party/mozjpeg/mozjpeg/win/gcc/projectTargets-release.cmake.in create mode 100644 third-party/mozjpeg/mozjpeg/win/jpeg.rc.in create mode 100644 third-party/mozjpeg/mozjpeg/win/projectTargets.cmake.in create mode 100644 third-party/mozjpeg/mozjpeg/win/turbojpeg.rc.in create mode 100644 third-party/mozjpeg/mozjpeg/win/vc/projectTargets-release.cmake.in delete mode 100644 third-party/mozjpeg/mozjpeg/wrrle.c diff --git a/Nicegram/AppLovinAdProvider/Sources/AdProviderMock.swift b/Nicegram/AppLovinAdProvider/Sources/AdProviderMock.swift new file mode 100644 index 00000000000..84fb1b29951 --- /dev/null +++ b/Nicegram/AppLovinAdProvider/Sources/AdProviderMock.swift @@ -0,0 +1,13 @@ +import NGAiChat + +public class AdProviderMock { + public init() {} +} + +extension AdProviderMock: AdProvider { + public func initialize() {} + + public func showAd() -> ShowAdResult { + .error(nil) + } +} diff --git a/Nicegram/NGLottie/Sources/LottieViewImpl.swift b/Nicegram/NGLottie/Sources/LottieViewImpl.swift index aced1ecb102..2d0428e9f57 100644 --- a/Nicegram/NGLottie/Sources/LottieViewImpl.swift +++ b/Nicegram/NGLottie/Sources/LottieViewImpl.swift @@ -26,7 +26,7 @@ public class LottieViewImpl: UIView { } } -extension LottieViewImpl: LottieView { +extension LottieViewImpl: LottieViewProtocol { public func play(completion: @escaping () -> Void) { animationView.play { _ in completion() diff --git a/Nicegram/NGUI/Sources/NicegramSettingsController.swift b/Nicegram/NGUI/Sources/NicegramSettingsController.swift index 27e035a4ea1..8361d219081 100644 --- a/Nicegram/NGUI/Sources/NicegramSettingsController.swift +++ b/Nicegram/NGUI/Sources/NicegramSettingsController.swift @@ -11,6 +11,7 @@ import AccountContext import Display import FeatImagesHubUI +import FeatPartners import Foundation import ItemListUI import NGData @@ -81,6 +82,7 @@ private enum EasyToggleType { case showRegDate case hideReactions case hideStories + case hidePartnerIntegrations } @@ -541,6 +543,10 @@ 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): @@ -806,6 +812,12 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat toggleIndex += 1 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/Package.resolved b/Package.resolved index 7a0abeab4b7..9619e9fdf1b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -42,7 +42,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "develop", - "revision" : "6ac92187e0dac03bffc786d6bf8378169ed425aa" + "revision" : "42ed26bd295a28a8a800f54583af10f4acdaaf34" } }, { @@ -51,7 +51,7 @@ "location" : "https://github.com/denis15yo/R.swift.git", "state" : { "branch" : "main", - "revision" : "77c0d9c202b9ac83c1992111b76b3fb10468015b" + "revision" : "4a0f8c97f1baa27d165dc801982c55bbf51126e5" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "1f06ef5007b6a580b3873ed2adee19e05d3b215a", - "version" : "5.18.3" + "revision" : "fd1950de05a5ad77cb252fd88576c1e1809ee50d", + "version" : "5.18.4" } }, { diff --git a/README.md b/README.md index 2728bbd0fd6..79f325aa139 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,28 @@ python3 build-system/Make/Make.py \ --configuration=release_arm64 ``` -## Tips +# FAQ + +## Xcode is stuck at "build-request.json not updated yet" + +Occasionally, you might observe the following message in your build log: +``` +"/Users/xxx/Library/Developer/Xcode/DerivedData/Telegram-xxx/Build/Intermediates.noindex/XCBuildData/xxx.xcbuilddata/build-request.json" not updated yet, waiting... +``` + +Should this occur, simply cancel the ongoing build and initiate a new one. + +## Telegram_xcodeproj: no such package + +Following a system restart, the auto-generated Xcode project might encounter a build failure accompanied by this error: +``` +ERROR: Skipping '@rules_xcodeproj_generated//generator/Telegram/Telegram_xcodeproj:Telegram_xcodeproj': no such package '@rules_xcodeproj_generated//generator/Telegram/Telegram_xcodeproj': BUILD file not found in directory 'generator/Telegram/Telegram_xcodeproj' of external repository @rules_xcodeproj_generated. Add a BUILD file to a directory to mark it as a package. +``` + +If you encounter this issue, re-run the project generation steps in the README. + + +# Tips ## Codesigning is not required for simulator-only builds diff --git a/Telegram/BUILD b/Telegram/BUILD index f146b0442fa..a0670469974 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -75,6 +75,12 @@ bool_flag( visibility = ["//visibility:public"], ) +bool_flag( + name = "projectIncludeRelease", + build_setting_default = False, + visibility = ["//visibility:public"], +) + config_setting( name = "disableExtensionsSetting", flag_values = { @@ -89,6 +95,13 @@ config_setting( }, ) +config_setting( + name = "projectIncludeReleaseSetting", + flag_values = { + ":projectIncludeRelease": "True", + }, +) + bool_flag( name = "disableStripping", build_setting_default = False, @@ -1232,7 +1245,7 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/TelegramUI:TelegramUI", + "//submodules/TelegramUI", "//submodules/TelegramUI/Components/ShareExtensionContext" ], ) @@ -2056,9 +2069,9 @@ xcodeproj( "Debug": { "//command_line_option:compilation_mode": "dbg", }, - "Release": { - "//command_line_option:compilation_mode": "opt", - }, + #"Release": { + # "//command_line_option:compilation_mode": "opt", + #}, }, default_xcode_configuration = "Debug" diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 1a312214f4b..c00455c3982 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -722,18 +722,25 @@ private final class NotificationServiceHandler { let semaphore = DispatchSemaphore(value: 0) var loggingSettings = LoggingSettings.defaultSettings - let _ = (self.accountManager.transaction { transaction -> LoggingSettings in + if buildConfig.isInternalBuild { + loggingSettings = LoggingSettings(logToFile: true, logToConsole: false, redactSensitiveData: true) + } + let _ = (self.accountManager.transaction { transaction -> LoggingSettings? in if let value = transaction.getSharedData(SharedDataKeys.loggingSettings)?.get(LoggingSettings.self) { return value } else { - return LoggingSettings.defaultSettings + return nil } }).start(next: { value in - loggingSettings = value + if let value { + loggingSettings = value + } semaphore.signal() }) semaphore.wait() + Logger.shared.log("NotificationService \(episode)", "Logging settings: (logToFile: \(loggingSettings.logToFile))") + Logger.shared.logToFile = loggingSettings.logToFile Logger.shared.logToConsole = loggingSettings.logToConsole Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData @@ -1421,15 +1428,24 @@ private final class NotificationServiceHandler { } } } + + let wasDisplayed = stateManager.postbox.transaction { transaction -> Bool in + if let messageId { + return _internal_getMessageNotificationWasDisplayed(transaction: transaction, id: messageId) + } else { + return false + } + } Logger.shared.log("NotificationService \(episode)", "Will fetch media") let _ = (combineLatest(queue: queue, fetchMediaSignal |> timeout(10.0, queue: queue, alternate: .single(nil)), fetchNotificationSoundSignal - |> timeout(10.0, queue: queue, alternate: .single(nil)) + |> timeout(10.0, queue: queue, alternate: .single(nil)), + wasDisplayed ) - |> deliverOn(queue)).start(next: { mediaData, notificationSoundData in + |> deliverOn(queue)).start(next: { mediaData, notificationSoundData, wasDisplayed in guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return @@ -1523,6 +1539,15 @@ private final class NotificationServiceHandler { Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") + if wasDisplayed { + content = NotificationContent(isLockedMessage: nil) + Logger.shared.log("NotificationService \(episode)", "Was already displayed, skipping content") + } else if let messageId { + let _ = (stateManager.postbox.transaction { transaction -> Void in + _internal_setMessageNotificationWasDisplayed(transaction: transaction, id: messageId) + }).start() + } + updateCurrentContent(content) completed() diff --git a/Telegram/Share/ShareRootController.swift b/Telegram/Share/ShareRootController.swift index fe182a9759d..0808a033f40 100644 --- a/Telegram/Share/ShareRootController.swift +++ b/Telegram/Share/ShareRootController.swift @@ -2,6 +2,9 @@ import UIKit import TelegramUI import BuildConfig import ShareExtensionContext +import SwiftSignalKit +import Postbox +import TelegramCore @objc(ShareRootController) class ShareRootController: UIViewController { @@ -46,7 +49,24 @@ class ShareRootController: UIViewController { let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" - self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appBundleId: baseAppBundleId, appBuildType: buildConfig.isAppStoreBuild ? .public : .internal, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil), useBetaFeatures: !buildConfig.isAppStoreBuild), getExtensionContext: { [weak self] in + self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appBundleId: baseAppBundleId, appBuildType: buildConfig.isAppStoreBuild ? .public : .internal, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil), useBetaFeatures: !buildConfig.isAppStoreBuild, makeTempContext: { accountManager, appLockContext, applicationBindings, InitialPresentationDataAndSettings, networkArguments in + return makeTempContext( + sharedContainerPath: appGroupUrl.path, + rootPath: rootPath, + appGroupPath: appGroupUrl.path, + accountManager: accountManager, + appLockContext: appLockContext, + encryptionParameters: ValueBoxEncryptionParameters( + forceEncryptionIfNoSet: false, + key: ValueBoxEncryptionParameters.Key(data: encryptionParameters.0)!, + salt: ValueBoxEncryptionParameters.Salt(data: encryptionParameters.1)! + ), + applicationBindings: applicationBindings, + initialPresentationDataAndSettings: InitialPresentationDataAndSettings, + networkArguments: networkArguments, + buildConfig: buildConfig + ) + }), getExtensionContext: { [weak self] in return self?.extensionContext }) } diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0a84e01980d..0b4d67059f6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10020,6 +10020,9 @@ Sorry for the inconvenience."; "Stats.Boosts.BoostersNone" = "BOOSTERS"; "Stats.Boosts.Boosters_1" = "%@ BOOSTER"; "Stats.Boosts.Boosters_any" = "%@ BOOSTERS"; +"Stats.Boosts.BoostsNone" = "BOOSTS"; +"Stats.Boosts.Boosts_1" = "%@ BOOST"; +"Stats.Boosts.Boosts_any" = "%@ BOOSTS"; "Stats.Boosts.NoBoostersYet" = "No users currently boost your channel"; "Stats.Boosts.BoostersInfo" = "Your channel is currently boosted by these users."; "Stats.Boosts.ExpiresOn" = "Boost expires on %@"; @@ -10086,3 +10089,325 @@ Sorry for the inconvenience."; "ChannelBoost.BoostLinkForwardTooltip.TwoChats.One" = "Boost link forwarded to **%@** and **%@**"; "ChannelBoost.BoostLinkForwardTooltip.ManyChats.One" = "Boost link forwarded to **%@** and %@ others"; "ChannelBoost.BoostLinkForwardTooltip.SavedMessages.One" = "Boost link forwarded to **Saved Messages**"; + +"ChannelBoost.YouBoostedChannelText" = "You boosted %1$@!"; +"ChannelBoost.YouBoostedOtherChannelText" = "You boosted this channel"; + +"PremiumGift.LabelRecipients_1" = "1 recipient"; +"PremiumGift.LabelRecipients_any" = "%d recipients"; + +"Conversation.ContextMenuQuote" = "Quote"; + +"Message.Giveaway" = "Giveaway"; + +"GiftLink.LinkSharedToChat" = "Gift link forwarded to **%@**"; +"GiftLink.LinkSharedToSavedMessages" = "Gift link forwarded to **Saved Messages**"; + +"ChatContextMenu.TextSelectionTip2" = "Hold on a word, then move cursor to select more| text to copy or quote."; + +"Notification.GiftLink" = "You received a gift"; + +"MESSAGE_GIFTCODE" = "%1$@ sent you a Gift Code for %2$@ months of Telegram Premium"; +"MESSAGE_GIVEAWAY" = "%1$@ sent you a giveaway of %2$@x %3$@mo Premium subscriptions"; +"CHANNEL_MESSAGE_GIVEAWAY" = "%1$@ posted a giveaway of %2$@x %3$@mo Premium subscriptions"; +"CHAT_MESSAGE_GIVEAWAY" = "%1$@ sent a giveaway of %3$@x %4$@mo Premium subscriptions to the group %2$@"; +"PINNED_GIVEAWAY" = "%1$@ pinned a giveaway"; +"REACT_GIVEAWAY" = "%1$@ reacted %2$@ to your giveaway"; +"CHAT_REACT_GIVEAWAY" = "%1$@ reacted %3$@ in group %2$@ to your giveaway"; + +"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"; +"NameColor.Title.Channel" = "Your Channel Color"; + +"NameColor.ChatPreview.Title" = "COLOR PREVIEW"; +"NameColor.ChatPreview.ReplyText.Account" = "Reply to your message"; +"NameColor.ChatPreview.ReplyText.Channel" = "Reply to your channel message"; +"NameColor.ChatPreview.MessageText.Account" = "Your name and replies to your messages will be shown in the selected color."; +"NameColor.ChatPreview.MessageText.Channel" = "The name of your channek and replies to its messages will be shown in the selected color."; +"NameColor.ChatPreview.LinkSite" = "Telegram"; +"NameColor.ChatPreview.LinkTitle" = "Link Preview"; +"NameColor.ChatPreview.LinkText" = "Your selected color will also tint the link preview."; +"NameColor.ChatPreview.Description.Account" = "You can choose an individual color to tint your name, the links you send, and replies to your messages."; +"NameColor.ChatPreview.Description.Channel" = "You can choose an individual color to tint your channel's name, the links it sends, and replies to its messages."; + +"NameColor.ApplyColor" = "Apply Color"; +"NameColor.ApplyColorAndBackgroundEmoji" = "Apply Color and Icon"; + +"NameColor.TooltipPremium.Account" = "Subscribe to [Telegram Premium]() to choose a custom color for your name."; + +"NameColor.BackgroundEmoji.Title" = "ADD ICONS TO REPLIES"; +"NameColor.BackgroundEmoji.Remove" = "REMOVE ICON"; + +"NameColor.UnsavedChanges.Title" = "Unsaved Changes"; +"NameColor.UnsavedChanges.Text" = "You have changed your color or icon settings. Apply changes?"; +"NameColor.UnsavedChanges.Discard" = "Discard"; +"NameColor.UnsavedChanges.Apply" = "Apply"; + +"Chat.ErrorQuoteOutdatedTitle" = "Quote Outdated"; +"Chat.ErrorQuoteOutdatedText" = "**%@** updated the message you are quoting. Edit your quote to make it up-to-date."; +"Chat.ErrorQuoteOutdatedActionEdit" = "Edit"; + +"Premium.BoostByGiftDescription" = "Boost your channel by gifting your subscribers Telegram Premium. [Get boosts >]()"; + +"ChatContextMenu.QuoteSelectionTip" = "Hold on a word, then move cursor to select more| text to quote."; + +"Chat.ReplyPanel.ReplyToQuoteBy" = "Reply to Quote by %@"; +"Chat.ReplyPanel.ReplyTo" = "Reply to %@"; +"Chat.ReplyPanel.AccessibilityReplyToMessageFrom" = "Reply to message. From: %@"; +"Chat.ReplyPanel.AccessibilityReplyToYourMessage" = "Reply to your message."; +"Chat.ReplyPanel.AccessibilityReplyToMessage" = "Reply to message."; +"Chat.ReplyPanel.HintReplyOptions" = "Tap here for reply options"; +"Chat.ReplyPanel.HintReplyOptionsShort" = "Tap here for options"; + +"Chat.ToastQuoteChatUnavailbalePrivateChannel" = "This quote is from a private channel"; +"Chat.ToastQuoteChatUnavailbalePrivateGroup" = "This quote is from a private group"; +"Chat.ToastQuoteChatUnavailbalePrivateChat" = "This quote is from a private chat"; + +"Chat.TitleQuoteSelection" = "Reply to Quote"; +"Chat.TitleReply" = "Reply to Message"; +"Chat.TitleLinkOptions" = "Link Preview Settings"; +"Chat.SubtitleQuoteSelectionTip" = "You can select a specific part to quote"; +"Chat.SubtitleLinkListTip" = "Tap on a link to generate its preview"; + +"Chat.ToastQuoteNotFound" = "Quote not found"; + +"TextFormat.Quote" = "Quote"; + +"TextSelection.SelectAll" = "Select All"; + +"Conversation.MessageOptionsApplyChanges" = "Apply Changes"; +"Conversation.ForwardOptionsCancel" = "Do Not Forward"; +"Conversation.MessageOptionsTabForward" = "Forward"; +"Conversation.MessageOptionsQuoteSelectedPart" = "Quote Selected Part"; +"Conversation.MessageOptionsQuoteSelect" = "Select Specific Quote"; +"Conversation.MessageOptionsQuoteRemove" = "Remove Quote"; +"Conversation.MessageOptionsReplyInAnotherChat" = "Reply in Another Chat"; +"Conversation.MessageOptionsReplyCancel" = "Do Not Reply"; +"Conversation.MessageOptionsTabReply" = "Reply"; +"Conversation.MoveReplyToAnotherChatTitle" = "Reply in..."; +"Conversation.MessageOptionsLinkMoveUp" = "Move Up"; +"Conversation.MessageOptionsLinkMoveDown" = "Move Down"; +"Conversation.MessageOptionsShrinkImage" = "Shrink Photo"; +"Conversation.MessageOptionsEnlargeImage" = "Enlarge Photo"; +"Conversation.MessageOptionsShrinkVideo" = "Shrink Video"; +"Conversation.MessageOptionsEnlargeVideo" = "Enlarge Video"; +"Conversation.LinkOptionsCancel" = "Do Not Preview"; +"Conversation.MessageOptionsTabLink" = "Link"; + +"Stats.Boosts.ShowMoreBoosts_1" = "Show %@ More Boost"; +"Stats.Boosts.ShowMoreBoosts_any" = "Show %@ More Boosts"; + +"ReassignBoost.Title" = "Reassign Boosts"; +"ReassignBoost.Description" = "To boost **%1$@**, reassign a previous boost or gift **Telegram Premium** to a friend to get **%2$@** additional boosts."; +"ReassignBoost.ReassignBoosts" = "Reassign Boosts"; +"ReassignBoost.AvailableIn" = "Available in %@"; +"ReassignBoost.ExpiresOn" = "Boost expires on %@"; +"ReassignBoost.WaitForCooldown" = "Wait until the boost is available or get **%1$@** more boosts by gifting a **Telegram Premium** subscription."; + +"ReassignBoost.Success" = "%1$@ are reassigned from %2$@."; +"ReassignBoost.Boosts_1" = "%@ boost"; +"ReassignBoost.Boosts_any" = "%@ boosts"; +"ReassignBoost.OtherChannels_1" = "%@ other channel"; +"ReassignBoost.OtherChannels_any" = "%@ other channels"; + +"ChannelBoost.MoreBoosts.Title" = "More Boosts Needed"; +"ChannelBoost.MoreBoosts.Text" = "To boost **%1$@** again, gift **Telegram Premium** to a friend and get **%2$@** additional boosts."; + +"BoostGift.Title" = "Boosts via Gifts"; +"BoostGift.Description" = "Get more boosts for your channel by gifting\nPremium to your subscribers."; +"BoostGift.PrepaidGiveawayTitle" = "PREPAID GIVEAWAY"; +"BoostGift.PrepaidGiveawayCount_1" = "%@ Telegram Premium"; +"BoostGift.PrepaidGiveawayCount_any" = "%@ Telegram Premium"; +"BoostGift.PrepaidGiveawayMonths" = "%@-month subscriptions"; +"BoostGift.CreateGiveaway" = "Create Giveaway"; +"BoostGift.CreateGiveawayInfo" = "winners are chosen randomly"; +"BoostGift.AwardSpecificUsers" = "Award Specific Users"; +"BoostGift.SelectRecipients" = "select recipients"; +"BoostGift.QuantityTitle" = "QUANTITY OF PRIZES"; +"BoostGift.QuantityBoosts_1" = "%@ BOOST"; +"BoostGift.QuantityBoosts_any" = "%@ BOOSTS"; +"BoostGift.QuantityInfo" = "Choose how many Premium subscriptions to give away and boosts to receive."; +"BoostGift.ChannelsTitle" = "CHANNELS INCLUDED IN THE GIVEAWAY"; +"BoostGift.AddChannel" = "Add Channel"; +"BoostGift.ChannelsBoosts_1" = "this channel will receive %@ boost"; +"BoostGift.ChannelsBoosts_any" = "this channel will receive %@ boosts"; +"BoostGift.ChannelsInfo" = "Choose the channels users need to be subscribed to take part in the giveaway"; +"BoostGift.UsersTitle" = "USERS ELIGIBLE FOR THE GIVEAWAY"; +"BoostGift.FromCountries_1" = "from %@ country"; +"BoostGift.FromCountries_any" = "from %@ countries"; +"BoostGift.FromTwoCountries" = "from %1$@ and %2$@"; +"BoostGift.FromOneCountry" = "from %1$@"; +"BoostGift.FromAllCountries" = "from all countries"; +"BoostGift.AllSubscribers" = "All subscribers"; +"BoostGift.OnlyNewSubscribers" = "Only new subscribers"; +"BoostGift.LimitSubscribersInfo" = "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started."; +"BoostGift.DateTitle" = "DATE WHEN GIVEAWAY ENDS"; +"BoostGift.DateEnds" = "Ends"; +"BoostGift.DateInfo" = "Choose when %1$@ of your channel will be randomly selected to receive Telegram Premium."; +"BoostGift.DateInfoSubscribers_1" = "%@ subscriber"; +"BoostGift.DateInfoSubscribers_any" = "%@ subscribers"; +"BoostGift.DurationTitle" = "DURATION OF PREMIUM SUBSCRIPTIONS"; +"BoostGift.PremiumInfo" = "You can review the list of features and terms of use for Telegram Premium [here]()."; +"BoostGift.GiftPremium" = "Gift Premium"; +"BoostGift.StartGiveaway" = "Start Giveaway"; +"BoostGift.ReduceQuantity.Title" = "Reduce Quantity"; +"BoostGift.ReduceQuantity.Text" = "You can't acquire %1$@ %2$@-month subscriptions in the app. Do you want to reduce quantity to %3$@?"; +"BoostGift.ReduceQuantity.Reduce" = "Reduce"; +"BoostGift.GiveawayCreated.Title" = "Giveaway Created"; +"BoostGift.GiveawayCreated.Text" = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."; +"BoostGift.PremiumGifted.Title" = "Premium Subscriptions Gifted"; +"BoostGift.PremiumGifted.Text" = "Check your channel's [Statistics]() to see how gifts boosted your channel."; + +"BoostGift.Subscribers.Title" = "Gift Premium"; +"BoostGift.Subscribers.Subtitle" = "select up to %@ subscribers"; +"BoostGift.Subscribers.SectionTitle" = "SUBSCRIBERS"; +"BoostGift.Subscribers.Joined" = "joined %@"; +"BoostGift.Subscribers.Search" = "Search Subscribers"; +"BoostGift.Subscribers.MaximumReached" = "You can select maximum %@ subscribers."; +"BoostGift.Subscribers.Save" = "Save Recipients"; + +"BoostGift.Channels.Title" = "Add Channels"; +"BoostGift.Channels.Subtitle" = "select up to %@ channels"; +"BoostGift.Channels.SectionTitle" = "CHANNELS"; +"BoostGift.Channels.Search" = "Search Channels"; +"BoostGift.Channels.MaximumReached" = "You can select maximum %@ channels."; +"BoostGift.Channels.PrivateChannel.Title" = "Channel is Private"; +"BoostGift.Channels.PrivateChannel.Text" = "Are you sure you want to add a private channel? Users won't be able to join it without an invite link."; +"BoostGift.Channels.PrivateChannel.Add" = "Add"; +"BoostGift.Channels.Save" = "Save Channels"; + +"Stats.Boosts.PrepaidGiveawaysTitle" = "PREPAID GIVEAWAYS"; +"Stats.Boosts.PrepaidGiveawayCount_1" = "%@ Telegram Premium"; +"Stats.Boosts.PrepaidGiveawayCount_any" = "%@ Telegram Premiums"; +"Stats.Boosts.PrepaidGiveawayMonths" = "%@-month subscriptions"; +"Stats.Boosts.PrepaidGiveawaysInfo" = "Select a giveaway you already paid for to set it up."; +"Stats.Boosts.ShortMonth" = "%@m"; +"Stats.Boosts.Giveaway" = "Giveaway"; +"Stats.Boosts.Gift" = "Gift"; +"Stats.Boosts.TabBoosts_1" = "%@ Boost"; +"Stats.Boosts.TabBoosts_any" = "%@ Boosts"; +"Stats.Boosts.TabGifts_1" = "%@ Boost"; +"Stats.Boosts.TabGifts_any" = "%@ Boosts"; +"Stats.Boosts.ToBeDistributed" = "To Be Distributed"; +"Stats.Boosts.Unclaimed" = "Unclaimed"; +"Stats.Boosts.GetBoosts" = "Get Boosts via Gifts"; +"Stats.Boosts.GetBoostsInfo" = "Get more boosts for your channel by gifting Premium to your subscribers."; +"Stats.Boosts.TooltipToBeDistributed" = "The recipient will be selected when the giveaway ends."; + +"Notification.PremiumPrize.Title" = "Congratulations!"; +"Notification.PremiumPrize.UnclaimedText" = "You have an unclaimed prize from a giveaway by **%1$@**.\n\nThis prize is a **Telegram Premium** subscription for %2$@."; +"Notification.PremiumPrize.GiveawayText" = "You won a prize in a giveaway organized by **%1$@**.\n\nYour prize is a **Telegram Premium** subscription for %2$@."; +"Notification.PremiumPrize.GiftText" = "You've received a gift from **%1$@**.\n\nYour gift is a **Telegram Premium** subscription for %2$@."; +"Notification.PremiumPrize.Months_1" = "**%@** month"; +"Notification.PremiumPrize.Months_any" = "**%@** months"; +"Notification.PremiumPrize.View" = "Open Gift Link"; +"Notification.PremiumPrize.Unclaimed" = "Unclaimed Prize"; + +"Story.SlideToSeek" = "Slide left or right to seek"; +"Story.Guide.Title" = "Watching Stories"; +"Story.Guide.Description" = "You can use these gestures to control playback."; +"Story.Guide.ForwardTitle" = "Go forward"; +"Story.Guide.ForwardDescription" = "Tap the screen"; +"Story.Guide.PauseTitle" = "Pause and Seek"; +"Story.Guide.PauseDescription" = "Hold and move sideways"; +"Story.Guide.BackTitle" = "Go back"; +"Story.Guide.BackDescription" = "Tap the left edge"; +"Story.Guide.MoveTitle" = "Move between authors"; +"Story.Guide.MoveDescription" = "Swipe left or right"; +"Story.Guide.Proceed" = "Tap to keep watching"; + +"Chat.Giveaway.Info.Title" = "About This Giveaway"; +"Chat.Giveaway.Info.EndedTitle" = "Giveaway Ended"; +"Chat.Giveaway.Info.AlmostOver" = "The giveaway is almost over."; +"Chat.Giveaway.Info.OngoingIntro" = "The giveaway is sponsored by the admins of **%1$@**, who acquired %2$@ for %3$@ for its followers."; +"Chat.Giveaway.Info.OngoingNewMany" = "On **%1$@**, Telegram will automatically select %2$@ that joined **%3$@** and %4$@ after **%5$@**."; +"Chat.Giveaway.Info.OngoingNew" = "On **%1$@**, Telegram will automatically select %2$@ that joined **%3$@** after **%4$@**."; +"Chat.Giveaway.Info.OngoingMany" = "On **%1$@**, Telegram will automatically select %2$@ of **%3$@** and %4$@."; +"Chat.Giveaway.Info.Ongoing" = "On **%1$@**, Telegram will automatically select %2$@ of **%3$@**."; +"Chat.Giveaway.Info.EndedIntro" = "The giveaway was sponsored by the admins of **%1$@**, who acquired %2$@ for %3$@ for its followers."; +"Chat.Giveaway.Info.EndedNewMany" = "On **%1$@**, Telegram automatically selected %2$@ that joined **%3$@** and other listed channels after **%4$@**."; +"Chat.Giveaway.Info.EndedNew" = "On **%1$@**, Telegram automatically selected %2$@ that joined **%3$@** after **%4$@**."; +"Chat.Giveaway.Info.EndedMany" = "On **%1$@**, Telegram automatically selected %2$@ of **%3$@** and other listed channels."; +"Chat.Giveaway.Info.Ended" = "On **%1$@**, Telegram automatically selected %2$@ of **%3$@**."; +"Chat.Giveaway.Info.NotAllowedJoinedEarly" = "You are not eligible to participate in this giveaway, because you joined this channel on **%@**, which is before the contest started."; +"Chat.Giveaway.Info.NotAllowedAdmin" = "You are not eligible to participate in this giveaway, because you are an admin of participating channel (**%@**)."; +"Chat.Giveaway.Info.NotAllowedCountry" = "You are not eligible to participate in this giveaway, because your country is not included in the terms of the giveaway."; +"Chat.Giveaway.Info.NotQualified" = "To take part in this giveaway please join the channel **%1$@** before **%2$@**."; +"Chat.Giveaway.Info.NotQualifiedMany" = "To take part in this giveaway please join the channel **%1$@** (and %2$@) before **%3$@**."; +"Chat.Giveaway.Info.Participating" = "You are participating in this giveaway, because you have joined the channel **%1$@**."; +"Chat.Giveaway.Info.ParticipatingMany" = "You are participating in this giveaway, because you have joined the channel **%1$@** (and %2$@)."; +"Chat.Giveaway.Info.OtherChannels_1" = "**%@** other listed channel"; +"Chat.Giveaway.Info.OtherChannels_any" = "**%@** other listed channels"; +"Chat.Giveaway.Info.Subscriptions_1" = "**%@ Telegram Premium** subscription"; +"Chat.Giveaway.Info.Subscriptions_any" = "**%@ Telegram Premium** subscriptions"; +"Chat.Giveaway.Info.RandomUsers_1" = "**%@** random user"; +"Chat.Giveaway.Info.RandomUsers_any" = "**%@** random user"; +"Chat.Giveaway.Info.RandomSubscribers_1" = "**%@** random subscriber"; +"Chat.Giveaway.Info.RandomSubscribers_any" = "**%@** random subscribers"; +"Chat.Giveaway.Info.Months_1" = "**%@** month"; +"Chat.Giveaway.Info.Months_any" = "**%@** months"; +"Chat.Giveaway.Info.ActivatedLinks_1" = "%@ winner already used their gift link."; +"Chat.Giveaway.Info.ActivatedLinks_any" = "%@ of the winners already used their gift links."; +"Chat.Giveaway.Info.Refunded" = "The channel cancelled the prizes by reversing the payment for them."; +"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.Toast.NotAllowed" = "You can't participate in this giveaway."; +"Chat.Giveaway.Toast.Participating" = "You are participating in this giveaway."; +"Chat.Giveaway.Toast.NotQualified" = "You are not qualified for this giveaway yet."; +"Chat.Giveaway.Toast.AlmostOver" = "The giveaway is almost over."; +"Chat.Giveaway.Toast.Ended" = "The giveaway is ended."; +"Chat.Giveaway.Toast.LearnMore" = "Learn More"; + +"Chat.Giveaway.Message.PrizeTitle" = "Giveaway Prizes"; +"Chat.Giveaway.Message.PrizeText" = "%1$@ for %2$@."; +"Chat.Giveaway.Message.Subscriptions_1" = "**%@** Telegram Premium Subscription"; +"Chat.Giveaway.Message.Subscriptions_any" = "**%@** Telegram Premium Subscriptions"; +"Chat.Giveaway.Message.Months_1" = "**%@** month"; +"Chat.Giveaway.Message.Months_any" = "**%@** months"; +"Chat.Giveaway.Message.ParticipantsTitle" = "Participants"; +"Chat.Giveaway.Message.ParticipantsNewMany" = "All users who join the channels below after this date:"; +"Chat.Giveaway.Message.ParticipantsNew" = "All users who join this channel after this date:"; +"Chat.Giveaway.Message.ParticipantsMany" = "All subscribers of the channels below:"; +"Chat.Giveaway.Message.Participants" = "All subscribers of this channel:"; +"Chat.Giveaway.Message.CountriesFrom" = "from %@"; +"Chat.Giveaway.Message.CountriesDelimiter" = ", "; +"Chat.Giveaway.Message.CountriesLastDelimiter" = " and "; +"Chat.Giveaway.Message.DateTitle" = "Winners Selection Date"; +"Chat.Giveaway.Message.LearnMore" = "LEARN MORE"; + +"GiftLink.Title" = "Gift Link"; +"GiftLink.UsedTitle" = "Used Gift Link"; +"GiftLink.Description" = "This link allows you to activate a **Telegram Premium** subscription."; +"GiftLink.UsedDescription" = "This link was used to activate a **Telegram Premium** subscription."; +"GiftLink.PersonalDescription" = "This link allows **%@** to activate a **Telegram Premium** subscription."; +"GiftLink.PersonalUsedDescription" = "This link allowed **%@** to activate a **Telegram Premium** subscription."; +"GiftLink.UnclaimedDescription" = "This link allows anyone to activate a **Telegram Premium** subscription."; +"GiftLink.Footer" = "You can also [send this link]() to a friend as a gift."; +"GiftLink.UsedFooter" = "This link was used on %@."; +"GiftLink.NotUsedFooter" = "This link hasn't been used yet."; +"GiftLink.UseLink" = "Use Link"; +"GiftLink.Gift" = "Gift"; +"GiftLink.From" = "From"; +"GiftLink.To" = "To"; +"GiftLink.Reason" = "Reason"; +"GiftLink.Reason.Giveaway" = "Giveaway"; +"GiftLink.Reason.Gift" = "You were selected by the channel"; +"GiftLink.Reason.Unclaimed" = "Incomplete Giveaway"; +"GiftLink.Date" = "Date"; +"GiftLink.NoRecipient" = "No recipient"; +"GiftLink.TelegramPremium_1" = "Telegram Premium for %@ month"; +"GiftLink.TelegramPremium_any" = "Telegram Premium for %@ months"; +"GiftLink.LinkHidden" = "Only the recipient can see the link."; + +"ChannelBoost.EnableColors" = "Enable Colors"; +"ChannelBoost.EnableColorsText" = "Your channel needs %1$@ to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:"; +"ChannelBoost.BoostAgain" = "Boost Again"; diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index 48658763c94..69583071ca0 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -547,6 +547,7 @@ def generate_project(bazel, arguments): disable_extensions = False disable_provisioning_profiles = False + project_include_release = False generate_dsym = False target_name = "Telegram" @@ -554,6 +555,8 @@ def generate_project(bazel, arguments): disable_extensions = arguments.disableExtensions if arguments.disableProvisioningProfiles is not None: disable_provisioning_profiles = arguments.disableProvisioningProfiles + if arguments.projectIncludeRelease is not None: + project_include_release = arguments.projectIncludeRelease if arguments.xcodeManagedCodesigning is not None and arguments.xcodeManagedCodesigning == True: disable_extensions = True if arguments.generateDsym is not None: @@ -567,6 +570,7 @@ def generate_project(bazel, arguments): build_environment=bazel_command_line.build_environment, disable_extensions=disable_extensions, disable_provisioning_profiles=disable_provisioning_profiles, + include_release=project_include_release, generate_dsym=generate_dsym, configuration_path=bazel_command_line.configuration_path, bazel_app_arguments=bazel_command_line.get_project_generation_arguments(), @@ -840,6 +844,15 @@ def add_project_and_build_common_arguments(current_parser: argparse.ArgumentPars ''' ) + generateProjectParser.add_argument( + '--projectIncludeRelease', + action='store_true', + default=False, + help=''' + Generate the Xcode project with Debug and Release configurations. + ''' + ) + generateProjectParser.add_argument( '--generateDsym', action='store_true', diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index 3fe31df8e90..37e2ca22bca 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -9,7 +9,7 @@ def remove_directory(path): if os.path.isdir(path): shutil.rmtree(path) -def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name): +def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name): if '/' in target_name: app_target_spec = target_name.split('/')[0] + '/' + target_name.split('/')[1] + ':' + target_name.split('/')[1] app_target = target_name @@ -49,141 +49,5 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, call_executable(['open', xcodeproj_path]) -def generate_tulsi(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name): - project_path = os.path.join(build_environment.base_path, 'build-input/gen/project') - - if '/' in target_name: - app_target_spec = target_name.split('/')[0] + '/' + target_name.split('/')[1] + ':' + target_name.split('/')[1] - app_target = target_name - app_target_clean = app_target.replace('/', '_') - else: - app_target_spec = '{target}:{target}'.format(target=target_name) - app_target = target_name - app_target_clean = app_target.replace('/', '_') - - os.makedirs(project_path, exist_ok=True) - remove_directory('{}/Tulsi.app'.format(project_path)) - remove_directory('{project}/{target}.tulsiproj'.format(project=project_path, target=app_target_clean)) - - tulsi_path = os.path.join(project_path, 'Tulsi.app/Contents/MacOS/Tulsi') - - tulsi_build_bazel_path = build_environment.bazel_path - - current_dir = os.getcwd() - os.chdir(os.path.join(build_environment.base_path, 'build-system/tulsi')) - - tulsi_build_command = [] - tulsi_build_command += [tulsi_build_bazel_path] - tulsi_build_command += ['build', '//:tulsi'] - if is_apple_silicon(): - tulsi_build_command += ['--macos_cpus=arm64'] - tulsi_build_command += ['--xcode_version={}'.format(build_environment.xcode_version)] - tulsi_build_command += ['--use_top_level_targets_for_symlinks'] - tulsi_build_command += ['--verbose_failures'] - tulsi_build_command += ['--swiftcopt=-whole-module-optimization'] - - call_executable(tulsi_build_command) - - os.chdir(current_dir) - - bazel_wrapper_path = os.path.abspath('build-input/gen/project/bazel') - - bazel_wrapper_arguments = [] - bazel_wrapper_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)] - - with open(bazel_wrapper_path, 'wb') as bazel_wrapper: - bazel_wrapper.write('''#!/bin/sh -{bazel} "$@" {arguments} -'''.format( - bazel=build_environment.bazel_path, - arguments=' '.join(bazel_wrapper_arguments) - ).encode('utf-8')) - - call_executable(['chmod', '+x', bazel_wrapper_path]) - - call_executable([ - 'unzip', '-oq', - 'build-system/tulsi/bazel-bin/tulsi.zip', - '-d', project_path - ]) - - user_defaults_path = os.path.expanduser('~/Library/Preferences/com.google.Tulsi.plist') - if os.path.isfile(user_defaults_path): - os.unlink(user_defaults_path) - - with open(user_defaults_path, 'wb') as user_defaults: - user_defaults.write(''' - - - - - defaultBazelURL - {} - - -'''.format(bazel_wrapper_path).encode('utf-8')) - - bazel_build_arguments = [] - bazel_build_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)] - if disable_extensions: - bazel_build_arguments += ['--//{}:disableExtensions'.format(app_target)] - if disable_provisioning_profiles: - bazel_build_arguments += ['--//{}:disableProvisioningProfiles'.format(app_target)] - if generate_dsym: - bazel_build_arguments += ['--apple_generate_dsym'] - bazel_build_arguments += ['--//{}:disableStripping'.format(app_target)] - bazel_build_arguments += ['--strip=never'] - - call_executable([ - tulsi_path, - '--', - '--verbose', - '--create-tulsiproj', app_target_clean, - '--workspaceroot', './', - '--bazel', bazel_wrapper_path, - '--outputfolder', project_path, - '--target', '{target_spec}'.format(target_spec=app_target_spec), - '--build-options', ' '.join(bazel_build_arguments) - ]) - - additional_arguments = [] - additional_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)] - additional_arguments += bazel_app_arguments - if disable_extensions: - additional_arguments += ['--//{}:disableExtensions'.format(app_target)] - - additional_arguments_string = ' '.join(additional_arguments) - - tulsi_config_path = 'build-input/gen/project/{target}.tulsiproj/Configs/{target}.tulsigen'.format(target=app_target_clean) - with open(tulsi_config_path, 'rb') as tulsi_config: - tulsi_config_json = json.load(tulsi_config) - for category in ['BazelBuildOptionsDebug', 'BazelBuildOptionsRelease']: - tulsi_config_json['optionSet'][category]['p'] += ' {}'.format(additional_arguments_string) - tulsi_config_json['sourceFilters'] = [ - 'Nicegram/...', - '{}/...'.format(app_target), - 'submodules/...', - 'third-party/...' - ] - with open(tulsi_config_path, 'wb') as tulsi_config: - tulsi_config.write(json.dumps(tulsi_config_json, indent=2).encode('utf-8')) - - call_executable([ - tulsi_path, - '--', - '--verbose', - '--genconfig', '{project}/{target}.tulsiproj:{target}'.format(project=project_path, target=app_target_clean), - '--bazel', bazel_wrapper_path, - '--outputfolder', project_path, - '--no-open-xcode' - ]) - - xcodeproj_path = '{project}/{target}.xcodeproj'.format(project=project_path, target=app_target_clean) - - call_executable(['open', xcodeproj_path]) - - -def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name): - generate_xcodeproj(build_environment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name) - #generate_tulsi(build_environment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name) - +def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name): + generate_xcodeproj(build_environment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name) diff --git a/ci/fastlane/Fastfile b/ci/fastlane/Fastfile index a59d7f24497..0dbb149112d 100644 --- a/ci/fastlane/Fastfile +++ b/ci/fastlane/Fastfile @@ -266,7 +266,7 @@ lane :generate_project do |options| --cacheDir=#{BAZEL_LOCAL_CACHE} \ generateProject \ --configurationPath=#{configuration_path} \ - --buildNumber=172 \ + --buildNumber=555 \ --gitCodesigningRepository=#{SIGN_URL} \ --gitCodesigningType=development" end diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 9ab64478a30..7f52bfaf72e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -300,7 +300,13 @@ public enum ResolvedUrl { case premiumOffer(reference: String?) case chatFolder(slug: String) case story(peerId: PeerId, id: Int32) - case boost(peerId: PeerId, status: ChannelBoostStatus?, canApplyStatus: CanApplyBoostStatus) + case boost(peerId: PeerId, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) + case premiumGiftCode(slug: String) +} + +public enum ResolveUrlResult { + case progress + case result(ResolvedUrl) } public enum NavigateToChatKeepStack { @@ -662,9 +668,10 @@ public final class ContactSelectionControllerParams { public let displayDeviceContacts: Bool public let displayCallIcons: Bool public let multipleSelection: Bool + public let requirePhoneNumbers: Bool public let confirmation: (ContactListPeer) -> Signal - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { self.context = context self.updatedPresentationData = updatedPresentationData self.autoDismiss = autoDismiss @@ -673,6 +680,7 @@ public final class ContactSelectionControllerParams { self.displayDeviceContacts = displayDeviceContacts self.displayCallIcons = displayCallIcons self.multipleSelection = multipleSelection + self.requirePhoneNumbers = requirePhoneNumbers self.confirmation = confirmation } } @@ -890,12 +898,15 @@ public protocol SharedAccountContext: AnyObject { func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set) -> Signal func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal + func resolveUrlWithProgress(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) + func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) + func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController @@ -915,6 +926,8 @@ public protocol SharedAccountContext: AnyObject { func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController + func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?, statsDatacenterId: Int32) -> ViewController + func makeDebugSettingsController(context: AccountContext?) -> ViewController? func navigateToCurrentCall() @@ -993,7 +1006,7 @@ public enum PremiumLimitSubject { case expiringStories case storiesWeekly case storiesMonthly - case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, boosted: Bool) + case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool) } public protocol ComposeController: ViewController { @@ -1037,6 +1050,7 @@ public protocol AccountContext: AnyObject { var isPremium: Bool { get } var userLimits: EngineConfiguration.UserLimits { get } + var peerNameColors: PeerNameColors { get } var imageCache: AnyObject? { get } @@ -1055,17 +1069,21 @@ public protocol AccountContext: AnyObject { public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { - return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false) + return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3) } public let isPremiumDisabled: Bool public let showPremiumGiftInAttachMenu: Bool public let showPremiumGiftInTextField: Bool + public let giveawayGiftsPurchaseAvailable: Bool + public let boostsPerGiftCount: Int32 - fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool) { + fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32) { self.isPremiumDisabled = isPremiumDisabled self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField + self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable + self.boostsPerGiftCount = boostsPerGiftCount } public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration { @@ -1073,7 +1091,9 @@ public struct PremiumConfiguration { 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 + 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) ) } else { return .defaultValue @@ -1187,3 +1207,138 @@ public struct StickersSearchConfiguration { } } } + +public class PeerNameColors: Equatable { + public struct Colors: Equatable { + public let main: UIColor + public let secondary: UIColor? + public let tertiary: UIColor? + + init(main: UIColor, secondary: UIColor?, tertiary: UIColor?) { + self.main = main + self.secondary = secondary + self.tertiary = tertiary + } + + init(main: UIColor) { + self.main = main + self.secondary = nil + self.tertiary = nil + } + + init?(colors: [UIColor]) { + guard let first = colors.first else { + return nil + } + self.main = first + if colors.count == 3 { + self.secondary = colors[1] + self.tertiary = colors[2] + } else if colors.count == 2, let second = colors.last { + self.secondary = second + self.tertiary = nil + } else { + self.secondary = nil + self.tertiary = nil + } + } + } + + public static var defaultSingleColors: [Int32: Colors] { + return [ + 0: Colors(main: UIColor(rgb: 0xcc5049)), + 1: Colors(main: UIColor(rgb: 0xd67722)), + 2: Colors(main: UIColor(rgb: 0x955cdb)), + 3: Colors(main: UIColor(rgb: 0x40a920)), + 4: Colors(main: UIColor(rgb: 0x309eba)), + 5: Colors(main: UIColor(rgb: 0x368ad1)), + 6: Colors(main: UIColor(rgb: 0xc7508b)) + ] + } + + public static var defaultValue: PeerNameColors { + return PeerNameColors( + colors: defaultSingleColors, + darkColors: [:], + displayOrder: [5, 3, 1, 0, 2, 4, 6] + ) + } + + public let colors: [Int32: Colors] + public let darkColors: [Int32: Colors] + public let displayOrder: [Int32] + + public func get(_ color: PeerNameColor, dark: Bool = false) -> Colors { + if dark, let colors = self.darkColors[color.rawValue] { + return colors + } else if let colors = self.colors[color.rawValue] { + return colors + } else { + return PeerNameColors.defaultSingleColors[5]! + } + } + + fileprivate init(colors: [Int32: Colors], darkColors: [Int32: Colors], displayOrder: [Int32]) { + self.colors = colors + self.darkColors = darkColors + self.displayOrder = displayOrder + } + + public static func with(appConfiguration: AppConfiguration) -> PeerNameColors { + if let data = appConfiguration.data { + var colors = PeerNameColors.defaultSingleColors + var darkColors: [Int32: Colors] = [:] + + if let peerColors = data["peer_colors"] as? [String: [String]] { + for (key, values) in peerColors { + if let index = Int32(key) { + let colorsArray = values.compactMap { UIColor(hexString: $0) } + if let colorValues = Colors(colors: colorsArray) { + colors[index] = colorValues + } + } + } + } + + if let darkPeerColors = data["dark_peer_colors"] as? [String: [String]] { + for (key, values) in darkPeerColors { + if let index = Int32(key) { + let colorsArray = values.compactMap { UIColor(hexString: $0) } + if let colorValues = Colors(colors: colorsArray) { + darkColors[index] = colorValues + } + } + } + } + + var displayOrder: [Int32] = [] + if let order = data["peer_colors_available"] as? [Double] { + displayOrder = order.map { Int32($0) } + } + if displayOrder.isEmpty { + displayOrder = PeerNameColors.defaultValue.displayOrder + } + + return PeerNameColors( + colors: colors, + darkColors: darkColors, + displayOrder: displayOrder + ) + } else { + return .defaultValue + } + } + + public static func == (lhs: PeerNameColors, rhs: PeerNameColors) -> Bool { + if lhs.colors != rhs.colors { + return false + } + if lhs.darkColors != rhs.darkColors { + return false + } + if lhs.displayOrder != rhs.displayOrder { + return false + } + return true + } +} diff --git a/submodules/AccountContext/Sources/AttachmentMainButtonState.swift b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift new file mode 100644 index 00000000000..3bcceef06d2 --- /dev/null +++ b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift @@ -0,0 +1,50 @@ +import Foundation +import UIKit + +public struct AttachmentMainButtonState { + public enum Background { + case color(UIColor) + case premium + } + + public enum Progress: Equatable { + case none + case side + case center + } + + public enum Font: Equatable { + case regular + case bold + } + + public let text: String? + public let font: Font + public let background: Background + public let textColor: UIColor + public let isVisible: Bool + public let progress: Progress + public let isEnabled: Bool + + public init( + text: String?, + font: Font, + background: Background, + textColor: UIColor, + isVisible: Bool, + progress: Progress, + isEnabled: Bool + ) { + self.text = text + self.font = font + self.background = background + self.textColor = textColor + self.isVisible = isVisible + self.progress = progress + self.isEnabled = isEnabled + } + + public static var initial: AttachmentMainButtonState { + return AttachmentMainButtonState(text: nil, font: .bold, background: .color(.clear), textColor: .clear, isVisible: false, progress: .none, isEnabled: false) + } +} diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 86d64529aa6..bbc7ed935f4 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -321,6 +321,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case strikethrough case underline case spoiler + case quote public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) @@ -348,6 +349,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { self = .underline case 8: self = .spoiler + case 9: + self = .quote default: assertionFailure() self = .bold @@ -379,6 +382,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { try container.encode(7 as Int32, forKey: "t") case .spoiler: try container.encode(8 as Int32, forKey: "t") + case .quote: + try container.encode(9 as Int32, forKey: "t") } } } @@ -452,6 +457,9 @@ public struct ChatTextInputStateText: Codable, Equatable { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.spoiler { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.quote, let value = value as? ChatTextInputTextQuoteAttribute { + let _ = value + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .quote, range: range.location ..< (range.location + range.length))) } } }) @@ -496,6 +504,8 @@ public struct ChatTextInputStateText: Codable, Equatable { result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case .spoiler: result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case .quote: + result.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } } return result @@ -509,8 +519,8 @@ public enum ChatControllerSubject: Equatable { } public struct ForwardOptions: Equatable { - public let hideNames: Bool - public let hideCaptions: Bool + public var hideNames: Bool + public var hideCaptions: Bool public init(hideNames: Bool, hideCaptions: Bool) { self.hideNames = hideNames @@ -518,10 +528,122 @@ public enum ChatControllerSubject: Equatable { } } - case message(id: MessageSubject, highlight: Bool, timecode: Double?) + public struct LinkOptions: Equatable { + public var messageText: String + public var messageEntities: [MessageTextEntity] + public var hasAlternativeLinks: Bool + public var replyMessageId: EngineMessage.Id? + public var replyQuote: String? + public var url: String + public var webpage: TelegramMediaWebpage + public var linkBelowText: Bool + public var largeMedia: Bool + + public init( + messageText: String, + messageEntities: [MessageTextEntity], + hasAlternativeLinks: Bool, + replyMessageId: EngineMessage.Id?, + replyQuote: String?, + url: String, + webpage: TelegramMediaWebpage, + linkBelowText: Bool, + largeMedia: Bool + ) { + self.messageText = messageText + self.messageEntities = messageEntities + self.hasAlternativeLinks = hasAlternativeLinks + self.replyMessageId = replyMessageId + self.replyQuote = replyQuote + self.url = url + self.webpage = webpage + self.linkBelowText = linkBelowText + self.largeMedia = largeMedia + } + } + + public enum MessageOptionsInfo: Equatable { + public struct Quote: Equatable { + public let messageId: EngineMessage.Id + public let text: String + + public init(messageId: EngineMessage.Id, text: String) { + self.messageId = messageId + self.text = text + } + } + + public struct SelectionState: Equatable { + public var canQuote: Bool + public var quote: Quote? + + public init(canQuote: Bool, quote: Quote?) { + self.canQuote = canQuote + self.quote = quote + } + } + + public struct Reply: Equatable { + public var quote: Quote? + public var selectionState: Promise + + public init(quote: Quote?, selectionState: Promise) { + self.quote = quote + self.selectionState = selectionState + } + + public static func ==(lhs: Reply, rhs: Reply) -> Bool { + if lhs.quote != rhs.quote { + return false + } + if lhs.selectionState !== rhs.selectionState { + return false + } + return true + } + } + + public struct Forward: Equatable { + public var options: Signal + + public init(options: Signal) { + self.options = options + } + + public static func ==(lhs: Forward, rhs: Forward) -> Bool { + return true + } + } + + public struct Link: Equatable { + public var options: Signal + + public init(options: Signal) { + self.options = options + } + + public static func ==(lhs: Link, rhs: Link) -> Bool { + return true + } + } + + case reply(Reply) + case forward(Forward) + case link(Link) + } + + public struct MessageHighlight: Equatable { + public var quote: String? + + public init(quote: String? = nil) { + self.quote = quote + } + } + + case message(id: MessageSubject, highlight: MessageHighlight?, timecode: Double?) case scheduledMessages case pinnedMessages(id: EngineMessage.Id?) - case forwardedMessages(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], options: Signal) + case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo) public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool { switch lhs { @@ -543,8 +665,8 @@ public enum ChatControllerSubject: Equatable { } else { return false } - case let .forwardedMessages(lhsPeerIds, lhsIds, _): - if case let .forwardedMessages(rhsPeerIds, rhsIds, _) = rhs, lhsPeerIds == rhsPeerIds, lhsIds == rhsIds { + case let .messageOptions(lhsPeerIds, lhsIds, lhsInfo): + if case let .messageOptions(rhsPeerIds, rhsIds, rhsInfo) = rhs, lhsPeerIds == rhsPeerIds, lhsIds == rhsIds, lhsInfo == rhsInfo { return true } else { return false @@ -663,6 +785,32 @@ public protocol PeerInfoScreen: ViewController { var peerId: PeerId { get } } +public extension Peer { + func canSetupAutoremoveTimeout(accountPeerId: EnginePeer.Id) -> Bool { + if let _ = self as? TelegramSecretChat { + return false + } else if let group = self as? TelegramGroup { + if case .creator = group.role { + return true + } else if case let .admin(rights, _) = group.role { + if rights.rights.contains(.canDeleteMessages) { + return true + } + } + } else if let user = self as? TelegramUser { + if user.id != accountPeerId && user.botInfo == nil { + return true + } + } else if let channel = self as? TelegramChannel { + if channel.hasPermission(.deleteAllMessages) { + return true + } + } + + return false + } +} + public protocol ChatController: ViewController { var chatLocation: ChatLocation { get } var canReadHistory: ValuePromise { get } diff --git a/submodules/AccountContext/Sources/ChatHistoryLocation.swift b/submodules/AccountContext/Sources/ChatHistoryLocation.swift index 7241d181a78..f85a351a724 100644 --- a/submodules/AccountContext/Sources/ChatHistoryLocation.swift +++ b/submodules/AccountContext/Sources/ChatHistoryLocation.swift @@ -7,11 +7,31 @@ public enum ChatHistoryInitialSearchLocation: Equatable { case id(MessageId) } +public struct MessageHistoryScrollToSubject: Equatable { + public var index: MessageHistoryAnchorIndex + public var quote: String? + + public init(index: MessageHistoryAnchorIndex, quote: String?) { + self.index = index + self.quote = quote + } +} + +public struct MessageHistoryInitialSearchSubject: Equatable { + public var location: ChatHistoryInitialSearchLocation + public var quote: String? + + public init(location: ChatHistoryInitialSearchLocation, quote: String?) { + self.location = location + self.quote = quote + } +} + public enum ChatHistoryLocation: Equatable { case Initial(count: Int) - case InitialSearch(location: ChatHistoryInitialSearchLocation, count: Int, highlight: Bool) + case InitialSearch(subject: MessageHistoryInitialSearchSubject, count: Int, highlight: Bool) case Navigation(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, highlight: Bool) - case Scroll(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool) + case Scroll(subject: MessageHistoryScrollToSubject, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool) } public struct ChatHistoryLocationInput: Equatable { diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 11cd6c8fca7..f1bb36cc4ef 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -72,6 +72,7 @@ public enum ContactMultiselectionControllerMode { } public enum ContactListFilter { + case excludeWithoutPhoneNumbers case excludeSelf case exclude([EnginePeer.Id]) case disable([EnginePeer.Id]) diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 947dbe23b36..d806ea0bf16 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -59,48 +59,6 @@ public final class PeerSelectionControllerParams { public let selectForumThreads: Bool public let hasCreation: Bool - /*public convenience init( - context: AccountContext, - updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, - filter: ChatListNodePeersFilter = [.onlyWriteable], - requestPeerType: [ReplyMarkupButtonRequestPeerType]? = nil, - forumPeerId: EnginePeer.Id? = nil, - hasFilters: Bool = false, - hasChatListSelector: Bool = true, - hasContactSelector: Bool = true, - hasGlobalSearch: Bool = true, - title: String? = nil, - attemptSelection: ((EnginePeer, Int64?) -> Void)? = nil, - createNewGroup: (() -> Void)? = nil, - pretendPresentedInModal: Bool = false, - multipleSelection: Bool = false, - forwardedMessageIds: [EngineMessage.Id] = [], - hasTypeHeaders: Bool = false, - selectForumThreads: Bool = false, - hasCreation: Bool = false - ) { - self.init( - context: .account(context), - updatedPresentationData: updatedPresentationData, - filter: filter, - requestPeerType: requestPeerType, - forumPeerId: forumPeerId, - hasFilters: hasFilters, - hasChatListSelector: hasChatListSelector, - hasContactSelector: hasContactSelector, - hasGlobalSearch: hasGlobalSearch, - title: title, - attemptSelection: attemptSelection, - createNewGroup: createNewGroup, - pretendPresentedInModal: pretendPresentedInModal, - multipleSelection: multipleSelection, - forwardedMessageIds: forwardedMessageIds, - hasTypeHeaders: hasTypeHeaders, - selectForumThreads: selectForumThreads, - hasCreation: hasCreation - ) - }*/ - public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, diff --git a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift index 6734d0d8dbb..cb7ce1554a7 100644 --- a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift +++ b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift @@ -100,7 +100,7 @@ private final class ContentNode: ASDisplayNode { self.updateImage(image: image, size: size, spacing: spacing) let disposable = (signal - |> deliverOnMainQueue).start(next: { [weak self] imageVersions in + |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in guard let strongSelf = self else { return } @@ -109,11 +109,11 @@ private final class ContentNode: ASDisplayNode { strongSelf.updateImage(image: image, size: size, spacing: spacing) } }) - self.disposable = disposable.strict() + self.disposable = disposable } else { let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id) + drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor) })! self.updateImage(image: image, size: size, spacing: spacing) } @@ -333,7 +333,7 @@ public final class AnimatedAvatarSetView: UIView { self.updateImage(image: image, size: size, spacing: spacing) let disposable = (signal - |> deliverOnMainQueue).start(next: { [weak self] imageVersions in + |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in guard let strongSelf = self else { return } @@ -342,11 +342,11 @@ public final class AnimatedAvatarSetView: UIView { strongSelf.updateImage(image: image, size: size, spacing: spacing) } }) - self.disposable = disposable.strict() + self.disposable = disposable } else { let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id) + drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor) })! self.updateImage(image: image, size: size, spacing: spacing) } diff --git a/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift b/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift index 1e4d6911beb..ae9341d8340 100644 --- a/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift +++ b/submodules/AnimatedCountLabelNode/Sources/AnimatedCountLabelNode.swift @@ -375,7 +375,7 @@ public class AnimatedCountLabelView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, segments initialSegments: [Segment], transition: ContainedViewLayoutTransition) -> Layout { + public func update(size: CGSize, segments initialSegments: [Segment], reducedLetterSpacing: Bool = false, transition: ContainedViewLayoutTransition) -> Layout { var segmentLayouts: [ResolvedSegment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:] let wasEmpty = self.resolvedSegments.isEmpty for (segmentKey, segmentAndTextNode) in self.resolvedSegments { @@ -393,7 +393,15 @@ public class AnimatedCountLabelView: UIView { } let attributes = string.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: 1)) - var remainingValue = value + for character in string.string { + if let _ = Int(String(character)) { + segments.append(.number(id: 1000 + segments.count, value: value, string: NSAttributedString(string: String(character), attributes: attributes))) + } else { + segments.append(.text(id: 1000 + segments.count, string: NSAttributedString(string: String(character), attributes: attributes))) + } + } + + /*var remainingValue = value let insertPosition = segments.count @@ -405,7 +413,7 @@ public class AnimatedCountLabelView: UIView { if remainingValue == 0 { break } - } + }*/ case let .text(id, string): segments.append(.text(id: id, string: string)) } @@ -489,7 +497,11 @@ public class AnimatedCountLabelView: UIView { } else if textNode.frame != textFrame { transition.updateFrameAdditive(node: textNode, frame: textFrame) } - currentOffset.x += effectiveSegmentWidth + if reducedLetterSpacing { + currentOffset.x += effectiveSegmentWidth * 0.9 + } else { + currentOffset.x += effectiveSegmentWidth + } if let (_, currentTextNode) = self.resolvedSegments[segment.key] { if currentTextNode !== textNode { currentTextNode.removeFromSupernode() diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 94976a0026b..b8289bd6d97 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -368,12 +368,12 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke } self.disposable.set((source.directDataPath(attemptSynchronously: false) |> filter { $0 != nil } - |> deliverOnMainQueue).start(next: { path in + |> deliverOnMainQueue).startStrict(next: { path in f(path!) })) case .cached: self.disposable.set((source.cachedDataPath(width: width, height: height) - |> deliverOnMainQueue).start(next: { [weak self] path, complete in + |> deliverOnMainQueue).startStrict(next: { [weak self] path, complete in guard let strongSelf = self else { return } diff --git a/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift index b597911dfa9..49eac5f5b5c 100644 --- a/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift @@ -106,7 +106,7 @@ public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode self.sourceDisposable = (source.directDataPath(attemptSynchronously: false) |> filter { $0 != nil } |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] path in + |> deliverOnMainQueue).startStrict(next: { [weak self] path in guard let strongSelf = self, let path = path else { return } diff --git a/submodules/AppLock/Sources/AppLock.swift b/submodules/AppLock/Sources/AppLock.swift index 91436d62566..d640244da43 100644 --- a/submodules/AppLock/Sources/AppLock.swift +++ b/submodules/AppLock/Sources/AppLock.swift @@ -92,6 +92,9 @@ public final class AppLockContextImpl: AppLockContext { private let rootPath: String private let syncQueue = Queue() + private var disposable: Disposable? + private var autolockTimeoutDisposable: Disposable? + private let applicationBindings: TelegramApplicationBindings private let accountManager: AccountManager private let presentationDataSignal: Signal @@ -172,14 +175,14 @@ public final class AppLockContextImpl: AppLockContext { strongSelf.hiddenAccountsAccessChallengeData = value }) - let _ = (combineLatest(queue: .mainQueue(), + self.disposable = (combineLatest(queue: .mainQueue(), accountManager.accessChallengeData(), accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationPasscodeSettings])), presentationDataSignal, applicationBindings.applicationIsActive, self.currentState.get() ) - |> deliverOnMainQueue).start(next: { [weak self] accessChallengeData, sharedData, presentationData, appInForeground, state in + |> deliverOnMainQueue).startStrict(next: { [weak self] accessChallengeData, sharedData, presentationData, appInForeground, state in guard let strongSelf = self else { return } @@ -363,8 +366,8 @@ public final class AppLockContextImpl: AppLockContext { strongSelf.accountManager.hiddenAccountManager.unlockedAccountRecordIdPromise.set(nil) }) - let _ = (self.autolockTimeout.get() - |> deliverOnMainQueue).start(next: { [weak self] autolockTimeout in + self.autolockTimeoutDisposable = (self.autolockTimeout.get() + |> deliverOnMainQueue).startStrict(next: { [weak self] autolockTimeout in self?.updateLockState { state in var state = state state.autolockTimeout = autolockTimeout @@ -385,6 +388,16 @@ public final class AppLockContextImpl: AppLockContext { } } + deinit { + // MARK: Nicegram DB Changes + self.hiddenAccountsAccessChallengeDataDisposable?.dispose() + self.applicationInForegroundDisposable?.dispose() + // + + self.disposable?.dispose() + self.autolockTimeoutDisposable?.dispose() + } + private func updateTimestampRenewTimer(shouldRun: Bool) { if shouldRun { if self.timestampRenewTimer == nil { @@ -496,9 +509,4 @@ public final class AppLockContextImpl: AppLockContext { return state } } - // MARK: Nicegram DB Changes - deinit { - self.hiddenAccountsAccessChallengeDataDisposable?.dispose() - self.applicationInForegroundDisposable?.dispose() - } } diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm index bba12025a8a..1b8754c8ee9 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -16,17 +16,47 @@ @implementation ASCustomTextContainer +- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage { + self = [super initWithSize:size]; + if (self != nil) { + } + return self; +} + +- (BOOL)isSimpleRectangularTextContainer { + return false; +} + - (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect { CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect]; -/*#if DEBUG - if (result.origin.y < 10.0f) { - result.size.width -= 21.0f; - if (result.size.width < 0.0f) { - result.size.width = 0.0f; + NSTextStorage *textStorage = self.layoutManager.textStorage; + if (textStorage != nil) { + NSString *string = textStorage.string; + int index = (int)characterIndex; + if (index >= 0 && index < string.length) { + NSDictionary *attributes = [textStorage attributesAtIndex:index effectiveRange:nil]; + NSObject *blockQuote = attributes[@"Attribute__Blockquote"]; + if (blockQuote != nil) { + bool isFirstLine = false; + if (index == 0) { + isFirstLine = true; + } else { + NSDictionary *previousAttributes = [textStorage attributesAtIndex:index - 1 effectiveRange:nil]; + NSObject *previousBlockQuote = previousAttributes[@"Attribute__Blockquote"]; + if (previousBlockQuote == nil) { + isFirstLine = true; + } else if (![blockQuote isEqual:previousBlockQuote]) { + isFirstLine = true; + } + } + + if (isFirstLine) { + result.size.width -= 100.0f; + } + } } } -#endif*/ return result; } @@ -139,8 +169,7 @@ + (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage components.layoutManager = layoutManager; [components.textStorage addLayoutManager:components.layoutManager]; - components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize]; - //components.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(textContainerSize.width - 60.0, 0.0, 60.0, 40.0)]]; + components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize textStorage:textStorage]; components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. [components.layoutManager addTextContainer:components.textContainer]; diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.h b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h index 5decde42f7d..13134b763bd 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitContext.h +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h @@ -52,6 +52,8 @@ AS_SUBCLASSING_RESTRICTED @interface ASCustomTextContainer : NSTextContainer +- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage; + @end #endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm index 8e9c276fef2..aa9dada4fa6 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm @@ -58,7 +58,7 @@ - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString [_textStorage setAttributedString:attributedString]; } - _textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize]; + _textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize textStorage:nil]; // We want the text laid out up to the very edges of the container. _textContainer.lineFragmentPadding = 0; _textContainer.lineBreakMode = lineBreakMode; diff --git a/submodules/AttachmentTextInputPanelNode/BUILD b/submodules/AttachmentTextInputPanelNode/BUILD index f82aeb9a52d..59571663187 100644 --- a/submodules/AttachmentTextInputPanelNode/BUILD +++ b/submodules/AttachmentTextInputPanelNode/BUILD @@ -34,6 +34,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index dc68e2f27fb..5af2ccc8a77 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -23,6 +23,7 @@ import LottieAnimationComponent import AnimationCache import MultiAnimationRenderer import TextNodeWithEntities +import ChatInputTextNode private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) private let minInputFontSize: CGFloat = 5.0 @@ -112,7 +113,7 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground } } -private class CaptionEditableTextNode: EditableTextNode { +private class CaptionEditableTextNode: ChatInputTextNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let previousAlpha = self.alpha self.alpha = 1.0 @@ -190,7 +191,7 @@ final class CustomEmojiContainerView: UIView { } } -public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate { +public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { private let context: AccountContext private let isCaption: Bool @@ -202,7 +203,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var textPlaceholderNode: ImmediateTextNode private let textInputContainerBackgroundNode: ASImageNode private let textInputContainer: ASDisplayNode - public var textInputNode: EditableTextNode? + public var textInputNode: ChatInputTextNode? private var dustNode: InvisibleInkDustNode? private var customEmojiContainerView: CustomEmojiContainerView? private var oneLineNode: TextNodeWithEntities @@ -249,7 +250,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS var storedInputLanguage: String? var effectiveInputLanguage: String? { if let textInputNode = textInputNode, textInputNode.isFirstResponder() { - return textInputNode.textInputMode.primaryLanguage + return textInputNode.textInputMode?.primaryLanguage } else { return self.storedInputLanguage } @@ -308,7 +309,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor) - self.editableTextNodeDidUpdateText(textInputNode) + self.chatInputTextNodeDidUpdateText() } } } @@ -442,7 +443,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return .complete() } } - |> deliverOnMainQueue).start(next: { [weak self] maxCaptionLength in + |> deliverOnMainQueue).startStandalone(next: { [weak self] maxCaptionLength in self?.maxCaptionLength = maxCaptionLength }) } @@ -516,7 +517,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS paragraphStyle.maximumLineHeight = 20.0 paragraphStyle.minimumLineHeight = 20.0 - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: paragraphStyle] textInputNode.clipsToBounds = false textInputNode.textView.clipsToBounds = false textInputNode.delegate = self @@ -532,7 +533,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.textView.inputAssistantItem.trailingBarButtonGroups = [] if let presentationInterfaceState = self.presentationInterfaceState { - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState) } @@ -541,6 +542,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) textInputNode.view.layoutIfNeeded() + textInputNode.textView.updateLayout(size: textInputNode.bounds.size) self.updateSpoiler() } @@ -587,8 +589,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - let measuredHeight = textInputNode.measure(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) - let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight.height)) + let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: 0.0) + let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight)) let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) @@ -674,7 +676,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.attributedText = updatedText textInputNode.selectedRange = selectedRange } - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor] self.updateSpoiler() } @@ -960,11 +962,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } private var skipUpdate = false - @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + public func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoiler() @@ -973,7 +975,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.skipUpdate = true self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) - self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode.primaryLanguage }) + self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage }) if self.isCaption, let presentationInterfaceState = self.presentationInterfaceState { self.presentationInterfaceState = presentationInterfaceState.updatedInterfaceState({ return $0.withUpdatedComposeInputState(inputTextState) @@ -988,6 +990,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } + @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidUpdateText() + } + private func updateSpoiler() { guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { return @@ -1131,7 +1137,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.textView.isScrollEnabled = false - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) @@ -1345,13 +1351,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + public func chatInputTextNodeShouldReturn() -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { self.sendButtonPressed() } return false } + @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldReturn() + } + private func applyUpdateSendButtonIcon() { if let interfaceState = self.presentationInterfaceState { let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil @@ -1371,7 +1381,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - @objc public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + public func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) { if !dueToEditing && !self.updatingInputState { let inputTextState = self.inputTextState self.skipUpdate = true @@ -1385,13 +1395,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoilersRevealed() } } - @objc public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + @objc public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing) + } + + public func chatInputTextNodeDidBeginEditing() { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in return (.text, state.keyboardButtonsMessage?.id) }) @@ -1404,8 +1418,15 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage + @objc public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidBeginEditing() + } + + public func chatInputTextNodeDidFinishEditing() { + guard let editableTextNode = self.textInputNode else { + return + } + self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage self.inputMenu.deactivate() self.focusUpdated?(false) @@ -1415,6 +1436,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } + public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidFinishEditing() + } + public func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { if action == makeSelectorFromString("_accessibilitySpeak:") { if case .format = self.inputMenu.state { @@ -1461,8 +1486,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return nil } - @available(iOS 16.0, *) - public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + @available(iOS 13.0, *) + public func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + guard let editableTextNode = self.textInputNode else { + return UIMenu(children: suggestedActions) + } + var actions = suggestedActions if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 { @@ -1520,6 +1549,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return UIMenu(children: actions) } + @available(iOS 16.0, *) + public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + return self.chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions) + } + private var currentSpeechHolder: SpeechSynthesizerHolder? @objc func _accessibilitySpeak(_ sender: Any) { var text = "" @@ -1612,7 +1646,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.updateSpoilersRevealed(animated: animated) } - @objc public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + public func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let editableTextNode = self.textInputNode else { + return true + } + var cleanText = text let removeSequences: [String] = ["\u{202d}", "\u{202c}"] for sequence in removeSequences { @@ -1645,7 +1683,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return true } - @objc public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) + } + + public func chatInputTextNodeShouldCopy() -> Bool { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count))) return (current, inputMode) @@ -1653,7 +1695,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return false } - @objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldCopy() + } + + public func chatInputTextNodeShouldPaste() -> Bool { let pasteboard = UIPasteboard.general var attributedString: NSAttributedString? @@ -1678,6 +1724,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return true } + public func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool { + return true + } + + @objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldPaste() + } + + public func chatInputTextNodeBackspaceWhileEmpty() { + } + @objc func sendButtonPressed() { let inputTextMaxLength: Int32? if let maxCaptionLength = self.maxCaptionLength { diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index e9c2f7b890d..3770639b734 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -37,6 +37,8 @@ swift_library( "//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/TextFormat:TextFormat", + "//submodules/TelegramUI/Components/LegacyMessageInputPanel", + "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index eea80631531..0fe0add1fa6 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -11,6 +11,8 @@ import AccountContext import TelegramStringFormatting import UIKitRuntimeUtils import MediaResources +import LegacyMessageInputPanel +import LegacyMessageInputPanelInputView import AttachmentTextInputPanelNode public enum AttachmentButtonType: Equatable { @@ -231,19 +233,19 @@ public class AttachmentController: ViewController { didSet { if let mediaPickerContext = self.mediaPickerContext { self.captionDisposable.set((mediaPickerContext.caption - |> deliverOnMainQueue).start(next: { [weak self] caption in + |> deliverOnMainQueue).startStrict(next: { [weak self] caption in if let strongSelf = self { strongSelf.panel.updateCaption(caption ?? NSAttributedString()) } })) self.mediaSelectionCountDisposable.set((mediaPickerContext.selectionCount - |> deliverOnMainQueue).start(next: { [weak self] count in + |> deliverOnMainQueue).startStrict(next: { [weak self] count in if let strongSelf = self { strongSelf.updateSelectionCount(count) } })) self.loadingProgressDisposable.set((mediaPickerContext.loadingProgress - |> deliverOnMainQueue).start(next: { [weak self] progress in + |> deliverOnMainQueue).startStrict(next: { [weak self] progress in if let strongSelf = self { strongSelf.panel.updateLoadingProgress(progress) if let layout = strongSelf.validLayout { @@ -252,13 +254,13 @@ public class AttachmentController: ViewController { } })) self.mainButtonStateDisposable.set((mediaPickerContext.mainButtonState - |> deliverOnMainQueue).start(next: { [weak self] mainButtonState in + |> deliverOnMainQueue).startStrict(next: { [weak self] mainButtonState in if let strongSelf = self { let _ = (strongSelf.panel.animatingTransitionPromise.get() |> filter { value in return !value } - |> take(1)).start(next: { [weak self] _ in + |> take(1)).startStandalone(next: { [weak self] _ in if let strongSelf = self { strongSelf.panel.updateMainButtonState(mainButtonState) if let layout = strongSelf.validLayout { @@ -433,6 +435,8 @@ public class AttachmentController: ViewController { deinit { self.captionDisposable.dispose() self.mediaSelectionCountDisposable.dispose() + self.loadingProgressDisposable.dispose() + self.mainButtonStateDisposable.dispose() } private var inputContainerHeight: CGFloat? @@ -591,7 +595,7 @@ public class AttachmentController: ViewController { $0 } |> take(1) - |> deliverOnMainQueue).start(next: { [weak self, weak snapshotView] _ in + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak snapshotView] _ in guard let strongSelf = self, let layout = strongSelf.validLayout else { return } @@ -1039,7 +1043,7 @@ public class AttachmentController: ViewController { let disposableSet = DisposableSet() let _ = (context.engine.messages.attachMenuBots() |> take(1) - |> deliverOnMainQueue).start(next: { bots in + |> deliverOnMainQueue).startStandalone(next: { bots in for bot in bots { for (name, file) in bot.icons { if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 741e1808b2a..60064a039dd 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -19,6 +19,8 @@ import MediaResources import MultilineTextComponent import ShimmerEffect import TextFormat +import LegacyMessageInputPanel +import LegacyMessageInputPanelInputView private let buttonSize = CGSize(width: 88.0, height: 49.0) private let smallButtonWidth: CGFloat = 69.0 @@ -85,7 +87,7 @@ private final class IconComponent: Component { self.disposable = (svgIconImageFile(account: component.account, fileReference: fileReference) |> runOn(Queue.concurrentDefaultQueue()) - |> deliverOnMainQueue).start(next: { [weak self] transform in + |> deliverOnMainQueue).startStrict(next: { [weak self] transform in let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: availableSize, boundingSize: availableSize, intrinsicInsets: UIEdgeInsets()) let drawingContext = transform(arguments) let image = drawingContext?.generateImage()?.withRenderingMode(.alwaysTemplate) @@ -375,54 +377,6 @@ private final class LoadingProgressNode: ASDisplayNode { } } -public struct AttachmentMainButtonState { - public enum Background { - case color(UIColor) - case premium - } - - public enum Progress: Equatable { - case none - case side - case center - } - - public enum Font: Equatable { - case regular - case bold - } - - public let text: String? - public let font: Font - public let background: Background - public let textColor: UIColor - public let isVisible: Bool - public let progress: Progress - public let isEnabled: Bool - - public init( - text: String?, - font: Font, - background: Background, - textColor: UIColor, - isVisible: Bool, - progress: Progress, - isEnabled: Bool - ) { - self.text = text - self.font = font - self.background = background - self.textColor = textColor - self.isVisible = isVisible - self.progress = progress - self.isEnabled = isEnabled - } - - static var initial: AttachmentMainButtonState { - return AttachmentMainButtonState(text: nil, font: .bold, background: .color(.clear), textColor: .clear, isVisible: false, progress: .none, isEnabled: false) - } -} - private final class MainButtonNode: HighlightTrackingButtonNode { private var state: AttachmentMainButtonState private var size: CGSize? @@ -783,7 +737,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { self.makeEntityInputView = makeEntityInputView - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.containerNode = ASDisplayNode() self.containerNode.clipsToBounds = true @@ -827,6 +781,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState($0.forwardOptionsState) }) }) } }, presentForwardOptions: { _ in + }, presentReplyOptions: { _ in + }, presentLinkOptions: { _ in }, shareSelectedMessages: { }, updateTextInputStateAndMode: { [weak self] f in if let strongSelf = self { @@ -968,7 +924,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { } let _ = (strongSelf.context.account.viewTracker.peerView(peerId) |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] peerView in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { return } @@ -983,7 +939,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { sendWhenOnlineAvailable = false } - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, completion: { + let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, completion: { }, sendMessage: { [weak textInputPanelNode] mode in switch mode { case .generic: @@ -1032,7 +988,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, statuses: nil) self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) - |> deliverOnMainQueue).start(next: { [weak self] presentationData in + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.presentationData = presentationData @@ -1168,7 +1124,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { self.iconDisposables[file.fileId] = accountFullSizeData.start() } } else { - self.iconDisposables[file.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .attachBot(peer: peer, media: file)).start() + self.iconDisposables[file.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .attachBot(peer: peer, media: file)).startStrict() } } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index a3db1b478d6..5aa943345c2 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -237,7 +237,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF return } - let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.strings.Common_Paste, accessibilityLabel: strongSelf.strings.Common_Paste), action: { [weak self] in + let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.strings.Common_Paste, accessibilityLabel: strongSelf.strings.Common_Paste), action: { [weak self] in self?.updateCode(code) })]) @@ -339,7 +339,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF let timeout = min(timeout, 5) #endif self.currentTimeoutTime = timeout - let disposable = ((Signal.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).start(next: { [weak self] _ in + let disposable = ((Signal.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).startStrict(next: { [weak self] _ in if let strongSelf = self { if let currentTimeoutTime = strongSelf.currentTimeoutTime, currentTimeoutTime > 0 { strongSelf.currentTimeoutTime = currentTimeoutTime - 1 @@ -363,7 +363,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF }) self.countdownDisposable.set(disposable) } else if case let .email(_, _, _, pendingDate, _, _) = codeType, let pendingDate { - let disposable = ((Signal.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).start(next: { [weak self] _ in + let disposable = ((Signal.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).startStrict(next: { [weak self] _ in if let strongSelf = self { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let interval = pendingDate - currentTime diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 81d5d3c5875..3a38f602b1f 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -82,7 +82,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } } |> distinctUntilChanged - |> deliverOnMainQueue).start(next: { [weak self] state in + |> deliverOnMainQueue).startStrict(next: { [weak self] state in self?.updateState(state: state) }).strict() } @@ -124,7 +124,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: isTestingEnvironment, masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: isTestingEnvironment, masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() } } } @@ -152,9 +152,9 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if !strongSelf.otherAccountPhoneNumbers.1.isEmpty { let _ = (strongSelf.sharedContext.accountManager.transaction { transaction -> Void in transaction.removeAuth() - }).start() + }).startStandalone() } else { - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .empty)).startStandalone() } }) if let splashController = splashController { @@ -175,14 +175,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail |> take(1) |> timeout(2.0, queue: .mainQueue(), alternate: .single(nil)) let _ = (authorizationPushConfiguration - |> deliverOnMainQueue).start(next: { [weak self] authorizationPushConfiguration in + |> deliverOnMainQueue).startStandalone(next: { [weak self] authorizationPushConfiguration in if let strongSelf = self { strongSelf.actionDisposable.set((sendAuthorizationCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, phoneNumber: number, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash, pushNotificationConfiguration: authorizationPushConfiguration, firebaseSecretStream: strongSelf.sharedContext.firebaseSecretStream, syncContacts: syncContacts, forcedPasswordSetupNotice: { value in guard let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) else { return nil } return (ApplicationSpecificNotice.forcedPasswordSetupKey(), entry) - }) |> deliverOnMainQueue).start(next: { [weak self] result in + }) |> deliverOnMainQueue).startStrict(next: { [weak self] result in if let strongSelf = self { switch result { case let .sentCode(account): @@ -312,12 +312,12 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) controller.retryResetEmail = { [weak self] in if let self { self.actionDisposable.set( - resetLoginEmail(account: self.account, phoneNumber: number, phoneCodeHash: phoneCodeHash).start() + resetLoginEmail(account: self.account, phoneNumber: number, phoneCodeHash: phoneCodeHash).startStandalone() ) } } @@ -328,7 +328,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let _ = resetPendingDate { self.actionDisposable.set( (resetLoginEmail(account: self.account, phoneNumber: number, phoneCodeHash: phoneCodeHash) - |> deliverOnMainQueue).start(error: { [weak self] error in + |> deliverOnMainQueue).startStrict(error: { [weak self] error in if let self, case .alreadyInProgress = error { let formattedNumber = formatPhoneNumber(number) let title = NSAttributedString(string: self.presentationData.strings.Login_Email_PremiumRequiredTitle, font: Font.semibold(self.presentationData.listsFontSize.baseDisplaySize), textColor: self.presentationData.theme.actionSheet.primaryTextColor) @@ -342,7 +342,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } else if let resetAvailablePeriod { if resetAvailablePeriod == 0 { self.actionDisposable.set( - resetLoginEmail(account: self.account, phoneNumber: number, phoneCodeHash: phoneCodeHash).start() + resetLoginEmail(account: self.account, phoneNumber: number, phoneCodeHash: phoneCodeHash).startStrict() ) } else { let pattern = pattern.replacingOccurrences(of: "*", with: "#") @@ -362,7 +362,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } self.actionDisposable.set( (resetLoginEmail(account: self.account, phoneNumber: number, phoneCodeHash: phoneCodeHash) - |> deliverOnMainQueue).start(error: { [weak self] error in + |> deliverOnMainQueue).startStrict(error: { [weak self] error in Queue.mainQueue().async { guard let self, let controller = controller else { return @@ -378,7 +378,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail case .codeExpired: text = self.presentationData.strings.Login_CodeExpired let account = self.account - let _ = TelegramEngineUnauthorized(account: self.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: self.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() } controller.presentInGlobalOverlay(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})])) @@ -405,7 +405,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if case let .email(_, _, _, _, _, setup) = type, setup, case let .emailVerification(emailCode) = authorizationCode { strongSelf.actionDisposable.set(((verifyLoginEmailSetup(account: strongSelf.account, code: emailCode)) - |> deliverOnMainQueue).start(error: { error in + |> deliverOnMainQueue).startStrict(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false @@ -427,7 +427,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() case .timeout: text = strongSelf.presentationData.strings.Login_NetworkError case .invalidEmailToken: @@ -452,7 +452,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } return (ApplicationSpecificNotice.forcedPasswordSetupKey(), entry) }) - |> deliverOnMainQueue).start(next: { result in + |> deliverOnMainQueue).startStrict(next: { result in guard let strongSelf = self else { return } @@ -474,7 +474,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail guard let strongSelf = self else { return } - let _ = beginSignUp(account: strongSelf.account, data: data).start() + let _ = beginSignUp(account: strongSelf.account, data: data).startStandalone() }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Login_TermsOfServiceDecline, action: { dismissImpl?() guard let strongSelf = self else { @@ -487,7 +487,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail return } let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() })]), on: .root, blockInteraction: false, completion: {}) }) ], actionLayout: .vertical, dismissOnOutsideTap: true) @@ -508,7 +508,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } presentAlertImpl() } else { - let _ = beginSignUp(account: strongSelf.account, data: data).start() + let _ = beginSignUp(account: strongSelf.account, data: data).startStandalone() } case .loggedIn: controller?.animateSuccess() @@ -535,7 +535,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() case .invalidEmailToken: text = strongSelf.presentationData.strings.Login_InvalidEmailTokenError case .invalidEmailAddress: @@ -582,7 +582,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } else { controller?.inProgress = true strongSelf.actionDisposable.set((resendAuthorizationCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash, firebaseSecretStream: strongSelf.sharedContext.firebaseSecretStream) - |> deliverOnMainQueue).start(next: { result in + |> deliverOnMainQueue).startStrict(next: { result in controller?.inProgress = false }, error: { error in if let strongSelf = self, let controller = controller { @@ -622,7 +622,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail controller.reset = { [weak self] in if let strongSelf = self { let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() } } controller.signInWithApple = { [weak self] in @@ -674,7 +674,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) } controller.proceedWithEmail = { [weak self, weak controller] email in @@ -687,7 +687,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail strongSelf.currentEmail = email strongSelf.actionDisposable.set((sendLoginEmailCode(account: strongSelf.account, email: email) - |> deliverOnMainQueue).start(error: { error in + |> deliverOnMainQueue).startStrict(error: { error in if let strongSelf = self, let controller = controller { controller.inProgress = false @@ -747,7 +747,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if self.signInWithAppleSetup { self.actionDisposable.set((verifyLoginEmailSetup(account: self.account, code: .appleToken(token)) - |> deliverOnMainQueue).start(error: { [weak self, weak lastController] error in + |> deliverOnMainQueue).startStrict(error: { [weak self, weak lastController] error in if let strongSelf = self, let lastController = lastController { let text: String switch error { @@ -774,13 +774,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail return nil } return (ApplicationSpecificNotice.forcedPasswordSetupKey(), entry) - }).start(next: { [weak self] result in + }).startStrict(next: { [weak self] result in guard let strongSelf = self else { return } switch result { case let .signUp(data): - let _ = beginSignUp(account: strongSelf.account, data: data).start() + let _ = beginSignUp(account: strongSelf.account, data: data).startStandalone() case .loggedIn: break } @@ -798,7 +798,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() case .invalidEmailToken: text = strongSelf.presentationData.strings.Login_InvalidEmailTokenError case .invalidEmailAddress: @@ -839,13 +839,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) controller.loginWithPassword = { [weak self, weak controller] password in if let strongSelf = self { controller?.inProgress = true - strongSelf.actionDisposable.set((authorizeWithPassword(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, password: password, syncContacts: syncContacts) |> deliverOnMainQueue).start(error: { error in + strongSelf.actionDisposable.set((authorizeWithPassword(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, password: password, syncContacts: syncContacts) |> deliverOnMainQueue).startStrict(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false @@ -872,18 +872,18 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((TelegramEngineUnauthorized(account: strongSelf.account).auth.requestTwoStepVerificationPasswordRecoveryCode() - |> deliverOnMainQueue).start(next: { pattern in + |> deliverOnMainQueue).startStrict(next: { pattern in if let strongSelf = self, let strongController = controller { strongController.inProgress = false let _ = (TelegramEngineUnauthorized(account: strongSelf.account).auth.state() |> take(1) - |> deliverOnMainQueue).start(next: { state in + |> deliverOnMainQueue).startStandalone(next: { state in guard let strongSelf = self else { return } if case let .unauthorized(state) = state, case let .passwordEntry(hint, number, code, _, syncContacts) = state.contents { - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern, syncContacts: syncContacts))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern, syncContacts: syncContacts))).startStandalone() } }) } @@ -907,7 +907,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) - |> deliverOnMainQueue).start(next: { + |> deliverOnMainQueue).startStrict(next: { if let strongController = controller { strongController.inProgress = false } @@ -954,12 +954,12 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let _ = (TelegramEngineUnauthorized(account: strongSelf.account).auth.state() |> take(1) - |> deliverOnMainQueue).start(next: { state in + |> deliverOnMainQueue).startStandalone(next: { state in guard let strongSelf = self else { return } if case let .unauthorized(state) = state, case let .passwordRecovery(hint, number, code, _, syncContacts) = state.contents { - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: true, syncContacts: syncContacts))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: true, syncContacts: syncContacts))).startStandalone() } }) } @@ -984,7 +984,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { @@ -994,7 +994,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) - |> deliverOnMainQueue).start(next: { + |> deliverOnMainQueue).startStrict(next: { if let strongController = controller { strongController.inProgress = false } @@ -1018,7 +1018,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail controller.logout = { [weak self] in if let strongSelf = self { let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() } } } @@ -1044,7 +1044,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).start() + let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }, displayCancel: displayCancel) controller.signUpWithName = { [weak self, weak controller] firstName, lastName, avatarData, avatarAsset, avatarAdjustments in if let strongSelf = self { @@ -1112,7 +1112,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } return (ApplicationSpecificNotice.forcedPasswordSetupKey(), entry) }) - |> deliverOnMainQueue).start(error: { error in + |> deliverOnMainQueue).startStrict(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index 26cb4321d88..c6560db3d8a 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -444,7 +444,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { if let account = account { self.tokenEventsDisposable.set((account.updateLoginTokenEvents - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in self?.refreshQrToken() })) } @@ -672,19 +672,19 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } self.exportTokenDisposable.set((tokenSignal - |> deliverOnMainQueue).start(next: { [weak self] result in + |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let strongSelf = self else { return } switch result { case let .displayToken(token): var tokenString = token.value.base64EncodedString() - print("export token \(tokenString)") + //print("export token \(tokenString)") tokenString = tokenString.replacingOccurrences(of: "+", with: "-") tokenString = tokenString.replacingOccurrences(of: "/", with: "_") let urlString = "tg://login?token=\(tokenString)" let _ = (qrCode(string: urlString, color: .black, backgroundColor: .white, icon: .none) - |> deliverOnMainQueue).start(next: { _, generate in + |> deliverOnMainQueue).startStrict(next: { _, generate in guard let strongSelf = self else { return } @@ -698,7 +698,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { let timestamp = Int32(Date().timeIntervalSince1970) let timeout = max(5, token.validUntil - timestamp) strongSelf.exportTokenDisposable.set((Signal.complete() - |> delay(Double(timeout), queue: .mainQueue())).start(completed: { + |> delay(Double(timeout), queue: .mainQueue())).startStrict(completed: { guard let strongSelf = self else { return } @@ -709,7 +709,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { strongSelf.account = account strongSelf.accountUpdated?(account) strongSelf.tokenEventsDisposable.set((account.updateLoginTokenEvents - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).startStrict(next: { _ in self?.refreshQrToken() })) strongSelf.refreshQrToken() diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift index e7d1b1fa836..1bb8bf05215 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift @@ -9,6 +9,7 @@ import LegacyComponents import ProgressNavigationButtonNode import ImageCompression import LegacyMediaPickerUI +import Postbox final class AuthorizationSequenceSignUpController: ViewController { private var controllerNode: AuthorizationSequenceSignUpControllerNode { @@ -179,7 +180,10 @@ final class AuthorizationSequenceSignUpController: ViewController { if let name = name { self.signUpWithName?(name.0, name.1, self.controllerNode.currentPhoto.flatMap({ image in - return compressImageToJPEG(image, quality: 0.7) + let tempFile = TempBox.shared.tempFile(fileName: "file") + let result = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) + TempBox.shared.dispose(tempFile) + return result }), self.avatarAsset, self.avatarAdjustments) } } diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 545b4533693..73b3fdde510 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -66,7 +66,7 @@ private class AvatarNodeParameters: NSObject { } } -private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { +private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { let colorIndex: Int if let explicitColorIndex = explicitColorIndex { colorIndex = explicitColorIndex @@ -113,7 +113,11 @@ private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, i colors = AvatarNode.grayscaleColors } } else { - colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count] + if let nameColor { + colors = AvatarNode.gradientColors[Int(nameColor.rawValue) % AvatarNode.gradientColors.count] + } else { + colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count] + } } return colors @@ -125,7 +129,7 @@ public enum AvatarNodeExplicitIcon { private enum AvatarNodeState: Equatable { case empty - case peerAvatar(EnginePeer.Id, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle) + case peerAvatar(EnginePeer.Id, PeerNameColor?, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle) case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?) } @@ -133,8 +137,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { switch (lhs, rhs) { case (.empty, .empty): return true - case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)): - return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle + case let (.peerAvatar(lhsPeerId, lhsPeerNameColor, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsPeerNameColor, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)): + return lhsPeerId == rhsPeerId && lhsPeerNameColor == rhsPeerNameColor && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)): return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon default: @@ -454,7 +458,7 @@ public final class AvatarNode: ASDisplayNode { } else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil { representation = peer?.smallProfileImage } - let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle) + let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle) if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme { self.state = updatedState self.overrideImage = overrideImage @@ -489,7 +493,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -498,7 +502,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme) + let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -621,7 +625,7 @@ public final class AvatarNode: ASDisplayNode { else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil || isAllowedChat(peer: peer?._asPeer(), contentSettings: genericContext.currentContentSettings.with { $0 }) { representation = peer?.smallProfileImage } - let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle) + let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle) if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme { self.state = updatedState self.overrideImage = overrideImage @@ -658,7 +662,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } // MARK: Nicegram changes else if let signal = nicegramAvatarImage(nicegramImage: nicegramImage) { @@ -674,7 +678,7 @@ public final class AvatarNode: ASDisplayNode { return next?.0 }) - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme), letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme), letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -683,7 +687,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme) + let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -720,9 +724,9 @@ public final class AvatarNode: ASDisplayNode { let parameters: AvatarNodeParameters if let icon = icon, case .phone = icon { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } else { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } self.displaySuspended = true diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 4cfef7a990f..b73efb4d1fb 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -156,7 +156,7 @@ public func peerAvatarCompleteImage(postbox: Postbox, network: Network, peer: En iconSignal = Signal { subscriber in let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), round: round, font: font, letters: displayLetters, peerId: peerId) + drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), round: round, font: font, letters: displayLetters, peerId: peerId, nameColor: peer.nameColor) if blurred { context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.5).cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) @@ -354,7 +354,7 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P } } -public func drawPeerAvatarLetters(context: CGContext, size: CGSize, round: Bool = true, font: UIFont, letters: [String], peerId: EnginePeer.Id) { +public func drawPeerAvatarLetters(context: CGContext, size: CGSize, round: Bool = true, font: UIFont, letters: [String], peerId: EnginePeer.Id, nameColor: PeerNameColor?) { if round { context.beginPath() context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: @@ -373,7 +373,11 @@ public func drawPeerAvatarLetters(context: CGContext, size: CGSize, round: Bool if colorIndex == -1 { colorsArray = AvatarNode.grayscaleColors.map(\.cgColor) as NSArray } else { - colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count].map(\.cgColor) as NSArray + var index = colorIndex % AvatarNode.gradientColors.count + if let nameColor { + index = Int(nameColor.rawValue) % AvatarNode.gradientColors.count + } + colorsArray = AvatarNode.gradientColors[index].map(\.cgColor) as NSArray } var locations: [CGFloat] = [1.0, 0.0] diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 5c3060be6ed..6b17989a2e8 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -25,7 +25,7 @@ public final class AvatarVideoNode: ASDisplayNode { private var emojiMarkup: TelegramMediaImage.EmojiMarkup? - private var fileDisposable: Disposable? + private var fileDisposable = MetaDisposable() private var animationFile: TelegramMediaFile? private var itemLayer: EmojiPagerContentComponent.View.ItemLayer? private var useAnimationNode = false @@ -55,7 +55,7 @@ public final class AvatarVideoNode: ASDisplayNode { } deinit { - self.fileDisposable?.dispose() + self.fileDisposable.dispose() self.stickerFetchedDisposable.dispose() self.playbackStartDisposable.dispose() } @@ -76,7 +76,7 @@ public final class AvatarVideoNode: ASDisplayNode { } if self.useAnimationNode { - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(animationFile), resource: chatMessageStickerResource(file: animationFile, small: false)).start()) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(animationFile), resource: chatMessageStickerResource(file: animationFile, small: false)).startStrict()) let animationNode = DefaultAnimatedStickerNodeImpl() animationNode.autoplay = false @@ -174,27 +174,27 @@ public final class AvatarVideoNode: ASDisplayNode { switch markup.content { case let .emoji(fileId): - self.fileDisposable = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) - |> deliverOnMainQueue).start(next: { [weak self] files in + self.fileDisposable.set((self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).startStrict(next: { [weak self] files in if let strongSelf = self, let file = files.values.first { strongSelf.animationFile = file strongSelf.setupAnimation() } - }).strict() + })) case let .sticker(packReference, fileId): - self.fileDisposable = (self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) + self.fileDisposable.set((self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) |> map { pack -> TelegramMediaFile? in if case let .result(_, items, _) = pack, let item = items.first(where: { $0.file.fileId.id == fileId }) { return item.file } return nil } - |> deliverOnMainQueue).start(next: { [weak self] file in + |> deliverOnMainQueue).startStrict(next: { [weak self] file in if let strongSelf = self, let file { strongSelf.animationFile = file strongSelf.setupAnimation() } - }).strict() + })) } } @@ -265,7 +265,7 @@ public final class AvatarVideoNode: ASDisplayNode { return playing } |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStrict(completed: { [weak self] in if let strongSelf = self { Queue.mainQueue().after(0.15) { strongSelf.videoNode?.isHidden = false diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 0b2e4afb3ae..d3a781b7e48 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -562,7 +562,7 @@ // } // // private func longPressMedia(_ media: InstantPageMedia) { -// let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in +// let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in // if let strongSelf = self, let image = media.media as? TelegramMediaImage { // let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) // let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() @@ -709,7 +709,7 @@ // })) // } // -// let controller = ContextMenuController(actions: actions) +// let controller = makeContextMenuController(actions: actions) // controller.dismissed = { [weak self] in // self?.updateTextSelectionRects([], text: nil) // } diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 8ce880ad552..58f1bc18846 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -70,7 +70,7 @@ private final class MediaPreviewView: SimpleLayer { |> map { image in return image.flatMap(processImage) } - |> deliverOnMainQueue).start(next: { [weak self] image in + |> deliverOnMainQueue).startStrict(next: { [weak self] image in guard let strongSelf = self else { return } @@ -1180,7 +1180,7 @@ public final class CalendarMessageScreen: ViewController { self.isLoadingMoreDisposable = (self.calendarSource.isLoadingMore |> distinctUntilChanged |> filter { !$0 } - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in guard let strongSelf = self else { return } @@ -1188,7 +1188,7 @@ public final class CalendarMessageScreen: ViewController { }).strict() self.stateDisposable = (self.calendarSource.state - |> deliverOnMainQueue).start(next: { [weak self] state in + |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let strongSelf = self else { return } @@ -1487,7 +1487,7 @@ public final class CalendarMessageScreen: ViewController { mainPeer: chatPeer ) } - |> deliverOnMainQueue).start(next: { [weak self] info in + |> deliverOnMainQueue).startStandalone(next: { [weak self] info in guard let strongSelf = self, let info = info else { return } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index aeeafc34e29..583d63023ad 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -127,7 +127,7 @@ public final class CallListController: TelegramBaseController { } self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings @@ -217,7 +217,7 @@ public final class CallListController: TelegramBaseController { let _ = (strongSelf.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) - |> deliverOnMainQueue).start(next: { peer in + |> deliverOnMainQueue).startStandalone(next: { peer in if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } @@ -335,7 +335,7 @@ public final class CallListController: TelegramBaseController { self?.clearDisposable.set(nil) } strongSelf.clearDisposable.set((signal - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStrict(completed: { })) } @@ -386,7 +386,7 @@ public final class CallListController: TelegramBaseController { controller.navigationPresentation = .modal self.createActionDisposable.set((controller.result |> take(1) - |> deliverOnMainQueue).start(next: { [weak controller, weak self] result in + |> deliverOnMainQueue).startStrict(next: { [weak controller, weak self] result in controller?.dismissSearch() if let strongSelf = self, let (contactPeers, action, _, _, _) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer { strongSelf.call(peer.id, isVideo: action == .videoCall, began: { @@ -396,7 +396,7 @@ public final class CallListController: TelegramBaseController { |> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true)) |> delay(0.5, queue: Queue.mainQueue()) |> take(1) - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).startStandalone(next: { _ in if let _ = self, let controller = controller, let navigationController = controller.navigationController as? NavigationController { if navigationController.viewControllers.last === controller { let _ = navigationController.popViewController(animated: true) @@ -467,7 +467,7 @@ public final class CallListController: TelegramBaseController { private func call(_ peerId: EnginePeer.Id, isVideo: Bool, began: (() -> Void)? = nil) { self.peerViewDisposable.set((self.context.account.viewTracker.peerView(peerId) |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] view in + |> deliverOnMainQueue).startStrict(next: { [weak self] view in if let strongSelf = self { guard let peer = peerViewMainPeer(view) else { return diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index 0812280fd2b..937fb4f4d0f 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -329,7 +329,7 @@ final class CallListControllerNode: ASDisplayNode { let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) - |> deliverOnMainQueue).start(next: { peer in + |> deliverOnMainQueue).startStandalone(next: { peer in guard let strongSelf = self, let peer = peer else { return } @@ -342,7 +342,7 @@ final class CallListControllerNode: ASDisplayNode { guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forEveryone).startStandalone() })) items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in @@ -352,7 +352,7 @@ final class CallListControllerNode: ASDisplayNode { return } - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).startStandalone() })) actionSheet.setItemGroups([ @@ -369,10 +369,10 @@ final class CallListControllerNode: ASDisplayNode { if let strongSelf = self { let _ = updateCallListSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { $0.withUpdatedShowTab(value) - }).start() + }).startStandalone() if value { - let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).start() + let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).startStandalone() } } }, openGroupCall: { [weak self] peerId in @@ -596,7 +596,7 @@ final class CallListControllerNode: ASDisplayNode { } } - self.callListDisposable.set(appliedTransition.start()) + self.callListDisposable.set(appliedTransition.startStrict()) self.callListLocationAndType.set(self.currentLocationAndType) @@ -606,7 +606,7 @@ final class CallListControllerNode: ASDisplayNode { } |> distinctUntilChanged - self.emptyStateDisposable.set((combineLatest(emptySignal, typeSignal, self.statePromise.get()) |> deliverOnMainQueue).start(next: { [weak self] isEmpty, type, state in + self.emptyStateDisposable.set((combineLatest(emptySignal, typeSignal, self.statePromise.get()) |> deliverOnMainQueue).startStrict(next: { [weak self] isEmpty, type, state in if let strongSelf = self { strongSelf.updateEmptyPlaceholder(theme: state.presentationData.theme, strings: state.presentationData.strings, type: type, isHidden: !isEmpty) } diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 249d6d4e7a1..f0bf380219a 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -95,7 +95,7 @@ final class CameraDeviceContext { return 30.0 } switch DeviceModel.current { - case .iPhone14ProMax, .iPhone13ProMax: + case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax: return 60.0 default: return 30.0 @@ -119,12 +119,6 @@ private final class CameraContext { private let detectedCodesPipe = ValuePipe<[CameraCode]>() fileprivate let modeChangePromise = ValuePromise(.none) - var previewNode: CameraPreviewNode? { - didSet { - self.previewNode?.prepare() - } - } - var previewView: CameraPreviewView? var simplePreviewView: CameraSimplePreviewView? @@ -310,9 +304,7 @@ private final class CameraContext { self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in guard let self, let mainDeviceContext = self.mainDeviceContext else { return - } - self.previewNode?.enqueue(sampleBuffer) - + } let timestamp = CACurrentMediaTime() if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording { var front = false @@ -350,8 +342,6 @@ private final class CameraContext { guard let self, let mainDeviceContext = self.mainDeviceContext else { return } - self.previewNode?.enqueue(sampleBuffer) - let timestamp = CACurrentMediaTime() if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording { var front = false @@ -371,20 +361,20 @@ private final class CameraContext { if #available(iOS 13.0, *), let previewView = self.simplePreviewView { if enabled, let secondaryPreviewView = self.secondaryPreviewView { let _ = (combineLatest(previewView.isPreviewing, secondaryPreviewView.isPreviewing) - |> map { first, second in + |> map { first, second in return first && second } |> filter { $0 } |> take(1) |> delay(0.1, queue: self.queue) - |> deliverOn(self.queue)).start(next: { [weak self] _ in + |> deliverOn(self.queue)).startStandalone(next: { [weak self] _ in self?.modeChange = .none }) } else { let _ = (previewView.isPreviewing |> filter { $0 } |> take(1) - |> deliverOn(self.queue)).start(next: { [weak self] _ in + |> deliverOn(self.queue)).startStandalone(next: { [weak self] _ in self?.modeChange = .none }) } @@ -814,20 +804,6 @@ public final class Camera { return disposable } } - - public func attachPreviewNode(_ node: CameraPreviewNode) { - let nodeRef: Unmanaged = Unmanaged.passRetained(node) - self.queue.async { - if let context = self.contextRef?.takeUnretainedValue() { - context.previewNode = nodeRef.takeUnretainedValue() - nodeRef.release() - } else { - Queue.mainQueue().async { - nodeRef.release() - } - } - } - } public func attachPreviewView(_ view: CameraPreviewView) { self.previewView = view diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 203e3263b1a..691f9c87bf4 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -152,6 +152,10 @@ final class CameraDevice { if device.isLowLightBoostSupported { device.automaticallyEnablesLowLightBoostWhenAvailable = true } + + if device.isExposureModeSupported(.continuousAutoExposure) { + device.exposureMode = .continuousAutoExposure + } } } diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index f4bc7f8a7c9..8bef2e9d077 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -7,6 +7,10 @@ public extension Camera { case iPhone14Plus case iPhone14Pro case iPhone14ProMax + case iPhone15 + case iPhone15Plus + case iPhone15Pro + case iPhone15ProMax case unknown init(model: DeviceModel) { @@ -21,6 +25,14 @@ public extension Camera { self = .iPhone14Pro case .iPhone14ProMax: self = .iPhone14ProMax + case .iPhone15: + self = .iPhone15 + case .iPhone15Plus: + self = .iPhone15Plus + case .iPhone15Pro: + self = .iPhone15Pro + case .iPhone15ProMax: + self = .iPhone15ProMax case .unknown: self = .unknown default: @@ -32,13 +44,9 @@ public extension Camera { switch self { case .singleCamera: return [1.0] - case .iPhone14: - return [0.5, 1.0, 2.0] - case .iPhone14Plus: + case .iPhone14, .iPhone14Plus, .iPhone15, .iPhone15Plus: return [0.5, 1.0, 2.0] - case .iPhone14Pro: - return [0.5, 1.0, 2.0, 3.0] - case .iPhone14ProMax: + case .iPhone14Pro, .iPhone14ProMax, .iPhone15Pro, .iPhone15ProMax: return [0.5, 1.0, 2.0, 3.0] case .unknown: return [1.0, 2.0] @@ -91,7 +99,11 @@ enum DeviceModel: CaseIterable, Equatable { .iPhone14, .iPhone14Plus, .iPhone14Pro, - .iPhone14ProMax + .iPhone14ProMax, + .iPhone15, + .iPhone15Plus, + .iPhone15Pro, + .iPhone15ProMax ] } @@ -154,6 +166,11 @@ enum DeviceModel: CaseIterable, Equatable { case iPhone14Pro case iPhone14ProMax + case iPhone15 + case iPhone15Plus + case iPhone15Pro + case iPhone15ProMax + case unknown(String) var modelId: [String] { @@ -248,6 +265,14 @@ enum DeviceModel: CaseIterable, Equatable { return ["iPhone15,2"] case .iPhone14ProMax: return ["iPhone15,3"] + case .iPhone15: + return ["iPhone15,4"] + case .iPhone15Plus: + return ["iPhone15,5"] + case .iPhone15Pro: + return ["iPhone16,1"] + case .iPhone15ProMax: + return ["iPhone16,2"] case let .unknown(modelId): return [modelId] } @@ -345,6 +370,14 @@ enum DeviceModel: CaseIterable, Equatable { return "iPhone 14 Pro" case .iPhone14ProMax: return "iPhone 14 Pro Max" + case .iPhone15: + return "iPhone 15" + case .iPhone15Plus: + return "iPhone 15 Plus" + case .iPhone15Pro: + return "iPhone 15 Pro" + case .iPhone15ProMax: + return "iPhone 15 Pro Max" case let .unknown(modelId): if modelId.hasPrefix("iPhone") { return "Unknown iPhone" diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index c0209e34c23..e312b335f7a 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -245,7 +245,7 @@ final class CameraOutput: NSObject { } let settings = AVCapturePhotoSettings(format: [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]) - settings.flashMode = flashMode + settings.flashMode = mirror ? .off : flashMode if let previewPhotoPixelFormatType = settings.availablePreviewPhotoPixelFormatTypes.first { settings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPhotoPixelFormatType] } diff --git a/submodules/Camera/Sources/CameraPreviewNode.swift b/submodules/Camera/Sources/CameraPreviewNode.swift deleted file mode 100644 index 019e73a47e5..00000000000 --- a/submodules/Camera/Sources/CameraPreviewNode.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display -import AVFoundation -import SwiftSignalKit - -private final class CameraPreviewNodeLayerNullAction: NSObject, CAAction { - @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { - } -} - -private final class CameraPreviewNodeLayer: AVSampleBufferDisplayLayer { - override func action(forKey event: String) -> CAAction? { - return CameraPreviewNodeLayerNullAction() - } -} - -public final class CameraPreviewNode: ASDisplayNode { - private var displayLayer: AVSampleBufferDisplayLayer - - private let fadeNode: ASDisplayNode - private var fadedIn = false - - public override init() { - self.displayLayer = AVSampleBufferDisplayLayer() - self.displayLayer.videoGravity = .resizeAspectFill - - self.fadeNode = ASDisplayNode() - self.fadeNode.backgroundColor = .black - self.fadeNode.isUserInteractionEnabled = false - - super.init() - - self.clipsToBounds = true - - self.layer.addSublayer(self.displayLayer) - - self.addSubnode(self.fadeNode) - } - - func prepare() { - DispatchQueue.main.async { - self.displayLayer.flushAndRemoveImage() - } - } - - func enqueue(_ sampleBuffer: CMSampleBuffer) { - self.displayLayer.enqueue(sampleBuffer) - - if !self.fadedIn { - self.fadedIn = true - Queue.mainQueue().after(0.2) { - self.fadeNode.alpha = 0.0 - self.fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - override public func layout() { - super.layout() - - var transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0) - transform = transform.scaledBy(x: 1.0, y: 1.0) - self.displayLayer.setAffineTransform(transform) - - self.displayLayer.frame = self.bounds - self.fadeNode.frame = self.bounds - } -} diff --git a/submodules/ChatImportUI/BUILD b/submodules/ChatImportUI/BUILD index 0fe6a7232f2..2ee541d7dae 100644 --- a/submodules/ChatImportUI/BUILD +++ b/submodules/ChatImportUI/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/ConfettiEffect:ConfettiEffect", "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", + "//submodules/ActivityIndicator", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index 97499707875..0442a760a1a 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -15,6 +15,7 @@ import MimeTypes import ConfettiEffect import TelegramUniversalVideoContent import SolidRoundedButtonNode +import ActivityIndicator private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { if useTotalFileAllocatedSize { @@ -148,7 +149,7 @@ private final class ImportManager { return .limitExceeded } } - |> deliverOnMainQueue).start(next: { [weak self] session in + |> deliverOnMainQueue).startStrict(next: { [weak self] session in guard let strongSelf = self else { return } @@ -200,7 +201,7 @@ private final class ImportManager { return } self.disposable.set((TelegramEngine(account: self.account).historyImport.startImport(session: session) - |> deliverOnMainQueue).start(error: { [weak self] _ in + |> deliverOnMainQueue).startStrict(error: { [weak self] _ in guard let strongSelf = self else { return } @@ -848,14 +849,14 @@ public final class ChatImportActivityScreen: ViewController { } self.disposable.set((resolvedPeerId - |> deliverOnMainQueue).start(next: { [weak self] peerId in + |> deliverOnMainQueue).startStrict(next: { [weak self] peerId in guard let strongSelf = self else { return } let importManager = ImportManager(account: strongSelf.context.account, peerId: peerId, mainFile: strongSelf.mainEntry, archivePath: strongSelf.archivePath, entries: strongSelf.otherEntries) strongSelf.importManager = importManager strongSelf.progressDisposable.set((importManager.state - |> deliverOnMainQueue).start(next: { state in + |> deliverOnMainQueue).startStrict(next: { state in guard let strongSelf = self else { return } @@ -886,3 +887,48 @@ public final class ChatImportActivityScreen: ViewController { } } } + +public final class ChatImportTempController: ViewController { + override public var _presentedInModal: Bool { + get { + return true + } set(value) { + } + } + + private let activityIndicator: ActivityIndicator + + public init(presentationData: PresentationData) { + let presentationData = presentationData + + self.activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.list.itemAccentColor, 22.0, 1.0, false)) + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) + + self.title = presentationData.strings.ChatImport_Title + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + //self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + } + + override public func displayNodeDidLoad() { + super.displayNodeDidLoad() + + self.displayNode.addSubnode(self.activityIndicator) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) + let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: navigationHeight + floor((layout.size.height - navigationHeight - indicatorSize.height) / 2.0)), size: indicatorSize)) + } +} + diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index 9390e85403e..e8423ecd3da 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -41,15 +41,15 @@ public struct ChatInterfaceSelectionState: Codable, Equatable { } public struct ChatEditMessageState: Codable, Equatable { - public let messageId: EngineMessage.Id - public let inputState: ChatTextInputState - public let disableUrlPreview: String? - public let inputTextMaxLength: Int32? + public var messageId: EngineMessage.Id + public var inputState: ChatTextInputState + public var disableUrlPreviews: [String] + public var inputTextMaxLength: Int32? - public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreview: String?, inputTextMaxLength: Int32?) { + public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreviews: [String], inputTextMaxLength: Int32?) { self.messageId = messageId self.inputState = inputState - self.disableUrlPreview = disableUrlPreview + self.disableUrlPreviews = disableUrlPreviews self.inputTextMaxLength = inputTextMaxLength } @@ -68,7 +68,15 @@ public struct ChatEditMessageState: Codable, Equatable { self.inputState = ChatTextInputState() } - self.disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") + if let disableUrlPreviews = try? container.decodeIfPresent([String].self, forKey: "dupl") { + self.disableUrlPreviews = disableUrlPreviews + } else { + if let disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") { + self.disableUrlPreviews = [disableUrlPreview] + } else { + self.disableUrlPreviews = [] + } + } self.inputTextMaxLength = try? container.decodeIfPresent(Int32.self, forKey: "tl") } @@ -82,20 +90,20 @@ public struct ChatEditMessageState: Codable, Equatable { try container.encode(self.inputState, forKey: "is") - try container.encodeIfPresent(self.disableUrlPreview, forKey: "dup") + try container.encode(self.disableUrlPreviews, forKey: "dupl") try container.encodeIfPresent(self.inputTextMaxLength, forKey: "tl") } public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool { - return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreview == rhs.disableUrlPreview && lhs.inputTextMaxLength == rhs.inputTextMaxLength + return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreviews == rhs.disableUrlPreviews && lhs.inputTextMaxLength == rhs.inputTextMaxLength } public func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState { - return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreview: self.disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength) + return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreviews: self.disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength) } - public func withUpdatedDisableUrlPreview(_ disableUrlPreview: String?) -> ChatEditMessageState { - return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreview: disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength) + public func withUpdatedDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatEditMessageState { + return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength) } } @@ -259,10 +267,27 @@ public struct ChatInterfaceHistoryScrollState: Codable, Equatable { } public final class ChatInterfaceState: Codable, Equatable { + public struct ReplyMessageSubject: Codable, Equatable { + public var messageId: EngineMessage.Id + public var quote: EngineMessageReplyQuote? + + public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?) { + self.messageId = messageId + self.quote = quote + } + + public var subjectModel: EngineMessageReplySubject { + return EngineMessageReplySubject( + messageId: self.messageId, + quote: self.quote + ) + } + } + public let timestamp: Int32 public let composeInputState: ChatTextInputState - public let composeDisableUrlPreview: String? - public let replyMessageId: EngineMessage.Id? + public let composeDisableUrlPreviews: [String] + public let replyMessageSubject: ReplyMessageSubject? public let forwardMessageIds: [EngineMessage.Id]? public let forwardOptionsState: ChatInterfaceForwardOptionsState? public let editMessage: ChatEditMessageState? @@ -277,15 +302,20 @@ public final class ChatInterfaceState: Codable, Equatable { public let inputLanguage: String? public var synchronizeableInputState: SynchronizeableChatInputState? { - if self.composeInputState.inputText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.replyMessageId == nil { + if self.composeInputState.inputText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.replyMessageSubject == nil { return nil } else { - return SynchronizeableChatInputState(replyToMessageId: self.replyMessageId, text: self.composeInputState.inputText.string, entities: generateChatInputTextEntities(self.composeInputState.inputText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange) + return SynchronizeableChatInputState(replySubject: self.replyMessageSubject?.subjectModel, text: self.composeInputState.inputText.string, entities: generateChatInputTextEntities(self.composeInputState.inputText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange) } } public func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> ChatInterfaceState { - var result = self.withUpdatedComposeInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(state?.text ?? "", entities: state?.entities ?? []))).withUpdatedReplyMessageId(state?.replyToMessageId) + var result = self.withUpdatedComposeInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(state?.text ?? "", entities: state?.entities ?? []))).withUpdatedReplyMessageSubject((state?.replySubject).flatMap { + return ReplyMessageSubject( + messageId: $0.messageId, + quote: $0.quote + ) + }) if let timestamp = state?.timestamp { result = result.withUpdatedTimestamp(timestamp) } @@ -307,8 +337,8 @@ public final class ChatInterfaceState: Codable, Equatable { public init() { self.timestamp = 0 self.composeInputState = ChatTextInputState() - self.composeDisableUrlPreview = nil - self.replyMessageId = nil + self.composeDisableUrlPreviews = [] + self.replyMessageSubject = nil self.forwardMessageIds = nil self.forwardOptionsState = nil self.editMessage = nil @@ -324,11 +354,11 @@ public final class ChatInterfaceState: Codable, Equatable { } // MARK: Nicegram (forwardAsCopy) - public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: EngineMessage.Id?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, forwardAsCopy: Bool = false, inputLanguage: String?) { + public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreviews: [String], replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, forwardAsCopy: Bool = false, inputLanguage: String?) { self.timestamp = timestamp self.composeInputState = composeInputState - self.composeDisableUrlPreview = composeDisableUrlPreview - self.replyMessageId = replyMessageId + self.composeDisableUrlPreviews = composeDisableUrlPreviews + self.replyMessageSubject = replyMessageSubject self.forwardMessageIds = forwardMessageIds self.forwardOptionsState = forwardOptionsState self.editMessage = editMessage @@ -352,18 +382,26 @@ public final class ChatInterfaceState: Codable, Equatable { } else { self.composeInputState = ChatTextInputState() } - if let composeDisableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") { - self.composeDisableUrlPreview = composeDisableUrlPreview + + if let composeDisableUrlPreviews = try? container.decodeIfPresent([String].self, forKey: "dupl") { + self.composeDisableUrlPreviews = composeDisableUrlPreviews + } else if let composeDisableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") { + self.composeDisableUrlPreviews = [composeDisableUrlPreview] } else { - self.composeDisableUrlPreview = nil + self.composeDisableUrlPreviews = [] } - let replyMessageIdPeerId: Int64? = try? container.decodeIfPresent(Int64.self, forKey: "r.p") - let replyMessageIdNamespace: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.n") - let replyMessageIdId: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.i") - if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { - self.replyMessageId = EngineMessage.Id(peerId: EnginePeer.Id(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId) + + if let replyMessageSubject = try? container.decodeIfPresent(ReplyMessageSubject.self, forKey: "replyMessageSubject") { + self.replyMessageSubject = replyMessageSubject } else { - self.replyMessageId = nil + let replyMessageIdPeerId: Int64? = try? container.decodeIfPresent(Int64.self, forKey: "r.p") + let replyMessageIdNamespace: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.n") + let replyMessageIdId: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.i") + if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { + self.replyMessageSubject = ReplyMessageSubject(messageId: EngineMessage.Id(peerId: EnginePeer.Id(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId), quote: nil) + } else { + self.replyMessageSubject = nil + } } if let forwardMessageIdsData = try? container.decodeIfPresent(Data.self, forKey: "fm") { self.forwardMessageIds = EngineMessage.Id.decodeArrayFromData(forwardMessageIdsData) @@ -408,19 +446,12 @@ public final class ChatInterfaceState: Codable, Equatable { try container.encode(self.timestamp, forKey: "ts") try container.encode(self.composeInputState, forKey: "is") - if let composeDisableUrlPreview = self.composeDisableUrlPreview { - try container.encode(composeDisableUrlPreview, forKey: "dup") - } else { - try container.encodeNil(forKey: "dup") - } - if let replyMessageId = self.replyMessageId { - try container.encode(replyMessageId.peerId.toInt64(), forKey: "r.p") - try container.encode(replyMessageId.namespace, forKey: "r.n") - try container.encode(replyMessageId.id, forKey: "r.i") + try container.encode(self.composeDisableUrlPreviews, forKey: "dup;") + + if let replyMessageSubject = self.replyMessageSubject { + try container.encode(replyMessageSubject, forKey: "replyMessageSubject") } else { - try container.encodeNil(forKey: "r.p") - try container.encodeNil(forKey: "r.n") - try container.encodeNil(forKey: "r.i") + try container.encodeNil(forKey: "replyMessageSubject") } if let forwardMessageIds = self.forwardMessageIds { try container.encode(EngineMessage.Id.encodeArrayToData(forwardMessageIds), forKey: "fm") @@ -465,7 +496,7 @@ public final class ChatInterfaceState: Codable, Equatable { } public static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { - if lhs.composeDisableUrlPreview != rhs.composeDisableUrlPreview { + if lhs.composeDisableUrlPreviews != rhs.composeDisableUrlPreviews { return false } if let lhsForwardMessageIds = lhs.forwardMessageIds, let rhsForwardMessageIds = rhs.forwardMessageIds { @@ -498,17 +529,23 @@ public final class ChatInterfaceState: Codable, Equatable { if lhs.inputLanguage != rhs.inputLanguage { return false } - return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage + return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageSubject == rhs.replyMessageSubject && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage + } + + // MARK: Nicegram ForwardAsCopy + public func withUpdatedForwardAsCopy(_ forwardAsCopy: Bool) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: forwardAsCopy, inputLanguage: self.inputLanguage) } + // public func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { let updatedComposeInputState = inputState - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } - public func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + public func withUpdatedComposeDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: disableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { @@ -520,19 +557,19 @@ public final class ChatInterfaceState: Codable, Equatable { updatedComposeInputState = inputState } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } - public func withUpdatedReplyMessageId(_ replyMessageId: EngineMessage.Id?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + public func withUpdatedReplyMessageSubject(_ replyMessageSubject: ReplyMessageSubject?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedForwardMessageIds(_ forwardMessageIds: [EngineMessage.Id]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedForwardOptionsState(_ forwardOptionsState: ChatInterfaceForwardOptionsState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedSelectedMessages(_ messageIds: [EngineMessage.Id]) -> ChatInterfaceState { @@ -543,7 +580,7 @@ public final class ChatInterfaceState: Codable, Equatable { for messageId in messageIds { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withToggledSelectedMessages(_ messageIds: [EngineMessage.Id], value: Bool) -> ChatInterfaceState { @@ -558,44 +595,39 @@ public final class ChatInterfaceState: Codable, Equatable { selectedIds.remove(messageId) } } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedSilentPosting(_ silentPosting: Bool) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: self.inputLanguage) } public func withUpdatedInputLanguage(_ inputLanguage: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: inputLanguage) - } - - // MARK: Nicegram - public func withUpdatedForwardAsCopy(_ forwardAsCopy: Bool) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: forwardAsCopy, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, forwardAsCopy: self.forwardAsCopy, inputLanguage: inputLanguage) } // diff --git a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift index 77bca338cd7..a6a5ffcfadf 100644 --- a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +++ b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift @@ -311,7 +311,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { let previous: Atomic<[ChatListSearchRecentPeersEntry]> = Atomic(value: []) let firstTime:Atomic = Atomic(value: true) - peersDisposable.add((combineLatest(queue: .mainQueue(), recent, self.itemCustomWidthValuePromise.get(), self.themeAndStringsPromise.get()) |> deliverOnMainQueue).start(next: { [weak self] peers, itemCustomWidth, themeAndStrings in + peersDisposable.add((combineLatest(queue: .mainQueue(), recent, self.itemCustomWidthValuePromise.get(), self.themeAndStringsPromise.get()) |> deliverOnMainQueue).startStrict(next: { [weak self] peers, itemCustomWidth, themeAndStrings in if let strongSelf = self { var entries: [ChatListSearchRecentPeersEntry] = [] for peer in peers.0 { @@ -348,7 +348,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { } })) if case .actionSheet = mode { - peersDisposable.add(_internal_managedUpdatedRecentPeers(accountPeerId: accountPeerId, postbox: postbox, network: network).start()) + peersDisposable.add(_internal_managedUpdatedRecentPeers(accountPeerId: accountPeerId, postbox: postbox, network: network).startStrict()) } self.disposable.set(peersDisposable) } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 895c1b41e84..233e5db0fc1 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -330,10 +330,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { c, _ in - c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) - c.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil) + c.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) }))) } } @@ -658,7 +658,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: }))) //c.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) - c.setItems(.single(ContextController.Items(content: .list(subItems))), minHeight: nil) + c.setItems(.single(ContextController.Items(content: .list(subItems))), minHeight: nil, animated: true) }))) items.append(.separator) @@ -813,7 +813,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: ], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedForever, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) - c.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) + c.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil, animated: true) } }))) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b970122f6e3..ef134e30fdb 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -909,9 +909,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: false, - isStatusSelection: true, - isReactionSelection: false, - isEmojiSelection: false, + subject: .status, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: false, @@ -1215,7 +1213,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .channel(channel) = actualPeer, channel.flags.contains(.isForum), let threadId { let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, keepStack: .never).startStandalone() } else { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: .message(id: .id(messageId), highlight: true, timecode: nil), purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), purposefulAction: { if deactivateOnAction { self?.deactivateSearch(animated: false) } @@ -1464,7 +1462,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { var subject: ChatControllerSubject? if case let .search(messageId) = source, let id = messageId { - subject = .message(id: .id(id), highlight: false, timecode: nil) + subject = .message(id: .id(id), highlight: nil, timecode: nil) } let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 68938689bc7..8f6e39e2039 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -788,6 +788,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele }, openArchiveSettings: { [weak self] in self?.openArchiveSettings() }, autoSetReady: !animated, isMainTab: index == 0) + self.pendingItemNode?.2.dispose() let disposable = MetaDisposable() self.pendingItemNode = (id, itemNode, disposable) @@ -804,6 +805,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele return } + strongSelf.pendingItemNode?.2.dispose() strongSelf.pendingItemNode = nil itemNode.listNode.tempTopInset = strongSelf.tempTopInset diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 9789d6e6d56..67846e0d4d0 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -198,7 +198,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) } }, openUrl: { [weak self] url in - openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in + let _ = openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in present(c, nil) }, openResolved: { [weak self] resolved in context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peerId, navigation in @@ -1377,7 +1377,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 4d8b8c8ec69..e81ee30de1a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3014,7 +3014,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.interaction.dismissInput() strongSelf.interaction.present(controller, nil) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: EngineMessage.Tags.music) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: EngineMessage.Tags.music) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -3531,7 +3531,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil)) let timestamp1: Int32 = 100000 var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer1.id] = peer1 @@ -3603,7 +3603,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { return nil case .links: var media: [EngineMedia] = [] - media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))))) + media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))))) let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 8c2abde6500..14526c30c65 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -150,7 +150,7 @@ final class ChatListShimmerNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil)) + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil)) let timestamp1: Int32 = 100000 let peers: [EnginePeer.Id: EnginePeer] = [:] let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index dd5058ffca2..0e83174898b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1007,6 +1007,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var cachedChatListText: (String, String)? private var cachedChatListSearchResult: CachedChatListSearchResult? + private var cachedChatListQuoteSearchResult: CachedChatListSearchResult? private var cachedCustomTextEntities: CachedCustomTextEntities? var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams, countersSize: CGFloat)? @@ -1625,6 +1626,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 let currentChatListText = self.cachedChatListText let currentChatListSearchResult = self.cachedChatListSearchResult + let currentChatListQuoteSearchResult = self.cachedChatListQuoteSearchResult let currentCustomTextEntities = self.cachedCustomTextEntities return { item, params, first, last, firstWithHeader, nextIsPinned in @@ -1914,6 +1916,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var chatListText: (String, String)? var chatListSearchResult: CachedChatListSearchResult? + var chatListQuoteSearchResult: CachedChatListSearchResult? var customTextEntities: CachedCustomTextEntities? let contentImageSide: CGFloat = max(10.0, min(20.0, floor(item.presentationData.fontSize.baseDisplaySize * 18.0 / 17.0))) @@ -2077,15 +2080,45 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { composedString = NSMutableAttributedString(attributedString: messageString) } + var composedReplyString: NSMutableAttributedString? if let searchQuery = item.interaction.searchTextHighightState { + var quoteText: String? + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + if let quote = attribute.quote { + quoteText = quote.text + } + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + if let quote = attribute.quote { + quoteText = quote.text + } + } + } + if let quoteText { + let quoteString = foldLineBreaks(stringWithAppliedEntities(quoteText, entities: [], baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)) + composedReplyString = NSMutableAttributedString(attributedString: quoteString) + } + if let cached = currentChatListSearchResult, cached.matches(text: composedString.string, searchQuery: searchQuery) { chatListSearchResult = cached } else { let (ranges, text) = findSubstringRanges(in: composedString.string, query: searchQuery) chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges) } + + if let composedReplyString { + if let cached = currentChatListQuoteSearchResult, cached.matches(text: composedReplyString.string, searchQuery: searchQuery) { + chatListQuoteSearchResult = cached + } else { + let (ranges, text) = findSubstringRanges(in: composedReplyString.string, query: searchQuery) + chatListQuoteSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges) + } + } else { + chatListQuoteSearchResult = nil + } } else { chatListSearchResult = nil + chatListQuoteSearchResult = nil } if let chatListSearchResult = chatListSearchResult, let firstRange = chatListSearchResult.resultRanges.first { @@ -2114,6 +2147,34 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { composedString = composedString.attributedSubstring(from: NSMakeRange(leftOrigin, composedString.length - leftOrigin)).mutableCopy() as! NSMutableAttributedString composedString.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: textFont, NSAttributedString.Key.foregroundColor: theme.messageTextColor]), at: 0) } + } else if var composedReplyString, let chatListQuoteSearchResult, let firstRange = chatListQuoteSearchResult.resultRanges.first { + for range in chatListQuoteSearchResult.resultRanges { + let stringRange = NSRange(range, in: chatListQuoteSearchResult.text) + if stringRange.location >= 0 && stringRange.location + stringRange.length <= composedReplyString.length { + var stringRange = stringRange + if stringRange.location > composedReplyString.length { + continue + } else if stringRange.location + stringRange.length > composedReplyString.length { + stringRange.length = composedReplyString.length - stringRange.location + } + composedReplyString.addAttribute(.foregroundColor, value: theme.messageHighlightedTextColor, range: stringRange) + } + } + + let firstRangeOrigin = chatListQuoteSearchResult.text.distance(from: chatListQuoteSearchResult.text.startIndex, to: firstRange.lowerBound) + if firstRangeOrigin > 24 { + var leftOrigin: Int = 0 + (composedReplyString.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in + let distanceFromEnd = firstRangeOrigin - range1.location + if (distanceFromEnd > 12 || range1.location == 0) && leftOrigin == 0 { + leftOrigin = range1.location + } + } + composedReplyString = composedReplyString.attributedSubstring(from: NSMakeRange(leftOrigin, composedReplyString.length - leftOrigin)).mutableCopy() as! NSMutableAttributedString + composedReplyString.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: textFont, NSAttributedString.Key.foregroundColor: theme.messageTextColor]), at: 0) + } + + composedString = composedReplyString } attributedText = composedString @@ -2786,6 +2847,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.currentItemHeight = itemHeight strongSelf.cachedChatListText = chatListText strongSelf.cachedChatListSearchResult = chatListSearchResult + strongSelf.cachedChatListQuoteSearchResult = chatListQuoteSearchResult strongSelf.cachedCustomTextEntities = customTextEntities strongSelf.onlineIsVoiceChat = onlineIsVoiceChat diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 862b20bc018..060ab6bd018 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -305,6 +305,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } else { messageText = strings.Notification_Story } + case _ as TelegramMediaGiveaway: + messageText = strings.Message_Giveaway + case let webpage as TelegramMediaWebpage: + if messageText.isEmpty, case let .Loaded(content) = webpage.content { + messageText = content.displayUrl + } default: break } diff --git a/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift b/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift index a26bf203618..59bd0ac957f 100644 --- a/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift +++ b/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift @@ -66,21 +66,26 @@ public class ChatMessageBackground: ASDisplayNode { private var hasWallpaper: Bool? private var graphics: PrincipalThemeEssentialGraphics? private var maskMode: Bool? - private let imageNode: ASImageNode private let outlineImageNode: ASImageNode private weak var backgroundNode: WallpaperBackgroundNode? + private var imageFrame: CGRect? + private var imageView: UIImageView? + private var imageViewImage: UIImage? + + public var customHighlightColor: UIColor? { + didSet { + self.imageView?.tintColor = self.customHighlightColor + } + } + public var backgroundFrame: CGRect = .zero public var hasImage: Bool { - self.imageNode.image != nil + self.imageViewImage != nil } public override init() { - self.imageNode = ASImageNode() - self.imageNode.displaysAsynchronously = false - self.imageNode.displayWithoutProcessing = true - self.outlineImageNode = ASImageNode() self.outlineImageNode.displaysAsynchronously = false self.outlineImageNode.displayWithoutProcessing = true @@ -89,16 +94,39 @@ public class ChatMessageBackground: ASDisplayNode { self.isUserInteractionEnabled = false self.addSubnode(self.outlineImageNode) - self.addSubnode(self.imageNode) + } + + override public func didLoad() { + super.didLoad() + + let imageView = UIImageView() + self.imageView = imageView + self.view.addSubview(imageView) + + imageView.image = self.imageViewImage + imageView.tintColor = self.customHighlightColor + + if let imageFrame = self.imageFrame { + imageView.frame = imageFrame + } } public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)) + let imageFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0) + self.imageFrame = imageFrame + if let imageView = self.imageView { + transition.updateFrame(view: imageView, frame: imageFrame) + } transition.updateFrame(node: self.outlineImageNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)) } public func updateLayout(size: CGSize, transition: ListViewItemUpdateAnimation) { - transition.animator.updateFrame(layer: self.imageNode.layer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0), completion: nil) + let imageFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0) + self.imageFrame = imageFrame + if let imageView = self.imageView { + transition.animator.updateFrame(layer: imageView.layer, frame: imageFrame, completion: nil) + } + transition.animator.updateFrame(layer: self.outlineImageNode.layer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0), completion: nil) } @@ -217,13 +245,11 @@ public class ChatMessageBackground: ASDisplayNode { } let outlineImage: UIImage? - var isIncoming = false if hasWallpaper { switch type { case .none: outlineImage = nil case let .incoming(mergeType): - isIncoming = true switch mergeType { case .None: outlineImage = graphics.chatMessageBackgroundIncomingOutlineImage @@ -267,38 +293,38 @@ public class ChatMessageBackground: ASDisplayNode { } if let previousType = previousType, previousType != .none, type == .none { - if transition.isAnimated { + if transition.isAnimated, let imageView = self.imageView { let tempLayer = CALayer() - tempLayer.contents = self.imageNode.layer.contents - tempLayer.contentsScale = self.imageNode.layer.contentsScale - tempLayer.rasterizationScale = self.imageNode.layer.rasterizationScale - tempLayer.contentsGravity = self.imageNode.layer.contentsGravity - tempLayer.contentsCenter = self.imageNode.layer.contentsCenter + tempLayer.contents = imageView.layer.contents + tempLayer.contentsScale = imageView.layer.contentsScale + tempLayer.rasterizationScale = imageView.layer.rasterizationScale + tempLayer.contentsGravity = imageView.layer.contentsGravity + tempLayer.contentsCenter = imageView.layer.contentsCenter - tempLayer.frame = self.imageNode.frame - self.layer.insertSublayer(tempLayer, above: self.imageNode.layer) + tempLayer.frame = imageView.frame + self.layer.insertSublayer(tempLayer, above: imageView.layer) transition.updateAlpha(layer: tempLayer, alpha: 0.0, completion: { [weak tempLayer] _ in tempLayer?.removeFromSuperlayer() }) } - } else if transition.isAnimated { - if let previousContents = self.imageNode.layer.contents { + } else if transition.isAnimated, let imageView = self.imageView { + if let previousContents = imageView.layer.contents { if let image = image { if (previousContents as AnyObject) !== image.cgImage { - self.imageNode.layer.animate(from: previousContents as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.42) + imageView.layer.animate(from: previousContents as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.42) } } else { let tempLayer = CALayer() - tempLayer.contents = self.imageNode.layer.contents - tempLayer.contentsScale = self.imageNode.layer.contentsScale - tempLayer.rasterizationScale = self.imageNode.layer.rasterizationScale - tempLayer.contentsGravity = self.imageNode.layer.contentsGravity - tempLayer.contentsCenter = self.imageNode.layer.contentsCenter - tempLayer.compositingFilter = self.imageNode.layer.compositingFilter + tempLayer.contents = imageView.layer.contents + tempLayer.contentsScale = imageView.layer.contentsScale + tempLayer.rasterizationScale = imageView.layer.rasterizationScale + tempLayer.contentsGravity = imageView.layer.contentsGravity + tempLayer.contentsCenter = imageView.layer.contentsCenter + tempLayer.compositingFilter = imageView.layer.compositingFilter - tempLayer.frame = self.imageNode.frame + tempLayer.frame = imageView.frame - self.imageNode.supernode?.layer.insertSublayer(tempLayer, above: self.imageNode.layer) + imageView.superview?.layer.insertSublayer(tempLayer, above: imageView.layer) transition.updateAlpha(layer: tempLayer, alpha: 0.0, completion: { [weak tempLayer] _ in tempLayer?.removeFromSuperlayer() }) @@ -306,26 +332,17 @@ public class ChatMessageBackground: ASDisplayNode { } } - self.imageNode.image = image - if highlighted && maskMode, let backdropNode = self.backdropNode, backdropNode.hasImage && isIncoming { - self.imageNode.layer.compositingFilter = "overlayBlendMode" - self.imageNode.alpha = 1.0 - - backdropNode.addSubnode(self.imageNode) - } else { - self.imageNode.layer.compositingFilter = nil - self.imageNode.alpha = 1.0 - - if self.imageNode.supernode != self { - self.addSubnode(self.imageNode) - } + self.imageViewImage = image + if let imageView = self.imageView { + imageView.image = image } + self.outlineImageNode.image = outlineImage } public func animateFrom(sourceView: UIView, transition: CombinedTransition) { if transition.isAnimated { - self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.imageView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) self.outlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) self.view.addSubview(sourceView) @@ -334,9 +351,11 @@ public class ChatMessageBackground: ASDisplayNode { sourceView?.removeFromSuperview() }) - transition.animateFrame(layer: self.imageNode.layer, from: sourceView.frame) + if let imageView = imageView { + transition.animateFrame(layer: imageView.layer, from: sourceView.frame) + transition.updateFrame(layer: sourceView.layer, frame: CGRect(origin: imageView.frame.origin, size: CGSize(width: imageView.frame.width - 7.0, height: imageView.frame.height))) + } transition.animateFrame(layer: self.outlineImageNode.layer, from: sourceView.frame) - transition.updateFrame(layer: sourceView.layer, frame: CGRect(origin: self.imageNode.frame.origin, size: CGSize(width: self.imageNode.frame.width - 7.0, height: self.imageNode.frame.height))) } } } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 24082c52289..03b686da640 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -74,7 +74,7 @@ public final class ChatPanelInterfaceInteraction { public let openGifs: () -> Void public let replyPrivately: (Message) -> Void // - public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void + public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let deleteSelectedMessages: () -> Void @@ -87,6 +87,8 @@ public final class ChatPanelInterfaceInteraction { public let forwardMessages: ([Message]) -> Void public let updateForwardOptionsState: ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void public let presentForwardOptions: (ASDisplayNode) -> Void + public let presentReplyOptions: (ASDisplayNode) -> Void + public let presentLinkOptions: (ASDisplayNode) -> Void public let shareSelectedMessages: () -> Void public let updateTextInputStateAndMode: (@escaping (ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void public let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void @@ -185,7 +187,7 @@ public final class ChatPanelInterfaceInteraction { openGifs: @escaping () -> Void = {}, replyPrivately: @escaping (Message) -> Void = { _ in }, // - setupReplyMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, + setupReplyMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, @@ -198,6 +200,8 @@ public final class ChatPanelInterfaceInteraction { forwardMessages: @escaping ([Message]) -> Void, updateForwardOptionsState: @escaping ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void, presentForwardOptions: @escaping (ASDisplayNode) -> Void, + presentReplyOptions: @escaping (ASDisplayNode) -> Void, + presentLinkOptions: @escaping (ASDisplayNode) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, @@ -308,6 +312,8 @@ public final class ChatPanelInterfaceInteraction { self.forwardMessages = forwardMessages self.updateForwardOptionsState = updateForwardOptionsState self.presentForwardOptions = presentForwardOptions + self.presentReplyOptions = presentReplyOptions + self.presentLinkOptions = presentLinkOptions self.shareSelectedMessages = shareSelectedMessages self.updateTextInputStateAndMode = updateTextInputStateAndMode self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId @@ -423,6 +429,8 @@ public final class ChatPanelInterfaceInteraction { }, forwardMessages: { _ in }, updateForwardOptionsState: { _ in }, presentForwardOptions: { _ in + }, presentReplyOptions: { _ in + }, presentLinkOptions: { _ in }, shareSelectedMessages: { }, updateTextInputStateAndMode: updateTextInputStateAndMode, updateInputModeAndDismissedButtonKeyboardMessageId: updateInputModeAndDismissedButtonKeyboardMessageId, openStickers: { }, editMessage: { diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index fad2f1e4994..2b1855e0375 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -324,6 +324,33 @@ public final class ChatPresentationInterfaceState: Equatable { } } + public struct UrlPreview: Equatable { + public var url: String + public var webPage: TelegramMediaWebpage + public var positionBelowText: Bool + public var largeMedia: Bool? + + public init(url: String, webPage: TelegramMediaWebpage, positionBelowText: Bool, largeMedia: Bool?) { + self.url = url + self.webPage = webPage + self.positionBelowText = positionBelowText + self.largeMedia = largeMedia + } + } + + public struct AccountPeerColor: Equatable { + public enum Style { + case solid + case doubleDashed + case tripleDashed + } + public var style: Style + + public init(style: Style) { + self.style = style + } + } + public let interfaceState: ChatInterfaceState public let chatLocation: ChatLocation public let renderedPeer: RenderedPeer? @@ -350,8 +377,8 @@ public final class ChatPresentationInterfaceState: Equatable { public let slowmodeState: ChatSlowmodeState? public let chatHistoryState: ChatHistoryNodeHistoryState? public let botStartPayload: String? - public let urlPreview: (String, TelegramMediaWebpage)? - public let editingUrlPreview: (String, TelegramMediaWebpage)? + public let urlPreview: UrlPreview? + public let editingUrlPreview: UrlPreview? public let search: ChatSearchData? public let searchQuerySuggestionResult: ChatPresentationInputQueryResult? public let presentationReady: Bool @@ -393,8 +420,10 @@ public final class ChatPresentationInterfaceState: Equatable { public let threadData: ThreadData? public let isGeneralThreadClosed: Bool? public let translationState: ChatPresentationTranslationState? + public let replyMessage: Message? + public let accountPeerColor: AccountPeerColor? - 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?) { + 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() self.inputTextPanelState = ChatTextInputPanelState() self.editMessageState = nil @@ -464,9 +493,11 @@ public final class ChatPresentationInterfaceState: Equatable { self.threadData = threadData self.isGeneralThreadClosed = isGeneralThreadClosed self.translationState = nil + self.replyMessage = replyMessage + self.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: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, 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?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?) { + 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?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?, replyMessage: Message?, accountPeerColor: AccountPeerColor?) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer @@ -536,6 +567,8 @@ public final class ChatPresentationInterfaceState: Equatable { self.threadData = threadData self.isGeneralThreadClosed = isGeneralThreadClosed self.translationState = translationState + self.replyMessage = replyMessage + self.accountPeerColor = accountPeerColor } public static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -622,20 +655,14 @@ public final class ChatPresentationInterfaceState: Equatable { return false } if let lhsUrlPreview = lhs.urlPreview, let rhsUrlPreview = rhs.urlPreview { - if lhsUrlPreview.0 != rhsUrlPreview.0 { - return false - } - if !lhsUrlPreview.1.isEqual(to: rhsUrlPreview.1) { + if lhsUrlPreview != rhsUrlPreview { return false } } else if (lhs.urlPreview != nil) != (rhs.urlPreview != nil) { return false } if let lhsEditingUrlPreview = lhs.editingUrlPreview, let rhsEditingUrlPreview = rhs.editingUrlPreview { - if lhsEditingUrlPreview.0 != rhsEditingUrlPreview.0 { - return false - } - if !lhsEditingUrlPreview.1.isEqual(to: rhsEditingUrlPreview.1) { + if lhsEditingUrlPreview != rhsEditingUrlPreview { return false } } else if (lhs.editingUrlPreview != nil) != (rhs.editingUrlPreview != nil) { @@ -758,35 +785,41 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.translationState != rhs.translationState { return false } + if lhs.replyMessage.flatMap(EngineMessage.init) != rhs.replyMessage.flatMap(EngineMessage.init) { + return false + } + if lhs.accountPeerColor != rhs.accountPeerColor { + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } public func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -798,219 +831,227 @@ 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } - public func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } - public func updatedEditingUrlPreview(_ editingUrlPreview: (String, TelegramMediaWebpage)?) -> 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState) + 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, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) } 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState) + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + } + + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor) + } + + 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, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor) } } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift index b4fd3e9b6d3..68b3c7170ae 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift @@ -8,23 +8,89 @@ public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, att let nsRange = NSRange(location: state.selectionRange.lowerBound, length: state.selectionRange.count) var addAttribute = true var attributesToRemove: [NSAttributedString.Key] = [] - state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in + state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in for (key, _) in attributes { - if key == attribute && range == nsRange { + if key == attribute { addAttribute = false attributesToRemove.append(key) } } } + var selectionRange = state.selectionRange + let result = NSMutableAttributedString(attributedString: state.inputText) for attribute in attributesToRemove { - result.removeAttribute(attribute, range: nsRange) + if attribute == ChatTextInputAttributes.quote { + var removeRange = nsRange + + var selectionIndex = nsRange.upperBound + if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a { + result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound) + selectionIndex += 1 + removeRange.length += 1 + } + if nsRange.lowerBound != 0 && (result.string as NSString).character(at: nsRange.lowerBound - 1) != 0x0a { + result.insert(NSAttributedString(string: "\n"), at: nsRange.lowerBound) + selectionIndex += 1 + removeRange.location += 1 + } else if nsRange.lowerBound != 0 { + removeRange.location -= 1 + removeRange.length += 1 + } + + if removeRange.lowerBound > result.length { + removeRange = NSRange(location: result.length, length: 0) + } else if removeRange.upperBound > result.length { + removeRange = NSRange(location: removeRange.lowerBound, length: result.length - removeRange.lowerBound) + } + result.removeAttribute(attribute, range: removeRange) + + if selectionRange.lowerBound > result.length { + selectionRange = result.length ..< result.length + } else if selectionRange.upperBound > result.length { + selectionRange = selectionRange.lowerBound ..< result.length + } + + // Prevent merge back + result.enumerateAttributes(in: NSRange(location: selectionIndex, length: result.length - selectionIndex), options: .longestEffectiveRangeNotRequired) { attributes, range, _ in + for (key, value) in attributes { + if let _ = value as? ChatTextInputTextQuoteAttribute { + result.removeAttribute(key, range: range) + result.addAttribute(key, value: ChatTextInputTextQuoteAttribute(), range: range) + } + } + } + + selectionRange = selectionIndex ..< selectionIndex + } else { + result.removeAttribute(attribute, range: nsRange) + } } + if addAttribute { - result.addAttribute(attribute, value: true as Bool, range: nsRange) + if attribute == ChatTextInputAttributes.quote { + result.addAttribute(attribute, value: ChatTextInputTextQuoteAttribute(), range: nsRange) + var selectionIndex = nsRange.upperBound + if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a { + result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound) + selectionIndex += 1 + } + if nsRange.lowerBound != 0 && (result.string as NSString).character(at: nsRange.lowerBound - 1) != 0x0a { + result.insert(NSAttributedString(string: "\n"), at: nsRange.lowerBound) + selectionIndex += 1 + } + selectionRange = selectionIndex ..< selectionIndex + } else { + result.addAttribute(attribute, value: true as Bool, range: nsRange) + } } - return ChatTextInputState(inputText: result, selectionRange: state.selectionRange) + if selectionRange.lowerBound > result.length { + selectionRange = result.length ..< result.length + } else if selectionRange.upperBound > result.length { + selectionRange = selectionRange.lowerBound ..< result.length + } + return ChatTextInputState(inputText: result, selectionRange: selectionRange) } else { return state } @@ -106,3 +172,29 @@ public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer: return state } } + +public func chatTextInputAddQuoteAttribute(_ state: ChatTextInputState, selectionRange: Range) -> ChatTextInputState { + if selectionRange.isEmpty { + return state + } + let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count) + var quoteRange = nsRange + var attributesToRemove: [(NSAttributedString.Key, NSRange)] = [] + state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in + for (key, _) in attributes { + if key == ChatTextInputAttributes.quote { + attributesToRemove.append((key, range)) + quoteRange = quoteRange.union(range) + } else { + attributesToRemove.append((key, nsRange)) + } + } + } + + let result = NSMutableAttributedString(attributedString: state.inputText) + for (attribute, range) in attributesToRemove { + result.removeAttribute(attribute, range: range) + } + result.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: nsRange) + return ChatTextInputState(inputText: result, selectionRange: selectionRange) +} diff --git a/submodules/ChatSendMessageActionUI/BUILD b/submodules/ChatSendMessageActionUI/BUILD index 119caa4a411..772c15f75c2 100644 --- a/submodules/ChatSendMessageActionUI/BUILD +++ b/submodules/ChatSendMessageActionUI/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 0b41fd4bd15..cd1e713f21f 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -36,7 +36,7 @@ public final class ChatSendMessageActionSheetController: ViewController { private let gesture: ContextGesture private let sourceSendButton: ASDisplayNode - private let textInputNode: EditableTextNode + private let textInputView: UITextView private let attachment: Bool private let canSendWhenOnline: Bool private let completion: () -> Void @@ -55,7 +55,7 @@ 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, textInputNode: EditableTextNode, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, translate: @escaping () -> Void = {}, chooseLanguage: @escaping () -> Void = {}, schedule: @escaping () -> Void) { + 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) { self.context = context self.peerId = peerId self.isScheduledMessages = isScheduledMessages @@ -63,7 +63,7 @@ public final class ChatSendMessageActionSheetController: ViewController { self.hasEntityKeyboard = hasEntityKeyboard self.gesture = gesture self.sourceSendButton = sourceSendButton - self.textInputNode = textInputNode + self.textInputView = textInputView self.attachment = attachment self.canSendWhenOnline = canSendWhenOnline self.completion = completion @@ -81,7 +81,7 @@ public final class ChatSendMessageActionSheetController: ViewController { self.blocksBackgroundWhenInOverlay = true self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) - |> deliverOnMainQueue).start(next: { [weak self] presentationData in + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.presentationData = presentationData if strongSelf.isNodeLoaded { @@ -127,7 +127,7 @@ public final class ChatSendMessageActionSheetController: ViewController { let interlocutorLangCode = getCachedLanguageCode(forChatWith: peerId) // // MARK: Nicegram TranslateEnteredMessage, change (interlocutorLangCode + translate + chooseLanguage) - self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in + self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputView: self.textInputView, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in self?.sendMessage(.generic) self?.dismiss(cancel: false) }, sendSilently: { [weak self] in diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index 6278321caa6..6d3c40770f7 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -13,6 +13,7 @@ import AppBundle import ContextUI import TextFormat import EmojiTextAttachmentView +import ChatInputTextNode private let leftInset: CGFloat = 16.0 private let rightInset: CGFloat = 16.0 @@ -172,7 +173,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private var presentationData: PresentationData private let sourceSendButton: ASDisplayNode private let textFieldFrame: CGRect - private let textInputNode: EditableTextNode + private let textInputView: UITextView private let attachment: Bool private let forwardedCount: Int? private let hasEntityKeyboard: Bool @@ -189,8 +190,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private let messageClipNode: ASDisplayNode private let messageBackgroundNode: ASImageNode - private let fromMessageTextNode: EditableTextNode - private let toMessageTextNode: EditableTextNode + private let fromMessageTextScrollView: UIScrollView + private let fromMessageTextNode: ChatInputTextNode + private let toMessageTextScrollView: UIScrollView + private let toMessageTextNode: ChatInputTextNode private let scrollNode: ASScrollNode private var fromCustomEmojiContainerView: CustomEmojiContainerView? @@ -207,12 +210,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? // MARK: Nicegram TranslateEnteredMessage, change (canTranslate + interlocutorLangCode + translate + chooseLanguage) - init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool, canSendWhenOnline: Bool, forwardedCount: Int?, hasEntityKeyboard: Bool, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, send: (() -> Void)?, sendSilently: (() -> Void)?, sendWhenOnline: (() -> Void)?, schedule: (() -> Void)?, canTranslate: Bool, interlocutorLangCode: String?, translate: (() -> Void)?, chooseLanguage: (() -> ())?, cancel: (() -> Void)?) { + init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool, canSendWhenOnline: Bool, forwardedCount: Int?, hasEntityKeyboard: Bool, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, send: (() -> Void)?, sendSilently: (() -> Void)?, sendWhenOnline: (() -> Void)?, schedule: (() -> Void)?, canTranslate: Bool, interlocutorLangCode: String?, translate: (() -> Void)?, chooseLanguage: (() -> ())?, cancel: (() -> Void)?) { self.context = context self.presentationData = presentationData self.sourceSendButton = sourceSendButton - self.textFieldFrame = textInputNode.convert(textInputNode.bounds, to: nil) - self.textInputNode = textInputNode + self.textFieldFrame = textInputView.convert(textInputView.bounds, to: nil) + self.textInputView = textInputView self.attachment = attachment self.forwardedCount = forwardedCount self.hasEntityKeyboard = hasEntityKeyboard @@ -237,11 +240,17 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageClipNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.messageBackgroundNode = ASImageNode() self.messageBackgroundNode.isUserInteractionEnabled = true - self.fromMessageTextNode = EditableTextNode() + self.fromMessageTextNode = ChatInputTextNode(disableTiling: true) + self.fromMessageTextNode.textView.isScrollEnabled = false self.fromMessageTextNode.isUserInteractionEnabled = false - self.toMessageTextNode = EditableTextNode() - self.toMessageTextNode.alpha = 0.0 + self.fromMessageTextScrollView = UIScrollView() + self.fromMessageTextScrollView.isUserInteractionEnabled = false + self.toMessageTextNode = ChatInputTextNode(disableTiling: true) + self.toMessageTextNode.textView.isScrollEnabled = false self.toMessageTextNode.isUserInteractionEnabled = false + self.toMessageTextScrollView = UIScrollView() + self.toMessageTextScrollView.alpha = 0.0 + self.toMessageTextScrollView.isUserInteractionEnabled = false self.scrollNode = ASScrollNode() self.scrollNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) @@ -291,8 +300,32 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.sendButtonNode.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) - if let attributedText = textInputNode.attributedText, !attributedText.string.isEmpty { + if let attributedText = textInputView.attributedText, !attributedText.string.isEmpty { self.animateInputField = true + if let textInputView = self.textInputView as? ChatInputTextView { + if let textTheme = textInputView.theme { + self.fromMessageTextNode.textView.theme = textTheme + + let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor + let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle + switch textTheme.quote.lineStyle { + case .solid: + mappedLineStyle = .solid(color: mainColor) + case .doubleDashed: + mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear) + case .tripleDashed: + mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear) + } + + self.toMessageTextNode.textView.theme = ChatInputTextView.Theme( + quote: ChatInputTextView.Theme.Quote( + background: mainColor.withMultipliedAlpha(0.1), + foreground: mainColor, + lineStyle: mappedLineStyle + ) + ) + } + } self.fromMessageTextNode.attributedText = attributedText if let toAttributedText = self.fromMessageTextNode.attributedText?.mutableCopy() as? NSMutableAttributedString { @@ -323,8 +356,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.addSubnode(self.sendButtonNode) self.scrollNode.addSubnode(self.messageClipNode) self.messageClipNode.addSubnode(self.messageBackgroundNode) - self.messageClipNode.addSubnode(self.fromMessageTextNode) - self.messageClipNode.addSubnode(self.toMessageTextNode) + self.messageClipNode.view.addSubview(self.fromMessageTextScrollView) + self.fromMessageTextScrollView.addSubview(self.fromMessageTextNode.view) + self.messageClipNode.view.addSubview(self.toMessageTextScrollView) + self.toMessageTextScrollView.addSubview(self.toMessageTextNode.view) self.contentNodes.forEach(self.contentContainerNode.addSubnode) @@ -413,7 +448,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.updateTextContents(rects: customEmojiRects, textInputNode: self.toMessageTextNode, from: false) } - func updateTextContents(rects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)], textInputNode: EditableTextNode, from: Bool) { + func updateTextContents(rects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)], textInputNode: ChatInputTextNode, from: Bool) { if !rects.isEmpty { let customEmojiContainerView: CustomEmojiContainerView if from, let current = self.fromCustomEmojiContainerView { @@ -460,7 +495,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.contentContainerNode.backgroundColor = self.presentationData.theme.contextMenu.backgroundColor - if let toAttributedText = self.textInputNode.attributedText?.mutableCopy() as? NSMutableAttributedString { + if let toAttributedText = self.textInputView.attributedText?.mutableCopy() as? NSMutableAttributedString { toAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: self.presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (toAttributedText.string as NSString).length)) self.toMessageTextNode.attributedText = toAttributedText } @@ -479,7 +514,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, return } - self.textInputNode.textView.setContentOffset(self.textInputNode.textView.contentOffset, animated: false) + self.textInputView.setContentOffset(self.textInputView.contentOffset, animated: false) self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -488,12 +523,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.sourceSendButton.isHidden = true if self.animateInputField { - self.fromMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.toMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) + self.fromMessageTextScrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.toMessageTextScrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) } else { self.messageBackgroundNode.isHidden = true - self.fromMessageTextNode.isHidden = true - self.toMessageTextNode.isHidden = true + self.fromMessageTextScrollView.isHidden = true + self.toMessageTextScrollView.isHidden = true } let duration = 0.4 @@ -501,7 +536,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.sendButtonNode.layer.animatePosition(from: self.sendButtonFrame.center, to: self.sendButtonNode.position, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) var initialWidth = self.textFieldFrame.width + 32.0 - if self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.attributedText.string.isEmpty { initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0) } @@ -529,12 +564,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageBackgroundNode.layer.animatePosition(from: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) var textXOffset: CGFloat = 0.0 - let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height - if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL { + let textYOffset = self.textInputView.contentSize.height - self.textInputView.contentOffset.y - self.textInputView.frame.height + if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { textXOffset = initialWidth - self.messageClipNode.bounds.width } - self.fromMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.toMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) @@ -545,7 +580,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, Queue.mainQueue().after(0.01, { if self.animateInputField { - self.textInputNode.isHidden = true + self.textInputView.isHidden = true } self.updateTextContents() }) @@ -569,7 +604,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, let intermediateCompletion: () -> Void = { [weak self] in if completedEffect && completedButton && completedBubble && completedAlpha && !completed { completed = true - self?.textInputNode.isHidden = false + self?.textInputView.isHidden = false self?.sourceSendButton.isHidden = false completion() } @@ -585,7 +620,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, Queue.mainQueue().after(0.45) { if !completed { completed = true - self.textInputNode.isHidden = false + self.textInputView.isHidden = false self.sourceSendButton.isHidden = false completion() } @@ -593,14 +628,14 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, if self.animateInputField { if cancel { - self.fromMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) - self.toMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) + self.fromMessageTextScrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) + self.toMessageTextScrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) self.messageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false, completion: { _ in completedAlpha = true intermediateCompletion() }) } else { - self.textInputNode.isHidden = false + self.textInputView.isHidden = false self.messageClipNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completedAlpha = true intermediateCompletion() @@ -623,11 +658,11 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } var initialWidth = self.textFieldFrame.width + 32.0 - if self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.attributedText.string.isEmpty { initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0) } - let toFrame = CGRect(origin: CGPoint(), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 1.0)) + let toFrame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 2.0)) let delta = (toFrame.height - self.messageClipNode.bounds.height) / 2.0 if cancel && self.animateInputField { @@ -641,7 +676,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, clipDelta -= self.contentContainerNode.frame.height + 16.0 } - self.messageClipNode.layer.animateBounds(from: self.messageClipNode.bounds, to: toFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + self.messageClipNode.layer.animateBounds(from: self.messageClipNode.bounds, to: toFrame.offsetBy(dx: 0.0, dy: 1.0), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in completedBubble = true intermediateCompletion() }) @@ -651,12 +686,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageBackgroundNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) var textXOffset: CGFloat = 0.0 - let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height - if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL { + let textYOffset = self.textInputView.contentSize.height - self.textInputView.contentOffset.y - self.textInputView.frame.height + if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { textXOffset = initialWidth - self.messageClipNode.bounds.width } - self.fromMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - self.toMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) } else { completedBubble = true } @@ -733,23 +768,26 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, messageFrame.size.width += 32.0 messageFrame.origin.x -= 13.0 messageFrame.origin.y = layout.size.height - messageFrame.origin.y - messageFrame.size.height - 1.0 + + let messageHeightAddition: CGFloat = max(0.0, 35.0 - messageFrame.size.height) + if inputHeight.isZero || layout.isNonExclusive { messageFrame.origin.y += menuHeightWithInset } - if self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.attributedText.string.isEmpty { messageFrame.size.width = ceil(layout.size.width - messageFrame.origin.x - sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 8.0) } var messageOriginDelta: CGFloat = 0.0 - if self.textInputNode.textView.numberOfLines == 1 || self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.numberOfLines == 1 || self.textInputView.attributedText.string.isEmpty { let textWidth = min(self.toMessageTextNode.textView.sizeThatFits(layout.size).width + 36.0, messageFrame.width) messageOriginDelta = messageFrame.width - textWidth messageFrame.origin.x += messageOriginDelta messageFrame.size.width = textWidth } - let messageHeight = max(messageFrame.size.height, self.textInputNode.textView.contentSize.height + 2.0) + let messageHeight = max(messageFrame.size.height, self.textInputView.contentSize.height + 2.0) messageFrame.size.height = messageHeight transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -763,20 +801,38 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, let clipFrame = messageFrame transition.updateFrame(node: self.messageClipNode, frame: clipFrame) - let backgroundFrame = CGRect(origin: CGPoint(), size: messageFrame.size) + var backgroundFrame = CGRect(origin: CGPoint(), size: messageFrame.size) + backgroundFrame.origin.y -= messageHeightAddition * 0.5 + backgroundFrame.size.height += messageHeightAddition transition.updateFrame(node: self.messageBackgroundNode, frame: backgroundFrame) var textFrame = self.textFieldFrame textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel) - textFrame.size.height = self.textInputNode.textView.contentSize.height - textFrame.size.width -= self.textInputNode.textContainerInset.right + textFrame.size.height = self.textInputView.contentSize.height - if self.textInputNode.isRTL { + if let textInputView = self.textInputView as? ChatInputTextView { + textFrame.origin.y -= 5.0 + + self.fromMessageTextNode.textView.defaultTextContainerInset = textInputView.defaultTextContainerInset + self.toMessageTextNode.textView.defaultTextContainerInset = textInputView.defaultTextContainerInset + } + /*if let textInputView = self.textInputView as? ChatInputTextView { + textFrame.size.width -= textInputView.defaultTextContainerInset.right + } else { + textFrame.size.width -= self.textInputView.textContainerInset.right + }*/ + + if self.textInputView.isRTL { textFrame.origin.x -= messageOriginDelta } - self.fromMessageTextNode.frame = textFrame - self.toMessageTextNode.frame = textFrame + self.fromMessageTextScrollView.frame = textFrame + self.fromMessageTextNode.frame = CGRect(origin: CGPoint(), size: textFrame.size) + self.fromMessageTextNode.updateLayout(size: textFrame.size) + + self.toMessageTextScrollView.frame = textFrame + self.toMessageTextNode.frame = CGRect(origin: CGPoint(), size: textFrame.size) + self.toMessageTextNode.updateLayout(size: textFrame.size) } @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/ComponentFlow/Source/Components/HStack.swift b/submodules/ComponentFlow/Source/Components/HStack.swift index 81b4e628cc8..c174e77d013 100644 --- a/submodules/ComponentFlow/Source/Components/HStack.swift +++ b/submodules/ComponentFlow/Source/Components/HStack.swift @@ -41,7 +41,8 @@ public final class HStack: CombinedComponent { size.width += child.size.width size.height = max(size.height, child.size.height) } - + size.width += context.component.spacing * CGFloat(updatedChildren.count - 1) + var nextX = 0.0 for child in updatedChildren { context.add(child diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index 6f32ccab3db..5517a6b0715 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -46,9 +46,10 @@ public final class Image: Component { func update(component: Image, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { self.image = component.image - self.tintColor = component.tintColor self.contentMode = component.contentMode + transition.setTintColor(view: self, color: component.tintColor ?? .white) + return component.size ?? availableSize } } diff --git a/submodules/ComponentFlow/Source/Components/List.swift b/submodules/ComponentFlow/Source/Components/List.swift index a6ac0f60890..2b8601496fa 100644 --- a/submodules/ComponentFlow/Source/Components/List.swift +++ b/submodules/ComponentFlow/Source/Components/List.swift @@ -11,11 +11,13 @@ public final class List: CombinedComponent { private let items: [AnyComponentWithIdentity] private let direction: Direction + private let centerAlignment: Bool private let appear: Transition.Appear - public init(_ items: [AnyComponentWithIdentity], direction: Direction = .vertical, appear: Transition.Appear = .default()) { + public init(_ items: [AnyComponentWithIdentity], direction: Direction = .vertical, centerAlignment: Bool = false, appear: Transition.Appear = .default()) { self.items = items self.direction = direction + self.centerAlignment = centerAlignment self.appear = appear } @@ -26,6 +28,9 @@ public final class List: CombinedComponent { if lhs.direction != rhs.direction { return false } + if lhs.centerAlignment != rhs.centerAlignment { + return false + } return true } @@ -42,6 +47,10 @@ public final class List: CombinedComponent { transition: context.transition ) } + + let maxWidth: CGFloat = updatedChildren.reduce(CGFloat(0.0)) { partialResult, child in + return max(partialResult, child.size.width) + } var nextOrigin: CGFloat = 0.0 for child in updatedChildren { @@ -51,7 +60,13 @@ public final class List: CombinedComponent { position = CGPoint(x: nextOrigin + child.size.width / 2.0, y: child.size.height / 2.0) nextOrigin += child.size.width case .vertical: - position = CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0) + let originX: CGFloat + if context.component.centerAlignment { + originX = maxWidth / 2.0 + } else { + originX = child.size.width / 2.0 + } + position = CGPoint(x: originX, y: nextOrigin + child.size.height / 2.0) nextOrigin += child.size.height } context.add(child @@ -63,8 +78,14 @@ public final class List: CombinedComponent { switch context.component.direction { case .horizontal: return CGSize(width: min(context.availableSize.width, nextOrigin), height: context.availableSize.height) - case.vertical: - return CGSize(width: context.availableSize.width, height: min(context.availableSize.height, nextOrigin)) + case .vertical: + let width: CGFloat + if context.component.centerAlignment { + width = maxWidth + } else { + width = context.availableSize.width + } + return CGSize(width: width, height: min(context.availableSize.height, nextOrigin)) } } } diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index 9fc9e1447ee..03743f5b6b0 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -224,6 +224,9 @@ public final class LottieAnimationComponent: Component { if updateColors, let animationView = self.animationView { if let value = component.colors["__allcolors__"] { + for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Colors" }) { + animationView.setValueProvider(GradientValueProvider([value.lottieColorValue, value.lottieColorValue]), keypath: AnimationKeypath(keypath: keypath)) + } for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Color" }) { animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath)) } diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 6cf3f9b4596..632084e1ff5 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -23,6 +23,7 @@ public final class ReactionIconView: PortalSourceView { private var file: TelegramMediaFile? private var animationCache: AnimationCache? private var animationRenderer: MultiAnimationRenderer? + private var contentTintColor: UIColor? private var placeholderColor: UIColor? private var size: CGSize? private var animateIdle: Bool? @@ -58,6 +59,7 @@ public final class ReactionIconView: PortalSourceView { fileId: Int64, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, + tintColor: UIColor?, placeholderColor: UIColor, animateIdle: Bool, reaction: MessageReaction.Reaction, @@ -66,6 +68,7 @@ public final class ReactionIconView: PortalSourceView { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer + self.contentTintColor = tintColor self.placeholderColor = placeholderColor self.size = size self.animateIdle = animateIdle @@ -112,6 +115,8 @@ public final class ReactionIconView: PortalSourceView { transition.updateFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) } + + self.updateTintColor() } public func updateIsAnimationHidden(isAnimationHidden: Bool, transition: ContainedViewLayoutTransition) { @@ -170,6 +175,36 @@ public final class ReactionIconView: PortalSourceView { animationLayer.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) animationLayer.isVisibleForAnimations = animateIdle && context.sharedContext.energyUsageSettings.loopEmoji + self.updateTintColor() + } + + private func updateTintColor() { + guard let file = self.file, let animationLayer = self.animationLayer else { + return + } + var accentTint = false + if file.isCustomTemplateEmoji { + accentTint = true + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + accentTint = true + } + default: + break + } + } + } + if accentTint, let tintColor = self.contentTintColor { + animationLayer.contentTintColor = tintColor + animationLayer.dynamicColor = tintColor + } else { + animationLayer.contentTintColor = nil + animationLayer.dynamicColor = nil + } } func reset() { @@ -769,6 +804,17 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { animateIdle = false } + let tintColor: UIColor + if layout.backgroundLayout.colors.isSelected { + if layout.spec.component.colors.selectedForeground != 0 { + tintColor = UIColor(argb: layout.spec.component.colors.selectedForeground) + } else { + tintColor = .white + } + } else { + tintColor = UIColor(argb: layout.spec.component.colors.deselectedForeground) + } + iconView.update( size: layout.imageFrame.size, context: layout.spec.component.context, @@ -776,6 +822,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { fileId: fileId, animationCache: arguments.animationCache, animationRenderer: arguments.animationRenderer, + tintColor: tintColor, placeholderColor: layout.spec.component.chosenOrder != nil ? UIColor(argb: layout.spec.component.colors.selectedMediaPlaceholder) : UIColor(argb: layout.spec.component.colors.deselectedMediaPlaceholder), animateIdle: animateIdle, reaction: layout.spec.component.reaction.value, diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 777a849ebc9..6c1e3327661 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -218,6 +218,37 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } reactionLayer.frame = iconFrame } + + self.updateReactionAccentColor() + } + + func updateReactionAccentColor() { + guard let file = self.file, let reactionLayer = self.reactionLayer, let theme = self.theme else { + return + } + var accentTint = false + if file.isCustomTemplateEmoji { + accentTint = true + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + accentTint = true + } + default: + break + } + } + } + if accentTint { + reactionLayer.contentTintColor = theme.contextMenu.badgeFillColor + reactionLayer.dynamicColor = theme.contextMenu.badgeFillColor + } else { + reactionLayer.contentTintColor = nil + reactionLayer.dynamicColor = nil + } } func update(presentationData: PresentationData, constrainedSize: CGSize, isSelected: Bool) -> CGSize { @@ -227,6 +258,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if let iconNode = self.iconNode { iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.contextMenu.primaryColor) } + + self.updateReactionLayer() } let sideInset: CGFloat = 12.0 @@ -478,6 +511,35 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } + func updateReactionAccentColor(theme: PresentationTheme) { + guard let file = self.file, let reactionLayer = self.reactionLayer else { + return + } + var accentTint = false + if file.isCustomTemplateEmoji { + accentTint = true + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + accentTint = true + } + default: + break + } + } + } + if accentTint { + reactionLayer.contentTintColor = theme.contextMenu.badgeFillColor + reactionLayer.dynamicColor = theme.contextMenu.badgeFillColor + } else { + reactionLayer.contentTintColor = nil + reactionLayer.dynamicColor = nil + } + } + func update(size: CGSize, presentationData: PresentationData, item: EngineMessageReactionListContext.Item, isLast: Bool, syncronousLoad: Bool) { let avatarInset: CGFloat = 12.0 let avatarSpacing: CGFloat = 8.0 @@ -495,6 +557,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if availableReaction.value == reaction { self.file = availableReaction.centerAnimation self.updateReactionLayer() + self.updateReactionAccentColor(theme: presentationData.theme) break } } @@ -507,6 +570,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } strongSelf.file = file strongSelf.updateReactionLayer() + strongSelf.updateReactionAccentColor(theme: presentationData.theme) }).strict() } } else { @@ -520,6 +584,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } } + self.updateReactionAccentColor(theme: presentationData.theme) if self.item != item { self.item = item diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index d4cc02c048c..cb9cb28dbe5 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -37,9 +37,18 @@ public final class SheetComponentEnvironment: Equatable { } } +public let sheetComponentTag = GenericComponentViewTag() public final class SheetComponent: Component { public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment) + public class ExternalState { + public fileprivate(set) var contentHeight: CGFloat + + public init() { + self.contentHeight = 0.0 + } + } + public enum BackgroundColor: Equatable { public enum BlurStyle: Equatable { case light @@ -52,15 +61,21 @@ public final class SheetComponent: Component { public let content: AnyComponent public let backgroundColor: BackgroundColor + public let followContentSizeChanges: Bool + public let externalState: ExternalState? public let animateOut: ActionSlot> public init( content: AnyComponent, backgroundColor: BackgroundColor, + followContentSizeChanges: Bool = false, + externalState: ExternalState? = nil, animateOut: ActionSlot> ) { self.content = content self.backgroundColor = backgroundColor + self.followContentSizeChanges = followContentSizeChanges + self.externalState = externalState self.animateOut = animateOut } @@ -71,6 +86,9 @@ public final class SheetComponent: Component { if lhs.backgroundColor != rhs.backgroundColor { return false } + if lhs.followContentSizeChanges != rhs.followContentSizeChanges { + return false + } if lhs.animateOut != rhs.animateOut { return false } @@ -90,12 +108,24 @@ public final class SheetComponent: Component { } } - public final class View: UIView, UIScrollViewDelegate { + public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { + public final class Tag { + public init() { + } + } + + public func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + private let dimView: UIView private let scrollView: ScrollView private let backgroundView: UIView private var effectView: UIVisualEffectView? - private let contentView: ComponentHostView + private let contentView: ComponentView private var isAnimatingOut: Bool = false private var previousIsDisplaying: Bool = false @@ -120,7 +150,7 @@ public final class SheetComponent: Component { self.backgroundView.layer.cornerRadius = 12.0 self.backgroundView.layer.masksToBounds = true - self.contentView = ComponentHostView() + self.contentView = ComponentView() super.init(frame: frame) @@ -128,7 +158,6 @@ public final class SheetComponent: Component { self.addSubview(self.dimView) self.scrollView.addSubview(self.backgroundView) - self.scrollView.addSubview(self.contentView) self.addSubview(self.scrollView) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:)))) @@ -159,6 +188,10 @@ public final class SheetComponent: Component { } } + public func dismissAnimated() { + self.dismiss?(true) + } + private var scrollingOut = false public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0 @@ -228,18 +261,21 @@ public final class SheetComponent: Component { self.isUserInteractionEnabled = false self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + guard let contentView = self.contentView.view else { + return + } if let initialVelocity = initialVelocity { let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) let contentOffset = (self.scrollView.contentOffset.y + self.scrollView.contentInset.top - self.scrollView.contentSize.height) * -1.0 - let dismissalOffset = self.scrollView.contentSize.height + abs(self.contentView.frame.minY) + let dismissalOffset = self.scrollView.contentSize.height + abs(contentView.frame.minY) let delta = dismissalOffset - contentOffset transition.updatePosition(layer: self.scrollView.layer, position: CGPoint(x: self.scrollView.center.x, y: self.scrollView.center.y + delta), completion: { _ in completion() }) } else { - self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.scrollView.contentSize.height + abs(self.contentView.frame.minY)), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.scrollView.contentSize.height + abs(contentView.frame.minY)), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in completion() }) } @@ -293,6 +329,7 @@ public final class SheetComponent: Component { containerSize = CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) } + self.contentView.parentState = state let contentSize = self.contentView.update( transition: transition, component: component.content, @@ -301,31 +338,39 @@ public final class SheetComponent: Component { }, containerSize: containerSize ) + component.externalState?.contentHeight = contentSize.height self.ignoreScrolling = true - - if sheetEnvironment.isCentered { - let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) - if let effectView = self.effectView { - transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + if let contentView = self.contentView.view { + if contentView.superview == nil { + self.scrollView.addSubview(contentView) } - } else { - transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) - if let effectView = self.effectView { - transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + if sheetEnvironment.isCentered { + let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + if let effectView = self.effectView { + transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + } + } else { + transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + if let effectView = self.effectView { + transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + } } } transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) + let previousContentSize = self.scrollView.contentSize self.scrollView.contentSize = contentSize self.scrollView.contentInset = UIEdgeInsets(top: max(0.0, availableSize.height - contentSize.height) + contentSize.height, left: 0.0, bottom: 0.0, right: 0.0) self.ignoreScrolling = false if let currentAvailableSize = self.currentAvailableSize, currentAvailableSize.height != availableSize.height { self.scrollView.contentOffset = CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)) + } else if component.followContentSizeChanges, !previousContentSize.height.isZero, previousContentSize != contentSize { + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: availableSize)) } if self.currentHasInputHeight != previousHasInputHeight { transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: self.scrollView.bounds.size)) diff --git a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift index fc8bc56aab6..91d354d021a 100644 --- a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift @@ -294,11 +294,11 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe if let currentText = strongSelf.textNode.attributedText { if currentText.string != attributedText.string || updatedTheme != nil { strongSelf.textNode.attributedText = attributedText - refreshGenericTextInputAttributes(strongSelf.textNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) } } else { strongSelf.textNode.attributedText = attributedText - refreshGenericTextInputAttributes(strongSelf.textNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) } if strongSelf.backgroundNode.supernode == nil { @@ -577,7 +577,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let item = self.item { if let _ = self.textNode.attributedText { - refreshGenericTextInputAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) let updatedText = stateAttributedStringForText(self.textNode.attributedText!) item.textUpdated(updatedText) } else { @@ -606,8 +606,8 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe UIMenuController.shared.update() } - refreshChatTextInputTypingAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshChatTextInputTypingAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0) + refreshGenericTextInputAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) } } @@ -657,8 +657,8 @@ private func chatTextInputAddFormattingAttribute(item: CreatePollTextInputItem, textNode.attributedText = result textNode.selectedRange = nsRange - refreshChatTextInputTypingAttributes(textNode, theme: theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(textNode, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshChatTextInputTypingAttributes(textNode.textView, theme: theme, baseFontSize: 17.0) + refreshGenericTextInputAttributes(textNode.textView, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) let updatedText = stateAttributedStringForText(textNode.attributedText!) item.textUpdated(updatedText) diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 9bc2728322b..d1cbb233b5d 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -1085,15 +1085,18 @@ public final class ContactListNode: ASDisplayNode { var existingNormalizedPhoneNumbers = Set() var excludeSelf = false + var requirePhoneNumbers = false for filter in filters { switch filter { - case .excludeSelf: - excludeSelf = true - existingPeerIds.insert(context.account.peerId) - case let .exclude(peerIds): - existingPeerIds = existingPeerIds.union(peerIds) - case let .disable(peerIds): - disabledPeerIds = disabledPeerIds.union(peerIds) + case .excludeSelf: + excludeSelf = true + existingPeerIds.insert(context.account.peerId) + case let .exclude(peerIds): + existingPeerIds = existingPeerIds.union(peerIds) + case let .disable(peerIds): + disabledPeerIds = disabledPeerIds.union(peerIds) + case .excludeWithoutPhoneNumbers: + requirePhoneNumbers = true } } @@ -1127,8 +1130,13 @@ public final class ContactListNode: ASDisplayNode { } for peer in remotePeers.0 { let matches: Bool - if peer.peer is TelegramUser { - matches = true + if let user = peer.peer as? TelegramUser { + let phone = user.phone ?? "" + if requirePhoneNumbers && phone.isEmpty { + matches = false + } else { + matches = true + } } else if searchGroups || searchChannels { if peer.peer is TelegramGroup && searchGroups { matches = true @@ -1157,8 +1165,13 @@ public final class ContactListNode: ASDisplayNode { } for peer in remotePeers.1 { let matches: Bool - if peer.peer is TelegramUser { - matches = true + if let user = peer.peer as? TelegramUser { + let phone = user.phone ?? "" + if requirePhoneNumbers && phone.isEmpty { + matches = false + } else { + matches = true + } } else if searchGroups || searchChannels { if peer.peer is TelegramGroup { matches = searchGroups @@ -1270,23 +1283,32 @@ public final class ContactListNode: ASDisplayNode { } var existingPeerIds = Set() var disabledPeerIds = Set() + var requirePhoneNumbers = false for filter in filters { switch filter { - case .excludeSelf: - existingPeerIds.insert(context.account.peerId) - case let .exclude(peerIds): - existingPeerIds = existingPeerIds.union(peerIds) - case let .disable(peerIds): - disabledPeerIds = disabledPeerIds.union(peerIds) + case .excludeSelf: + existingPeerIds.insert(context.account.peerId) + case let .exclude(peerIds): + existingPeerIds = existingPeerIds.union(peerIds) + case let .disable(peerIds): + disabledPeerIds = disabledPeerIds.union(peerIds) + case .excludeWithoutPhoneNumbers: + requirePhoneNumbers = true } } peers = peers.filter { contact in switch contact { - case let .peer(peer, _, _): - return !existingPeerIds.contains(peer.id) - default: - return true + case let .peer(peer, _, _): + if requirePhoneNumbers, let user = peer as? TelegramUser { + let phone = user.phone ?? "" + if phone.isEmpty { + return false + } + } + return !existingPeerIds.contains(peer.id) + default: + return true } } diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index 3333f44ac5f..90972a156b4 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -322,6 +322,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo var entries: [ContactListSearchEntry] = [] var existingPeerIds = Set() var disabledPeerIds = Set() + var requirePhoneNumbers = false for filter in filters { switch filter { case .excludeSelf: @@ -330,6 +331,8 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo existingPeerIds = existingPeerIds.union(peerIds) case let .disable(peerIds): disabledPeerIds = disabledPeerIds.union(peerIds) + case .excludeWithoutPhoneNumbers: + requirePhoneNumbers = true } } var existingNormalizedPhoneNumbers = Set() @@ -338,6 +341,14 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if existingPeerIds.contains(peer.id) { continue } + + if case let .user(user) = peer, requirePhoneNumbers { + let phone = user.phone ?? "" + if phone.isEmpty { + continue + } + } + existingPeerIds.insert(peer.id) var enabled = true if onlyWriteable { @@ -354,6 +365,14 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if !(peer.peer is TelegramUser) { continue } + + if let user = peer.peer as? TelegramUser, requirePhoneNumbers { + let phone = user.phone ?? "" + if phone.isEmpty { + continue + } + } + if !existingPeerIds.contains(peer.peer.id) { existingPeerIds.insert(peer.peer.id) @@ -373,6 +392,14 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if !(peer.peer is TelegramUser) { continue } + + if let user = peer.peer as? TelegramUser, requirePhoneNumbers { + let phone = user.phone ?? "" + if phone.isEmpty { + continue + } + } + if !existingPeerIds.contains(peer.peer.id) { existingPeerIds.insert(peer.peer.id) diff --git a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift index 572a01cba4f..579c98cf807 100644 --- a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift @@ -56,7 +56,7 @@ private enum InviteContactsEntry: Comparable, Identifiable { } else { status = .none } - let peer: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil)) + let peer: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil)) return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.toggleContact(id) }) diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index 6d9b1e33e8a..a9e9d78fa6e 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -25,6 +25,10 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/UndoUI:UndoUI", "//submodules/AnimationUI:AnimationUI", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index aa8489a6e15..59f89de751b 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -393,7 +393,18 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { var icon: UIImage? switch tip { case .textSelection: - var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip + var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip2 + if let range = rawText.range(of: "|") { + rawText.removeSubrange(range) + self.text = rawText + self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound + } else { + self.text = rawText + self.targetSelectionIndex = 1 + } + icon = UIImage(bundleImageName: "Chat/Context Menu/Tip") + case .quoteSelection: + var rawText = presentationData.strings.ChatContextMenu_QuoteSelectionTip if let range = rawText.range(of: "|") { rawText.removeSubrange(range) self.text = rawText @@ -446,7 +457,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { self.highlightBackgroundNode.clipsToBounds = true self.highlightBackgroundNode.cornerRadius = 14.0 - let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0), strings: presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { _ in + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0, isDark: presentationData.theme.overallDarkAppearance), strings: presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { _ in }, present: { _, _ in }, rootNode: { [weak self] in return self diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index be14401569b..96f7c72a878 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -22,9 +22,10 @@ public protocol ContextControllerProtocol: ViewController { var getOverlayViews: (() -> [UIView])? { get set } func dismiss(completion: (() -> Void)?) + func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) func getActionsMinHeight() -> ContextController.ActionsHeight? - func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?) + func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) func pushItems(items: Signal) func popItems() @@ -111,6 +112,14 @@ public final class ContextMenuActionItem { self.updateAction = updateAction } } + + public struct IconAnimation: Equatable { + public var name: String + + public init(name: String) { + self.name = name + } + } public let id: AnyHashable? public let text: String @@ -124,6 +133,7 @@ public final class ContextMenuActionItem { public let iconSource: ContextMenuActionItemIconSource? public let iconPosition: ContextMenuActionItemIconPosition public let animationName: String? + public let iconAnimation: IconAnimation? public let textIcon: (PresentationTheme) -> UIImage? public let textLinkAction: () -> Void public let action: ((Action) -> Void)? @@ -141,6 +151,7 @@ public final class ContextMenuActionItem { iconSource: ContextMenuActionItemIconSource? = nil, iconPosition: ContextMenuActionItemIconPosition = .right, animationName: String? = nil, + iconAnimation: IconAnimation? = nil, textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil }, textLinkAction: @escaping () -> Void = {}, action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)? @@ -158,6 +169,7 @@ public final class ContextMenuActionItem { iconSource: iconSource, iconPosition: iconPosition, animationName: animationName, + iconAnimation: iconAnimation, textIcon: textIcon, textLinkAction: textLinkAction, action: action.flatMap { action in @@ -181,6 +193,7 @@ public final class ContextMenuActionItem { iconSource: ContextMenuActionItemIconSource? = nil, iconPosition: ContextMenuActionItemIconPosition = .right, animationName: String? = nil, + iconAnimation: IconAnimation? = nil, textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil }, textLinkAction: @escaping () -> Void = {}, action: ((Action) -> Void)? @@ -197,6 +210,7 @@ public final class ContextMenuActionItem { self.iconSource = iconSource self.iconPosition = iconPosition self.animationName = animationName + self.iconAnimation = iconAnimation self.textIcon = textIcon self.textLinkAction = textLinkAction self.action = action @@ -232,14 +246,19 @@ func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> return targetWindowFrame } -private final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { +final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private weak var controller: ContextController? private var presentationData: PresentationData - private let source: ContextContentSource - private var items: Signal - private let beginDismiss: (ContextMenuActionResult) -> Void + + private let configuration: ContextController.Configuration + + private let legacySource: ContextContentSource + private var legacyItems: Signal + + let beginDismiss: (ContextMenuActionResult) -> Void private let beganAnimatingOut: () -> Void private let attemptTransitionControllerIntoNavigation: () -> Void - fileprivate var dismissedForCancel: (() -> Void)? + var dismissedForCancel: (() -> Void)? private let getController: () -> ContextControllerProtocol? private weak var gesture: ContextGesture? @@ -260,8 +279,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let dismissNode: ASDisplayNode private let dismissAccessibilityArea: AccessibilityAreaNode - private var presentationNode: ContextControllerPresentationNode? - private var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition? + private var sourceContainer: ContextSourceContainer? private let clippingNode: ASDisplayNode private let scrollNode: ASScrollNode @@ -288,32 +306,33 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let blurBackground: Bool var overlayWantsToBeBelowKeyboard: Bool { - if let presentationNode = self.presentationNode { - return presentationNode.wantsDisplayBelowKeyboard() - } else { + guard let sourceContainer = self.sourceContainer else { return false } + return sourceContainer.overlayWantsToBeBelowKeyboard } init( controller: ContextController, presentationData: PresentationData, - source: ContextContentSource, - items: Signal, + configuration: ContextController.Configuration, beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void ) { + self.controller = controller self.presentationData = presentationData - self.source = source - self.items = items + self.configuration = configuration self.beginDismiss = beginDismiss self.beganAnimatingOut = beganAnimatingOut self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation self.gesture = gesture + self.legacySource = configuration.sources[0].source + self.legacyItems = configuration.sources[0].items + self.getController = { [weak controller] in return controller } @@ -359,10 +378,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var updateLayout: (() -> Void)? var blurBackground = true - if case .reference = source { - blurBackground = false - } else if case let .extracted(extractedSource) = source, !extractedSource.blurBackground { - blurBackground = false + if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { + if case .reference = mainSource.source { + blurBackground = false + } else if case let .extracted(extractedSource) = mainSource.source, !extractedSource.blurBackground { + blurBackground = false + } } self.blurBackground = blurBackground @@ -423,9 +444,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } if strongSelf.didMoveFromInitialGesturePoint { - if let presentationNode = strongSelf.presentationNode { - let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view) - presentationNode.highlightGestureMoved(location: presentationPoint, hover: false) + if let sourceContainer = strongSelf.sourceContainer { + let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) + sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) } else { let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) @@ -447,8 +468,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } recognizer.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { - if let presentationNode = strongSelf.presentationNode { - presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil) + if let sourceContainer = strongSelf.sourceContainer { + sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) } else { if let (_, _) = viewAndPoint { if let highlightedActionNode = strongSelf.highlightedActionNode { @@ -485,9 +506,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } if strongSelf.didMoveFromInitialGesturePoint { - if let presentationNode = strongSelf.presentationNode { - let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view) - presentationNode.highlightGestureMoved(location: presentationPoint, hover: false) + if let sourceContainer = strongSelf.sourceContainer { + let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) + sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) } else { let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) @@ -513,8 +534,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } gesture.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { - if let presentationNode = strongSelf.presentationNode { - presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil) + if let sourceContainer = strongSelf.sourceContainer { + sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) } else { if let (_, _) = viewAndPoint { if let highlightedActionNode = strongSelf.highlightedActionNode { @@ -532,20 +553,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } - switch source { - case .location, .reference, .extracted: - self.contentReady.set(.single(true)) - case let .controller(source): - self.contentReady.set(source.controller.ready.get()) - } - self.initializeContent() - self.itemsDisposable.set((items - |> deliverOnMainQueue).start(next: { [weak self] items in - self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) - })) - self.dismissAccessibilityArea.activate = { [weak self] in self?.dimNodeTapped() return true @@ -591,9 +600,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi switch gestureRecognizer.state { case .changed: - if let presentationNode = self.presentationNode { - let presentationPoint = self.view.convert(localPoint, to: presentationNode.view) - presentationNode.highlightGestureMoved(location: presentationPoint, hover: true) + if let sourceContainer = self.sourceContainer { + let presentationPoint = self.view.convert(localPoint, to: sourceContainer.view) + sourceContainer.highlightGestureMoved(location: presentationPoint, hover: true) } else { let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view) let actionNode = self.actionsContainerNode.actionNode(at: actionPoint) @@ -606,8 +615,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } case .ended, .cancelled: - if let presentationNode = self.presentationNode { - presentationNode.highlightGestureMoved(location: CGPoint(x: -1, y: -1), hover: true) + if let sourceContainer = self.sourceContainer { + sourceContainer.highlightGestureMoved(location: CGPoint(x: -1, y: -1), hover: true) } else { if let highlightedActionNode = self.highlightedActionNode { self.highlightedActionNode = nil @@ -620,217 +629,86 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } private func initializeContent() { - switch self.source { - case let .location(source): - let presentationNode = ContextControllerExtractedPresentationNode( - getController: { [weak self] in - return self?.getController() - }, - requestUpdate: { [weak self] transition in - guard let strongSelf = self else { - return - } + if self.configuration.sources.count == 1 { + switch self.configuration.sources[0].source { + case .location: + break + case let .reference(source): + if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation { + self.contentReady.set(.single(true)) - if let validLayout = strongSelf.validLayout { - strongSelf.updateLayout( - layout: validLayout, - transition: transition, - previousActionsContainerNode: nil - ) - } - }, - requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in - guard let strongSelf = self else { - return - } - if let controller = strongSelf.getController() { - controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) - } - }, - requestDismiss: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.dismissedForCancel?() - strongSelf.beginDismiss(result) - }, - requestAnimateOut: { [weak self] result, completion in - guard let strongSelf = self else { - return - } - strongSelf.animateOut(result: result, completion: completion) - }, - source: .location(source) - ) - self.presentationNode = presentationNode - self.addSubnode(presentationNode) - case let .reference(source): - if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation { - let transitionInfo = source.transitionInfo() - if let transitionInfo = transitionInfo { - let referenceView = transitionInfo.referenceView - self.contentContainerNode.contentNode = .reference(view: referenceView) - self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace - self.customPosition = transitionInfo.customPosition - var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) - projectedFrame.origin.x += transitionInfo.insets.left - projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right - projectedFrame.origin.y += transitionInfo.insets.top - projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) - } - } else { - let presentationNode = ContextControllerExtractedPresentationNode( - getController: { [weak self] in - return self?.getController() - }, - requestUpdate: { [weak self] transition in - guard let strongSelf = self else { - return - } - if let validLayout = strongSelf.validLayout { - strongSelf.updateLayout( - layout: validLayout, - transition: transition, - previousActionsContainerNode: nil - ) - } - }, - requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in - guard let strongSelf = self else { - return - } - if let controller = strongSelf.getController() { - controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) - } - }, - requestDismiss: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.dismissedForCancel?() - strongSelf.beginDismiss(result) - }, - requestAnimateOut: { [weak self] result, completion in - guard let strongSelf = self else { - return - } - strongSelf.animateOut(result: result, completion: completion) - }, - source: .reference(source) - ) - self.presentationNode = presentationNode - self.addSubnode(presentationNode) - } - case let .extracted(source): - let presentationNode = ContextControllerExtractedPresentationNode( - getController: { [weak self] in - return self?.getController() - }, - requestUpdate: { [weak self] transition in - guard let strongSelf = self else { - return - } - if let validLayout = strongSelf.validLayout { - strongSelf.updateLayout( - layout: validLayout, - transition: transition, - previousActionsContainerNode: nil - ) - } - }, - requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in - guard let strongSelf = self else { - return - } - if let controller = strongSelf.getController() { - controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) - } - }, - requestDismiss: { [weak self] result in - guard let strongSelf = self else { - return - } - strongSelf.dismissedForCancel?() - strongSelf.beginDismiss(result) - }, - requestAnimateOut: { [weak self] result, completion in - guard let strongSelf = self else { - return + let transitionInfo = source.transitionInfo() + if let transitionInfo = transitionInfo { + let referenceView = transitionInfo.referenceView + self.contentContainerNode.contentNode = .reference(view: referenceView) + self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace + self.customPosition = transitionInfo.customPosition + var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) + projectedFrame.origin.x += transitionInfo.insets.left + projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right + projectedFrame.origin.y += transitionInfo.insets.top + projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) } - strongSelf.animateOut(result: result, completion: completion) - }, - source: .extracted(source) - ) - self.presentationNode = presentationNode - self.addSubnode(presentationNode) - /*let takenViewInfo = source.takeView() - - if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode { - self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace) - if source.keepInPlace || takenViewInfo.maskView != nil { - self.clippingNode.view.mask = takenViewInfo.maskView - self.clippingNode.addSubnode(self.contentContainerNode) - } else { - self.scrollNode.addSubnode(self.contentContainerNode) + + self.itemsDisposable.set((self.configuration.sources[0].items + |> deliverOnMainQueue).start(next: { [weak self] items in + self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) + })) + + return } - let contentParentNode = takenViewInfo.contentContainingNode - takenViewInfo.contentContainingNode.layoutUpdated = { [weak contentParentNode, weak self] size in - guard let strongSelf = self, let contentParentNode = contentParentNode, let parentSupernode = contentParentNode.supernode else { - return - } - if strongSelf.isAnimatingOut { - return - } - strongSelf.originalProjectedContentViewFrame = (convertFrame(contentParentNode.frame, from: parentSupernode.view, to: strongSelf.view), convertFrame(contentParentNode.contentRect, from: contentParentNode.view, to: strongSelf.view)) - if let validLayout = strongSelf.validLayout { - strongSelf.updateLayout(layout: validLayout, transition: .animated(duration: 0.2 * animationDurationFactor, curve: .easeInOut), previousActionsContainerNode: nil) + case .extracted: + break + case let .controller(source): + if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation { + self.contentReady.set(source.controller.ready.get()) + + let transitionInfo = source.transitionInfo() + if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { + let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in + self?.attemptTransitionControllerIntoNavigation() + }) + self.contentContainerNode.contentNode = .controller(contentParentNode) + self.scrollNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.clipsToBounds = true + self.contentContainerNode.cornerRadius = 14.0 + self.contentContainerNode.addSubnode(contentParentNode) + + let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) + self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) } + + self.itemsDisposable.set((self.configuration.sources[0].items + |> deliverOnMainQueue).start(next: { [weak self] items in + self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) + })) + + return } - - self.contentAreaInScreenSpace = takenViewInfo.contentAreaInScreenSpace - self.contentContainerNode.addSubnode(takenViewInfo.contentContainingNode.contentNode) - takenViewInfo.contentContainingNode.isExtractedToContextPreview = true - takenViewInfo.contentContainingNode.isExtractedToContextPreviewUpdated?(true) - - self.originalProjectedContentViewFrame = (convertFrame(takenViewInfo.contentContainingNode.frame, from: parentSupernode.view, to: self.view), convertFrame(takenViewInfo.contentContainingNode.contentRect, from: takenViewInfo.contentContainingNode.view, to: self.view)) - }*/ - case let .controller(source): - let transitionInfo = source.transitionInfo() - if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { - let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in - self?.attemptTransitionControllerIntoNavigation() - }) - self.contentContainerNode.contentNode = .controller(contentParentNode) - self.scrollNode.addSubnode(self.contentContainerNode) - self.contentContainerNode.clipsToBounds = true - self.contentContainerNode.cornerRadius = 14.0 - self.contentContainerNode.addSubnode(contentParentNode) - - let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) - self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) } } + + if let controller = self.controller { + let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration) + self.contentReady.set(sourceContainer.ready.get()) + self.itemsReady.set(.single(true)) + self.sourceContainer = sourceContainer + self.addSubnode(sourceContainer) + } } func animateIn() { self.gesture?.endPressedAppearance() self.hapticFeedback.impact() - if let _ = self.presentationNode { + if let sourceContainer = self.sourceContainer { self.didCompleteAnimationIn = true - self.currentPresentationStateTransition = .animateIn - if let validLayout = self.validLayout { - self.updateLayout( - layout: validLayout, - transition: .animated(duration: 0.5, curve: .spring), - previousActionsContainerNode: nil - ) - } + sourceContainer.animateIn() return } - switch self.source { + switch self.legacySource { case .location, .reference: break case .extracted: @@ -920,7 +798,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi case let .extracted(extracted, keepInPlace): let springDuration: Double = 0.42 * animationDurationFactor var springDamping: CGFloat = 104.0 - if case let .extracted(source) = self.source, source.centerVertically { + if case let .extracted(source) = self.legacySource, source.centerVertically { springDamping = 124.0 } @@ -934,7 +812,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var actionsDuration = springDuration var actionsOffset: CGFloat = 0.0 var contentDuration = springDuration - if case let .extracted(source) = self.source, source.centerVertically { + if case let .extracted(source) = self.legacySource, source.centerVertically { actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57 actionsDuration *= 1.0 contentDuration *= 0.9 @@ -973,7 +851,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.contentContainerNode.layer.animateSpring(from: min(localSourceFrame.width / self.contentContainerNode.frame.width, localSourceFrame.height / self.contentContainerNode.frame.height) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - switch self.source { + switch self.legacySource { case let .controller(controller): controller.animatedIn() default: @@ -1009,28 +887,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.beganAnimatingOut() - if let _ = self.presentationNode { - self.currentPresentationStateTransition = .animateOut(result: initialResult, completion: completion) - if let validLayout = self.validLayout { - if case let .custom(transition) = initialResult { - self.delayLayoutUpdate = true - Queue.mainQueue().after(0.1) { - self.delayLayoutUpdate = false - self.updateLayout( - layout: validLayout, - transition: transition, - previousActionsContainerNode: nil - ) - self.isAnimatingOut = true - } - } else { - self.updateLayout( - layout: validLayout, - transition: .animated(duration: 0.35, curve: .easeInOut), - previousActionsContainerNode: nil - ) - } - } + if let sourceContainer = self.sourceContainer { + sourceContainer.animateOut(result: initialResult, completion: completion) return } @@ -1039,7 +897,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var result = initialResult - switch self.source { + switch self.legacySource { case let .location(source): let transitionInfo = source.transitionInfo() if transitionInfo == nil { @@ -1281,7 +1139,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } var actionsOffset: CGFloat = 0.0 - if case let .extracted(source) = self.source, source.centerVertically { + if case let .extracted(source) = self.legacySource, source.centerVertically { actionsOffset = -localSourceFrame.width * 0.6 } @@ -1454,24 +1312,23 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { - if let presentationNode = self.presentationNode { - presentationNode.addRelativeContentOffset(offset, transition: transition) + if let sourceContainer = self.sourceContainer { + sourceContainer.addRelativeContentOffset(offset, transition: transition) } } func cancelReactionAnimation() { - if let presentationNode = self.presentationNode { - presentationNode.cancelReactionAnimation() + if let sourceContainer = self.sourceContainer { + sourceContainer.cancelReactionAnimation() } } func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { - if let presentationNode = self.presentationNode { - presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) + if let sourceContainer = self.sourceContainer { + sourceContainer.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) } } - func getActionsMinHeight() -> ContextController.ActionsHeight? { if !self.actionsContainerNode.bounds.height.isZero { return ContextController.ActionsHeight( @@ -1483,21 +1340,25 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } - func setItemsSignal(items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { - self.items = items - self.itemsDisposable.set((items - |> deliverOnMainQueue).start(next: { [weak self] items in - guard let strongSelf = self else { - return - } - strongSelf.setItems(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition) - })) + func setItemsSignal(items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition, animated: Bool) { + if let sourceContainer = self.sourceContainer { + sourceContainer.setItems(items: items, animated: animated) + } else { + self.legacyItems = items + self.itemsDisposable.set((items + |> deliverOnMainQueue).start(next: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.setItems(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition) + })) + } } private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { - if let presentationNode = self.presentationNode { + if let sourceContainer = self.sourceContainer { let disableAnimations = self.getController()?.immediateItemsTransitionAnimation == true - presentationNode.replaceItems(items: items, animated: self.didCompleteAnimationIn && !disableAnimations) + sourceContainer.setItems(items: .single(items), animated: !disableAnimations) if !self.didSetItemsReady { self.didSetItemsReady = true @@ -1539,18 +1400,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } func pushItems(items: Signal) { - self.itemsDisposable.set((items - |> deliverOnMainQueue).start(next: { [weak self] items in - guard let strongSelf = self, let presentationNode = strongSelf.presentationNode else { - return - } - presentationNode.pushItems(items: items) - })) + if let sourceContainer = self.sourceContainer { + sourceContainer.pushItems(items: items) + } } func popItems() { - if let presentationNode = self.presentationNode { - presentationNode.popItems() + if let sourceContainer = self.sourceContainer { + sourceContainer.popItems() } } @@ -1578,16 +1435,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.validLayout = layout - let presentationStateTransition = self.currentPresentationStateTransition - self.currentPresentationStateTransition = .none - - if let presentationNode = self.presentationNode { - transition.updateFrame(node: presentationNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - presentationNode.update( + if let sourceContainer = self.sourceContainer { + transition.updateFrame(node: sourceContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) + sourceContainer.update( presentationData: self.presentationData, layout: layout, - transition: transition, - stateTransition: presentationStateTransition + transition: transition ) return } @@ -1603,8 +1456,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi switch layout.metrics.widthClass { case .compact: - if case .reference = self.source { - } else if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground { + if case .reference = self.legacySource { + } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { } else if self.effectView.superview == nil { self.view.insertSubview(self.effectView, at: 0) if #available(iOS 10.0, *) { @@ -1619,8 +1472,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.dimNode.isHidden = false self.withoutBlurDimNode.isHidden = true case .regular: - if case .reference = self.source { - } else if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground { + if case .reference = self.legacySource { + } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { } else if self.effectView.superview != nil { self.effectView.removeFromSuperview() self.withoutBlurDimNode.alpha = 1.0 @@ -1729,7 +1582,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } case let .extracted(contentParentNode, keepInPlace): var centerVertically = false - if case let .extracted(source) = self.source, source.centerVertically { + if case let .extracted(source) = self.legacySource, source.centerVertically { centerVertically = true } let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0 @@ -1881,7 +1734,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } case let .controller(contentParentNode): var projectedFrame: CGRect = convertFrame(contentParentNode.sourceView.bounds, from: contentParentNode.sourceView, to: self.view) - switch self.source { + switch self.legacySource { case let .controller(source): let transitionInfo = source.transitionInfo() if let (sourceView, sourceRect) = transitionInfo?.sourceNode() { @@ -2112,8 +1965,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } - if let presentationNode = self.presentationNode { - return presentationNode.hitTest(self.view.convert(point, to: presentationNode.view), with: event) + if let sourceContainer = self.sourceContainer { + return sourceContainer.hitTest(self.view.convert(point, to: sourceContainer.view), with: event) } let mappedPoint = self.view.convert(point, to: self.scrollNode.view) @@ -2125,7 +1978,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi maybePassthrough = passthroughTouchEvent(self.view, point) } case let .extracted(contentParentNode, _): - if case let .extracted(source) = self.source { + if case let .extracted(source) = self.legacySource { if !source.ignoreContentTouches { let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view) if let result = contentParentNode.contentNode.customHitTest?(contentPoint) { @@ -2141,7 +1994,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } case let .controller(controller): var passthrough = false - switch self.source { + switch self.legacySource { case let .controller(controllerSource): passthrough = controllerSource.passthroughTouches default: @@ -2150,9 +2003,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if passthrough { let controllerPoint = self.view.convert(point, to: controller.controller.view) if let result = controller.controller.view.hitTest(controllerPoint, with: event) { - #if DEBUG - //return controller.view - #endif return result } } @@ -2183,15 +2033,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } fileprivate func performHighlightedAction() { - self.presentationNode?.highlightGestureFinished(performAction: true) + self.sourceContainer?.performHighlightedAction() } fileprivate func decreaseHighlightedIndex() { - self.presentationNode?.decreaseHighlightedIndex() + self.sourceContainer?.decreaseHighlightedIndex() } fileprivate func increaseHighlightedIndex() { - self.presentationNode?.increaseHighlightedIndex() + self.sourceContainer?.increaseHighlightedIndex() } } @@ -2359,6 +2209,30 @@ public protocol ContextControllerItemsContent: AnyObject { } public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder { + public final class Source { + public let id: AnyHashable + public let title: String + public let source: ContextContentSource + public let items: Signal + + public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal) { + self.id = id + self.title = title + self.source = source + self.items = items + } + } + + public final class Configuration { + public let sources: [Source] + public let initialId: AnyHashable + + public init(sources: [Source], initialId: AnyHashable) { + self.sources = sources + self.initialId = initialId + } + } + public struct Items { public enum Content { case list([ContextMenuItem]) @@ -2366,6 +2240,7 @@ public final class ContextController: ViewController, StandalonePresentableContr case custom(ContextControllerItemsContent) } + public var id: AnyHashable? public var content: Content public var context: AccountContext? public var reactionItems: [ReactionContextItem] @@ -2375,8 +2250,22 @@ public final class ContextController: ViewController, StandalonePresentableContr public var disablePositionLock: Bool public var tip: Tip? public var tipSignal: Signal? - - public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], selectedReactionItems: Set = Set(), animationCache: AnimationCache? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil, tipSignal: Signal? = nil) { + public var dismissed: (() -> Void)? + + public init( + id: AnyHashable? = nil, + content: Content, + context: AccountContext? = nil, + reactionItems: [ReactionContextItem] = [], + selectedReactionItems: Set = Set(), + animationCache: AnimationCache? = nil, + getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, + disablePositionLock: Bool = false, + tip: Tip? = nil, + tipSignal: Signal? = nil, + dismissed: (() -> Void)? = nil + ) { + self.id = id self.content = content self.context = context self.animationCache = animationCache @@ -2386,9 +2275,11 @@ public final class ContextController: ViewController, StandalonePresentableContr self.disablePositionLock = disablePositionLock self.tip = tip self.tipSignal = tipSignal + self.dismissed = dismissed } public init() { + self.id = nil self.content = .list([]) self.context = nil self.reactionItems = [] @@ -2397,6 +2288,7 @@ public final class ContextController: ViewController, StandalonePresentableContr self.disablePositionLock = false self.tip = nil self.tipSignal = nil + self.dismissed = nil } } @@ -2407,6 +2299,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public enum Tip: Equatable { case textSelection + case quoteSelection case messageViewsPrivacy case messageCopyProtection(isChannel: Bool) case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) @@ -2420,6 +2313,12 @@ public final class ContextController: ViewController, StandalonePresentableContr } else { return false } + case .quoteSelection: + if case .quoteSelection = rhs { + return true + } else { + return false + } case .messageViewsPrivacy: if case .messageViewsPrivacy = rhs { return true @@ -2465,8 +2364,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } private var presentationData: PresentationData - private let source: ContextContentSource - private var items: Signal + private let configuration: ContextController.Configuration private let _ready = Promise() override public var ready: Promise { @@ -2489,7 +2387,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } } - private var controllerNode: ContextControllerNode { + var controllerNode: ContextControllerNode { return self.displayNode as! ContextControllerNode } @@ -2518,17 +2416,41 @@ public final class ContextController: ViewController, StandalonePresentableContr public var getOverlayViews: (() -> [UIView])? - public init(presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false) { + convenience public init(presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false) { + self.init( + presentationData: presentationData, + configuration: ContextController.Configuration( + sources: [ContextController.Source( + id: AnyHashable(0 as Int), + title: "", + source: source, + items: items + )], + initialId: AnyHashable(0 as Int) + ), + recognizer: recognizer, + gesture: gesture, + workaroundUseLegacyImplementation: workaroundUseLegacyImplementation + ) + } + + public init( + presentationData: PresentationData, + configuration: ContextController.Configuration, + recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, + gesture: ContextGesture? = nil, + workaroundUseLegacyImplementation: Bool = false + ) { self.presentationData = presentationData - self.source = source - self.items = items + self.configuration = configuration self.recognizer = recognizer self.gesture = gesture self.workaroundUseLegacyImplementation = workaroundUseLegacyImplementation super.init(navigationBarPresentationData: nil) - - switch source { + + if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { + switch mainSource.source { case let .location(locationSource): self.statusBar.statusBarStyle = .Ignore @@ -2570,6 +2492,7 @@ public final class ContextController: ViewController, StandalonePresentableContr }).strict() case .controller: self.statusBar.statusBarStyle = .Hide + } } self.lockOrientation = true @@ -2585,7 +2508,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } override public func loadDisplayNode() { - self.displayNode = ContextControllerNode(controller: self, presentationData: self.presentationData, source: self.source, items: self.items, beginDismiss: { [weak self] result in + self.displayNode = ContextControllerNode(controller: self, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in self?.dismiss(result: result, completion: nil) }, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in guard let strongSelf = self else { @@ -2600,19 +2523,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } return true } - }, attemptTransitionControllerIntoNavigation: { [weak self] in - guard let strongSelf = self else { - return - } - switch strongSelf.source { - /*case let .controller(controller): - if let navigationController = controller.navigationController { - strongSelf.presentingViewController?.dismiss(animated: false, completion: nil) - navigationController.pushViewController(controller.controller, animated: false) - }*/ - default: - break - } + }, attemptTransitionControllerIntoNavigation: { }) self.controllerNode.dismissedForCancel = self.dismissedForCancel self.displayNodeDidLoad() @@ -2662,18 +2573,24 @@ public final class ContextController: ViewController, StandalonePresentableContr return nil } - public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?) { - self.items = items + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) { + //self.items = items + if self.isNodeLoaded { self.immediateItemsTransitionAnimation = false - self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: .scale) + self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: .scale, animated: animated) + } else { + assertionFailure() } } public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { - self.items = items + //self.items = items + if self.isNodeLoaded { - self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition) + self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition, animated: true) + } else { + assertionFailure() } } @@ -2698,7 +2615,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } } - private func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { + public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { if viewTreeContainsFirstResponder(view: self.view) { self.dismissOnInputClose = (result, completion) self.view.endEditing(true) @@ -2720,6 +2637,10 @@ public final class ContextController: ViewController, StandalonePresentableContr self.dismiss(result: .default, completion: completion) } + public func dismissWithCustomTransition(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { + self.dismiss(result: .custom(transition), completion: nil) + } + public func dismissWithoutContent() { self.dismiss(result: .dismissWithoutContent, completion: nil) } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index eb0ebb76333..6609265629b 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -13,6 +13,8 @@ import EntityKeyboard import AnimationCache import MultiAnimationRenderer import AnimationUI +import ComponentFlow +import LottieComponent public protocol ContextControllerActionsStackItemNode: ASDisplayNode { var wantsFullWidth: Bool { get } @@ -41,9 +43,11 @@ public protocol ContextControllerActionsStackItem: AnyObject { requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void ) -> ContextControllerActionsStackItemNode + var id: AnyHashable? { get } var tip: ContextController.Tip? { get } var tipSignal: Signal? { get } var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { get } + var dismissed: (() -> Void)? { get } } protocol ContextControllerActionsListItemNode: ASDisplayNode { @@ -58,7 +62,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin private let getController: () -> ContextControllerProtocol? private let requestDismiss: (ContextMenuActionResult) -> Void private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void - private let item: ContextMenuActionItem + private var item: ContextMenuActionItem private let highlightBackgroundNode: ASDisplayNode private let titleLabelNode: ImmediateTextNode @@ -68,6 +72,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin private var badgeIconNode: ASImageNode? private var animationNode: AnimationNode? + private var currentAnimatedIconContent: ContextMenuActionItem.IconAnimation? + private var animatedIcon: ComponentView? + private var currentBadge: (badge: ContextMenuActionBadge, image: UIImage)? private var iconDisposable: Disposable? @@ -187,6 +194,11 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin return super.hitTest(point, with: event) } + func setItem(item: ContextMenuActionItem) { + self.item = item + self.accessibilityLabel = item.text + } + func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) { let sideInset: CGFloat = 16.0 let verticalInset: CGFloat = 11.0 @@ -284,7 +296,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin ) } - let iconSize: CGSize? + var iconSize: CGSize? if let iconSource = self.item.iconSource { iconSize = iconSource.size self.iconNode.cornerRadius = iconSource.cornerRadius @@ -314,6 +326,34 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin iconSize = iconImage?.size } + if let iconAnimation = self.item.iconAnimation { + let animatedIcon: ComponentView + if let current = self.animatedIcon { + animatedIcon = current + } else { + animatedIcon = ComponentView() + self.animatedIcon = animatedIcon + } + + let animatedIconSize = CGSize(width: 24.0, height: 24.0) + let _ = animatedIcon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: iconAnimation.name), + color: titleColor, + startingPosition: .end, + loop: false + )), + environment: {}, + containerSize: animatedIconSize + ) + + iconSize = animatedIconSize + } else if let animatedIcon = self.animatedIcon { + self.animatedIcon = nil + animatedIcon.view?.removeFromSuperview() + } + let additionalIcon = self.item.additionalLeftIcon?(presentationData.theme) var additionalIconSize: CGSize? self.additionalIconNode.image = additionalIcon @@ -469,6 +509,21 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin if let animationNode = self.animationNode { transition.updateFrame(node: animationNode, frame: iconFrame, beginWithCurrentState: true) } + if let animatedIconView = self.animatedIcon?.view { + if animatedIconView.superview == nil { + self.view.addSubview(animatedIconView) + animatedIconView.frame = iconFrame + } else { + transition.updateFrame(view: animatedIconView, frame: iconFrame, beginWithCurrentState: true) + if let currentAnimatedIconContent = self.currentAnimatedIconContent, currentAnimatedIconContent != self.item.iconAnimation { + if let animatedIconView = animatedIconView as? LottieComponent.View { + animatedIconView.playOnce() + } + } + } + + self.currentAnimatedIconContent = self.item.iconAnimation + } } if let additionalIconSize { @@ -585,7 +640,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C } final class ContextControllerActionsListStackItem: ContextControllerActionsStackItem { - private final class Node: ASDisplayNode, ContextControllerActionsStackItemNode { + final class Node: ASDisplayNode, ContextControllerActionsStackItemNode { private final class Item { let node: ContextControllerActionsListItemNode let separatorNode: ASDisplayNode? @@ -597,12 +652,16 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack } private let requestUpdate: (ContainedViewLayoutTransition) -> Void + private let getController: () -> ContextControllerProtocol? + private let requestDismiss: (ContextMenuActionResult) -> Void private var items: [ContextMenuItem] private var itemNodes: [Item] private var hapticFeedback: HapticFeedback? private var highlightedItemNode: Item? + private var invalidatedItemNodes: Bool = false + var wantsFullWidth: Bool { return false } @@ -614,6 +673,8 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack items: [ContextMenuItem] ) { self.requestUpdate = requestUpdate + self.getController = getController + self.requestDismiss = requestDismiss self.items = items var requestUpdateAction: ((AnyHashable, ContextMenuActionItem) -> Void)? @@ -660,41 +721,62 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack } requestUpdateAction = { [weak self] id, action in - guard let strongSelf = self else { + guard let self else { return } - loop: for i in 0 ..< strongSelf.items.count { - switch strongSelf.items[i] { - case let .action(currentAction): - if currentAction.id == id { - let previousNode = strongSelf.itemNodes[i] - previousNode.node.removeFromSupernode() - previousNode.separatorNode?.removeFromSupernode() - - let addedNode = Item( - node: ContextControllerActionsListActionItemNode( - getController: getController, - requestDismiss: requestDismiss, - requestUpdateAction: { id, action in - requestUpdateAction?(id, action) - }, - item: action - ), - separatorNode: ASDisplayNode() - ) - strongSelf.itemNodes[i] = addedNode - if let separatorNode = addedNode.separatorNode { - strongSelf.insertSubnode(separatorNode, at: 0) - } - strongSelf.addSubnode(addedNode.node) - - strongSelf.requestUpdate(.immediate) - - break loop + self.requestUpdateAction(id: id, action: action) + } + } + + func updateItems(items: [ContextMenuItem]) { + self.items = items + for i in 0 ..< items.count { + if self.itemNodes.count < i { + break + } + if case let .action(action) = items[i] { + if let itemNode = self.itemNodes[i].node as? ContextControllerActionsListActionItemNode { + itemNode.setItem(item: action) + } + } + } + } + + private func requestUpdateAction(id: AnyHashable, action: ContextMenuActionItem) { + loop: for i in 0 ..< self.items.count { + switch self.items[i] { + case let .action(currentAction): + if currentAction.id == id { + let previousNode = self.itemNodes[i] + previousNode.node.removeFromSupernode() + previousNode.separatorNode?.removeFromSupernode() + + let addedNode = Item( + node: ContextControllerActionsListActionItemNode( + getController: self.getController, + requestDismiss: self.requestDismiss, + requestUpdateAction: { [weak self] id, action in + guard let self else { + return + } + self.requestUpdateAction(id: id, action: action) + }, + item: action + ), + separatorNode: ASDisplayNode() + ) + self.itemNodes[i] = addedNode + if let separatorNode = addedNode.separatorNode { + self.insertSubnode(separatorNode, at: 0) } - default: - break + self.addSubnode(addedNode.node) + + self.requestUpdate(.immediate) + + break loop } + default: + break } } } @@ -720,6 +802,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack combinedSize.width = max(combinedSize.width, itemNodeLayout.minSize.width) combinedSize.height += itemNodeLayout.minSize.height } + self.invalidatedItemNodes = false combinedSize.width = max(combinedSize.width, standardMinWidth) var nextItemOrigin = CGPoint() @@ -820,21 +903,27 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack } } - private let items: [ContextMenuItem] + let id: AnyHashable? + let items: [ContextMenuItem] let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? let tipSignal: Signal? + let dismissed: (() -> Void)? init( + id: AnyHashable?, items: [ContextMenuItem], reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, - tipSignal: Signal? + tipSignal: Signal?, + dismissed: (() -> Void)? ) { + self.id = id self.items = items self.reactionItems = reactionItems self.tip = tip self.tipSignal = tipSignal + self.dismissed = dismissed } func node( @@ -912,21 +1001,27 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } } + let id: AnyHashable? private let content: ContextControllerItemsContent let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? let tipSignal: Signal? + let dismissed: (() -> Void)? init( + id: AnyHashable?, content: ContextControllerItemsContent, reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, - tipSignal: Signal? + tipSignal: Signal?, + dismissed: (() -> Void)? ) { + self.id = id self.content = content self.reactionItems = reactionItems self.tip = tip self.tipSignal = tipSignal + self.dismissed = dismissed } func node( @@ -951,11 +1046,11 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C } switch items.content { case let .list(listItems): - return [ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal)] + return [ContextControllerActionsListStackItem(id: items.id, items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] case let .twoLists(listItems1, listItems2): - return [ContextControllerActionsListStackItem(items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil), ContextControllerActionsListStackItem(items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil)] + return [ContextControllerActionsListStackItem(id: items.id, items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(id: nil, items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)] case let .custom(customContent): - return [ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal)] + return [ContextControllerActionsCustomStackItem(id: items.id, content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] } } @@ -1065,12 +1160,14 @@ final class ContextControllerActionsStackNode: ASDisplayNode { final class ItemContainer: ASDisplayNode { let getController: () -> ContextControllerProtocol? let requestUpdate: (ContainedViewLayoutTransition) -> Void + let item: ContextControllerActionsStackItem let node: ContextControllerActionsStackItemNode let dimNode: ASDisplayNode var tip: ContextController.Tip? let tipSignal: Signal? var tipNode: InnerTextSelectionTipContainerNode? let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + let itemDismissed: (() -> Void)? var storedScrollingState: CGFloat? let positionLock: CGFloat? @@ -1085,10 +1182,12 @@ final class ContextControllerActionsStackNode: ASDisplayNode { tip: ContextController.Tip?, tipSignal: Signal?, reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + itemDismissed: (() -> Void)?, positionLock: CGFloat? ) { self.getController = getController self.requestUpdate = requestUpdate + self.item = item self.node = item.node( getController: getController, requestDismiss: requestDismiss, @@ -1101,6 +1200,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.dimNode.alpha = 0.0 self.reactionItems = reactionItems + self.itemDismissed = itemDismissed self.positionLock = positionLock self.tip = tip @@ -1294,9 +1394,51 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } } - func replace(item: ContextControllerActionsStackItem, animated: Bool) { + func replace(item: ContextControllerActionsStackItem, animated: Bool?) { + if let item = item as? ContextControllerActionsListStackItem, let topContainer = self.itemContainers.last, let topItem = topContainer.item as? ContextControllerActionsListStackItem, let topId = topItem.id, let id = item.id, topId == id, item.items.count == topItem.items.count { + if let topNode = topContainer.node as? ContextControllerActionsListStackItem.Node { + var matches = true + for i in 0 ..< item.items.count { + switch item.items[i] { + case .action: + if case .action = topItem.items[i] { + } else { + matches = false + } + case .custom: + if case .custom = topItem.items[i] { + } else { + matches = false + } + case .separator: + if case .separator = topItem.items[i] { + } else { + matches = false + } + } + } + + if matches { + topNode.updateItems(items: item.items) + self.requestUpdate(.animated(duration: 0.3, curve: .spring)) + return + } + } + } + + var resolvedAnimated = false + if let animated { + resolvedAnimated = animated + } else { + if let id = item.id, let lastId = self.itemContainers.last?.item.id { + if id != lastId { + resolvedAnimated = true + } + } + } + for itemContainer in self.itemContainers { - if animated { + if resolvedAnimated { self.dismissingItemContainers.append((itemContainer, false)) } else { itemContainer.tipNode?.removeFromSupernode() @@ -1306,7 +1448,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.itemContainers.removeAll() self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 - self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: animated) + self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: resolvedAnimated) } func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) { @@ -1327,6 +1469,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { tip: item.tip, tipSignal: item.tipSignal, reactionItems: item.reactionItems, + itemDismissed: item.dismissed, positionLock: positionLock ) self.itemContainers.append(itemContainer) @@ -1353,6 +1496,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let itemContainer = self.itemContainers[self.itemContainers.count - 1] self.itemContainers.remove(at: self.itemContainers.count - 1) self.dismissingItemContainers.append((itemContainer, true)) + + itemContainer.itemDismissed?() } self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 8de9f83057e..430f27c9447 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -115,9 +115,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo case location(ContextLocationContentSource) case reference(ContextReferenceContentSource) case extracted(ContextExtractedContentSource) + case controller(ContextControllerContentSource) } - private final class ContentNode: ASDisplayNode { + private final class ItemContentNode: ASDisplayNode { let offsetContainerNode: ASDisplayNode var containingItem: ContextControllerTakeViewInfo.ContainingItem @@ -160,6 +161,56 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + private final class ControllerContentNode: ASDisplayNode { + let controller: ViewController + let passthroughTouches: Bool + var storedContentHeight: CGFloat? + + init(controller: ViewController, passthroughTouches: Bool) { + self.controller = controller + self.passthroughTouches = passthroughTouches + + super.init() + + self.clipsToBounds = true + self.cornerRadius = 14.0 + + self.addSubnode(self.controller.displayNode) + } + + func update(presentationData: PresentationData, parentLayout: ContainerViewLayout, size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size)) + self.controller.containerLayoutUpdated( + ContainerViewLayout( + size: size, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + deviceMetrics: parentLayout.deviceMetrics, + intrinsicInsets: UIEdgeInsets(), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ), + transition: transition + ) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if self.passthroughTouches { + let controllerPoint = self.view.convert(point, to: self.controller.view) + if let result = self.controller.view.hitTest(controllerPoint, with: event) { + return result + } + } + return self.view + } + } + private final class AnimatingOutState { var currentContentScreenFrame: CGRect @@ -170,6 +221,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + private let _ready = Promise() + private var didSetReady: Bool = false + var ready: Signal { + return self._ready.get() + } + private let getController: () -> ContextControllerProtocol? private let requestUpdate: (ContainedViewLayoutTransition) -> Void private let requestUpdateOverlayWantsToBeBelowKeyboard: (ContainedViewLayoutTransition) -> Void @@ -177,7 +234,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private let requestAnimateOut: (ContextMenuActionResult, @escaping () -> Void) -> Void private let source: ContentSource - private let backgroundNode: NavigationBackgroundNode private let dismissTapNode: ASDisplayNode private let dismissAccessibilityArea: AccessibilityAreaNode private let clippingNode: ASDisplayNode @@ -187,7 +243,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private var reactionContextNode: ReactionContextNode? private var reactionContextNodeIsAnimatingOut: Bool = false - private var contentNode: ContentNode? + private var itemContentNode: ItemContentNode? + private var controllerContentNode: ControllerContentNode? private let contentRectDebugNode: ASDisplayNode private var actionsContainerNode: ASDisplayNode @@ -224,8 +281,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.requestAnimateOut = requestAnimateOut self.source = source - self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false) - self.dismissTapNode = ASDisplayNode() self.dismissAccessibilityArea = AccessibilityAreaNode() @@ -272,7 +327,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.view.addSubview(self.scroller) self.scroller.isHidden = true - self.addSubnode(self.backgroundNode) self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.dismissTapNode) @@ -310,7 +364,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } - if case let .extracted(source) = self.source, !source.ignoreContentTouches, let contentNode = self.contentNode { + if case let .extracted(source) = self.source, !source.ignoreContentTouches, let contentNode = self.itemContentNode { let contentPoint = self.view.convert(point, to: contentNode.containingItem.contentView) if let result = contentNode.containingItem.customHitTest?(contentPoint) { return result @@ -321,6 +375,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo return contentNode.containingItem.contentView } } + } else if case .controller = self.source, let contentNode = self.controllerContentNode { + let contentPoint = self.view.convert(point, to: contentNode.view) + let _ = contentPoint + //TODO: } return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) @@ -426,7 +484,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } - func replaceItems(items: ContextController.Items, animated: Bool) { + func replaceItems(items: ContextController.Items, animated: Bool?) { if case .twoLists = items.content { let stackItems = makeContextControllerActionsStackItem(items: items) self.actionsStackNode.replace(item: stackItems.first!, animated: animated) @@ -442,11 +500,21 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if !items.disablePositionLock { positionLock = self.getActionsStackPositionLock() } + if self.actionsStackNode.topPositionLock == nil { + if let contentNode = self.controllerContentNode, contentNode.bounds.height != 0.0 { + contentNode.storedContentHeight = contentNode.bounds.height + } + } self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items).first!, currentScrollingState: currentScrollingState, positionLock: positionLock, animated: true) } func popItems() { self.actionsStackNode.pop() + if self.actionsStackNode.topPositionLock == nil { + if let contentNode = self.controllerContentNode { + contentNode.storedContentHeight = nil + } + } } private func getCurrentScrollingState() -> CGFloat { @@ -457,7 +525,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch self.source { case .location, .reference: return nil - case .extracted: + case .extracted, .controller: return self.actionsStackNode.view.convert(CGPoint(), to: self.view).y } } @@ -481,13 +549,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo ) { self.validLayout = layout - let contentActionsSpacing: CGFloat = 7.0 + var contentActionsSpacing: CGFloat = 7.0 let actionsEdgeInset: CGFloat - let actionsSideInset: CGFloat = 6.0 + let actionsSideInset: CGFloat let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0 let bottomInset: CGFloat = 10.0 - let contentNode: ContentNode? + let itemContentNode: ItemContentNode? + let controllerContentNode: ControllerContentNode? var contentTransition = transition if self.strings !== presentationData.strings { @@ -498,51 +567,64 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch self.source { case .location, .reference: - self.backgroundNode.updateColor( - color: .clear, - enableBlur: false, - forceKeepBlur: false, - transition: .immediate - ) actionsEdgeInset = 16.0 + actionsSideInset = 6.0 case .extracted: - self.backgroundNode.updateColor( - color: presentationData.theme.contextMenu.dimColor, - enableBlur: true, - forceKeepBlur: true, - transition: .immediate - ) actionsEdgeInset = 12.0 + actionsSideInset = 6.0 + case .controller: + actionsEdgeInset = 12.0 + actionsSideInset = -2.0 + contentActionsSpacing += 3.0 } - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) - self.backgroundNode.update(size: layout.size, transition: transition) - transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) transition.updateFrame(view: self.scroller, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) } - if let current = self.contentNode { - contentNode = current + if let current = self.itemContentNode { + itemContentNode = current } else { switch self.source { - case .location, .reference: - contentNode = nil + case .location, .reference, .controller: + itemContentNode = nil case let .extracted(source): guard let takeInfo = source.takeView() else { return } - let contentNodeValue = ContentNode(containingItem: takeInfo.containingItem) + let contentNodeValue = ItemContentNode(containingItem: takeInfo.containingItem) contentNodeValue.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace self.scrollNode.insertSubnode(contentNodeValue, aboveSubnode: self.actionsContainerNode) - self.contentNode = contentNodeValue - contentNode = contentNodeValue + self.itemContentNode = contentNodeValue + itemContentNode = contentNodeValue contentTransition = .immediate } } + if let current = self.controllerContentNode { + controllerContentNode = current + } else { + switch self.source { + case let .controller(source): + let controllerContentNodeValue = ControllerContentNode(controller: source.controller, passthroughTouches: source.passthroughTouches) + + //source.controller.viewWillAppear(false) + //source.controller.setIgnoreAppearanceMethodInvocations(true) + + self.scrollNode.insertSubnode(controllerContentNodeValue, aboveSubnode: self.actionsContainerNode) + self.controllerContentNode = controllerContentNodeValue + controllerContentNode = controllerContentNodeValue + contentTransition = .immediate + + //source.controller.setIgnoreAppearanceMethodInvocations(false) + //source.controller.viewDidAppear(false) + case .location, .reference, .extracted: + controllerContentNode = nil + } + } + var animateReactionsIn = false var contentTopInset: CGFloat = topInset var removedReactionContextNode: ReactionContextNode? @@ -630,7 +712,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo removedReactionContextNode = reactionContextNode } - if let contentNode = contentNode { + if let contentNode = itemContentNode { switch stateTransition { case .animateIn, .animateOut: contentNode.storedGlobalFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view) @@ -643,6 +725,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let contentParentGlobalFrame: CGRect var contentRect: CGRect + var isContentResizeableVertically: Bool = false + let _ = isContentResizeableVertically switch self.source { case let .location(location): @@ -661,7 +745,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo return } case .extracted: - if let contentNode = contentNode { + if let contentNode = itemContentNode { contentParentGlobalFrame = convertFrame(contentNode.containingItem.view.bounds, from: contentNode.containingItem.view, to: self.view) let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingItem.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingItem.contentRect.height), size: contentNode.containingItem.contentRect.size) @@ -672,6 +756,52 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } else { return } + case let .controller(source): + if let contentNode = controllerContentNode { + var defaultContentSize = CGSize(width: layout.size.width - 12.0 * 2.0, height: layout.size.height - 12.0 * 2.0 - contentTopInset - layout.safeInsets.bottom) + if case .regular = layout.metrics.widthClass { + defaultContentSize.width = min(defaultContentSize.width, 400.0) + } + 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)) { + contentSize = preferredSize + } else if let storedContentHeight = contentNode.storedContentHeight { + contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight) + } else { + contentSize = defaultContentSize + isContentResizeableVertically = true + } + + if case .regular = layout.metrics.widthClass { + if let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { + let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) + + contentRect = CGRect(origin: CGPoint(x: sourcePoint.x - floor(contentSize.width * 0.5), y: sourcePoint.y - floor(contentSize.height * 0.5)), size: contentSize) + if contentRect.origin.x < 0.0 { + contentRect.origin.x = 0.0 + } + if contentRect.origin.y < 0.0 { + contentRect.origin.y = 0.0 + } + if contentRect.origin.x + contentRect.width > layout.size.width { + contentRect.origin.x = layout.size.width - contentRect.width + } + if contentRect.origin.y + contentRect.height > layout.size.height { + contentRect.origin.y = layout.size.height - contentRect.height + } + } else { + contentRect = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) * 0.5), y: floor((layout.size.height - contentSize.height) * 0.5)), size: contentSize) + } + } else { + contentRect = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) * 0.5), y: floor((layout.size.height - contentSize.height) * 0.5)), size: contentSize) + } + + contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)) + } else { + return + } } let keepInPlace: Bool @@ -683,11 +813,15 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo case let .extracted(source): keepInPlace = source.keepInPlace actionsHorizontalAlignment = source.actionsHorizontalAlignment + case .controller: + //TODO: + keepInPlace = false + actionsHorizontalAlignment = .default } var defaultScrollY: CGFloat = 0.0 if self.animatingOutState == nil { - if let contentNode = contentNode { + if let contentNode = itemContentNode { contentNode.update( presentationData: presentationData, size: contentNode.containingItem.view.bounds.size, @@ -708,7 +842,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let actionsStackPresentation: ContextControllerActionsStackNode.Presentation switch self.source { - case .location, .reference: + case .location, .reference, .controller: actionsStackPresentation = .inline case .extracted: actionsStackPresentation = .modal @@ -729,6 +863,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition: transition ) + if isContentResizeableVertically && self.actionsStackNode.topPositionLock == nil { + var contentHeight = layout.size.height - contentTopInset - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom - actionsSize.height + contentHeight = min(contentHeight, contentRect.height) + contentHeight = max(contentHeight, 200.0) + + if case .regular = layout.metrics.widthClass { + } else { + contentRect = CGRect(origin: CGPoint(x: contentRect.minX, y: floor(contentRect.midY - contentHeight * 0.5)), size: CGSize(width: contentRect.width, height: contentHeight)) + } + } + var isAnimatingOut = false if case .animateOut = stateTransition { isAnimatingOut = true @@ -842,6 +987,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } case .extracted: actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 + case .controller: + //TODO: + actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 } } } @@ -872,13 +1020,28 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition.updateFrame(node: self.actionsStackNode, frame: CGRect(origin: CGPoint(x: 0.0, y: combinedActionsFrame.height - actionsSize.height), size: actionsSize), beginWithCurrentState: true) transition.updateFrame(node: self.additionalActionsStackNode, frame: CGRect(origin: .zero, size: additionalActionsSize), beginWithCurrentState: true) - if let contentNode = contentNode { + if let contentNode = itemContentNode { var contentFrame = CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size) if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 { contentFrame.origin.x = layout.size.width - contentFrame.maxX } contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true) } + if let contentNode = controllerContentNode { + //TODO: + var contentFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentRect.size) + if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 { + contentFrame.origin.x = layout.size.width - contentFrame.maxX + } + contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true) + + contentNode.update( + presentationData: presentationData, + parentLayout: layout, + size: contentFrame.size, + transition: contentTransition + ) + } let contentHeight: CGFloat if self.actionsStackNode.topPositionLock != nil || self.currentReactionsPositionLock != nil { @@ -931,7 +1094,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch stateTransition { case .animateIn: - if let contentNode = contentNode { + if let contentNode = itemContentNode { contentNode.takeContainingNode() } @@ -940,11 +1103,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.scroller.contentOffset = CGPoint(x: 0.0, y: defaultScrollY) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - let animationInContentYDistance: CGFloat let currentContentScreenFrame: CGRect - if let contentNode = contentNode { + if let contentNode = itemContentNode { if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace { self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 0.0, y: animateClippingFromContentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: animateClippingFromContentAreaInScreenSpace.height)), to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2) self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2) @@ -981,6 +1142,34 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo damping: springDamping, additive: true ) + } else if let contentNode = controllerContentNode { + if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { + let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) + animationInContentYDistance = contentRect.midY - sourcePoint.y + } else { + animationInContentYDistance = 0.0 + } + currentContentScreenFrame = contentRect + + contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + contentNode.layer.animateSpring( + from: -animationInContentYDistance as NSNumber, to: 0.0 as NSNumber, + keyPath: "position.y", + duration: duration, + delay: 0.0, + initialVelocity: 0.0, + damping: springDamping, + additive: true + ) + contentNode.layer.animateSpring( + from: 0.01 as NSNumber, to: 1.0 as NSNumber, + keyPath: "transform.scale", + duration: duration, + delay: 0.0, + initialVelocity: 0.0, + damping: springDamping, + additive: false + ) } else { animationInContentYDistance = 0.0 currentContentScreenFrame = contentRect @@ -1010,7 +1199,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } let actionsVerticalTransitionDirection: CGFloat - if let contentNode = contentNode { + if let contentNode = itemContentNode { if contentNode.frame.minY < self.actionsContainerNode.frame.minY { actionsVerticalTransitionDirection = -1.0 } else { @@ -1052,13 +1241,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.actionsStackNode.animateIn() - if let contentNode = contentNode { + if let contentNode = itemContentNode { contentNode.containingItem.isExtractedToContextPreview = true contentNode.containingItem.isExtractedToContextPreviewUpdated?(true) contentNode.containingItem.willUpdateIsExtractedToContextPreview?(true, transition) contentNode.containingItem.layoutUpdated = { [weak self] _, animation in - guard let strongSelf = self, let _ = strongSelf.contentNode else { + guard let strongSelf = self else { return } @@ -1127,11 +1316,22 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) } - if let contentNode = contentNode { + if let contentNode = itemContentNode { currentContentScreenFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view) } else { return } + case let .controller(source): + if let putBackInfo = source.transitionInfo() { + let _ = putBackInfo + /*self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)*/ + + //TODO: + currentContentScreenFrame = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)) + } else { + return + } } self.animatingOutState = AnimatingOutState( @@ -1140,20 +1340,20 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view) - let animationInContentYDistance: CGFloat + var animationInContentYDistance: CGFloat switch result { case .default, .custom: animationInContentYDistance = currentContentLocalFrame.minY - currentContentScreenFrame.minY case .dismissWithoutContent: animationInContentYDistance = 0.0 - if let contentNode = contentNode { + if let contentNode = itemContentNode { contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) } } let actionsVerticalTransitionDirection: CGFloat - if let contentNode = contentNode { + if let contentNode = itemContentNode { if contentNode.frame.minY < self.actionsContainerNode.frame.minY { actionsVerticalTransitionDirection = -1.0 } else { @@ -1167,9 +1367,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } - let completeWithActionStack = contentNode == nil + let completeWithActionStack = itemContentNode == nil && controllerContentNode == nil - if let contentNode = contentNode { + if let contentNode = itemContentNode { contentNode.containingItem.willUpdateIsExtractedToContextPreview?(false, transition) var animationInContentXDistance: CGFloat = 0.0 @@ -1205,7 +1405,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentNode.containingItem.isExtractedToContextPreview = false contentNode.containingItem.isExtractedToContextPreviewUpdated?(false) - if let strongSelf = self, let contentNode = strongSelf.contentNode { + if let strongSelf = self, let contentNode = strongSelf.itemContentNode { switch contentNode.containingItem { case let .node(containingNode): containingNode.addSubnode(containingNode.contentNode) @@ -1219,6 +1419,38 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } ) } + if let contentNode = controllerContentNode { + if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { + let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) + animationInContentYDistance = contentRect.midY - sourcePoint.y + } else { + animationInContentYDistance = 0.0 + } + + contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.8, removeOnCompletion: false, completion: { _ in + completion() + }) + contentNode.layer.animate( + from: 0.0 as NSNumber, + to: -animationInContentYDistance as NSNumber, + keyPath: "position.y", + timingFunction: timingFunction, + duration: duration, + delay: 0.0, + removeOnCompletion: false, + additive: true + ) + contentNode.layer.animate( + from: 1.0 as NSNumber, + to: 0.01 as NSNumber, + keyPath: "transform.scale", + timingFunction: timingFunction, + duration: duration, + delay: 0.0, + removeOnCompletion: false, + additive: false + ) + } self.actionsContainerNode.layer.animateAlpha(from: self.actionsContainerNode.alpha, to: 0.0, duration: duration, removeOnCompletion: false) self.actionsContainerNode.layer.animate( @@ -1257,8 +1489,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo additive: true ) - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) - if let reactionContextNode = self.reactionContextNode { reactionContextNode.animateOut(to: currentContentScreenFrame, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) } diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index caf6c57fbe6..5137890b60e 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -14,7 +14,9 @@ enum ContextControllerPresentationNodeStateTransition { } protocol ContextControllerPresentationNode: ASDisplayNode { - func replaceItems(items: ContextController.Items, animated: Bool) + var ready: Signal { get } + + func replaceItems(items: ContextController.Items, animated: Bool?) func pushItems(items: ContextController.Items) func popItems() func wantsDisplayBelowKeyboard() -> Bool diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift new file mode 100644 index 00000000000..a12b0cd5016 --- /dev/null +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -0,0 +1,672 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramPresentationData +import SwiftSignalKit +import TelegramCore +import ReactionSelectionNode +import ComponentFlow +import TabSelectorComponent +import ComponentDisplayAdapters + +final class ContextSourceContainer: ASDisplayNode { + final class Source { + weak var controller: ContextController? + + let id: AnyHashable + let title: String + let source: ContextContentSource + + private var _presentationNode: ContextControllerPresentationNode? + var presentationNode: ContextControllerPresentationNode { + return self._presentationNode! + } + + var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition? + + var validLayout: ContainerViewLayout? + var presentationData: PresentationData? + var delayLayoutUpdate: Bool = false + var isAnimatingOut: Bool = false + + let itemsDisposable = MetaDisposable() + + let ready = Promise() + private let contentReady = Promise() + private let actionsReady = Promise() + + init( + controller: ContextController, + id: AnyHashable, + title: String, + source: ContextContentSource, + items: Signal + ) { + self.controller = controller + self.id = id + self.title = title + self.source = source + + self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get()) + |> map { a, b -> Bool in + return a && b + } + |> distinctUntilChanged) + + switch source { + case let .location(source): + self.contentReady.set(.single(true)) + + let presentationNode = ContextControllerExtractedPresentationNode( + getController: { [weak self] in + guard let self else { + return nil + } + return self.controller + }, + requestUpdate: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + }, + requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in + guard let self else { + return + } + if let controller = self.controller { + controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) + } + }, + requestDismiss: { [weak self] result in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.dismissedForCancel?() + controller.controllerNode.beginDismiss(result) + }, + requestAnimateOut: { [weak self] result, completion in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.animateOut(result: result, completion: completion) + }, + source: .location(source) + ) + self._presentationNode = presentationNode + case let .reference(source): + self.contentReady.set(.single(true)) + + let presentationNode = ContextControllerExtractedPresentationNode( + getController: { [weak self] in + guard let self else { + return nil + } + return self.controller + }, + requestUpdate: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + }, + requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in + guard let self else { + return + } + if let controller = self.controller { + controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) + } + }, + requestDismiss: { [weak self] result in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.dismissedForCancel?() + controller.controllerNode.beginDismiss(result) + }, + requestAnimateOut: { [weak self] result, completion in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.animateOut(result: result, completion: completion) + }, + source: .reference(source) + ) + self._presentationNode = presentationNode + case let .extracted(source): + self.contentReady.set(.single(true)) + + let presentationNode = ContextControllerExtractedPresentationNode( + getController: { [weak self] in + guard let self else { + return nil + } + return self.controller + }, + requestUpdate: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + }, + requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in + guard let self else { + return + } + if let controller = self.controller { + controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) + } + }, + requestDismiss: { [weak self] result in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.dismissedForCancel?() + controller.controllerNode.beginDismiss(result) + }, + requestAnimateOut: { [weak self] result, completion in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.animateOut(result: result, completion: completion) + }, + source: .extracted(source) + ) + self._presentationNode = presentationNode + case let .controller(source): + self.contentReady.set(source.controller.ready.get()) + + let presentationNode = ContextControllerExtractedPresentationNode( + getController: { [weak self] in + guard let self else { + return nil + } + return self.controller + }, + requestUpdate: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + }, + requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in + guard let self else { + return + } + if let controller = self.controller { + controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) + } + }, + requestDismiss: { [weak self] result in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.dismissedForCancel?() + controller.controllerNode.beginDismiss(result) + }, + requestAnimateOut: { [weak self] result, completion in + guard let self, let controller = self.controller else { + return + } + controller.controllerNode.animateOut(result: result, completion: completion) + }, + source: .controller(source) + ) + self._presentationNode = presentationNode + } + + self.itemsDisposable.set((items |> deliverOnMainQueue).start(next: { [weak self] items in + guard let self else { + return + } + + self.setItems(items: items, animated: nil) + self.actionsReady.set(.single(true)) + })) + } + + deinit { + self.itemsDisposable.dispose() + } + + func animateIn() { + self.currentPresentationStateTransition = .animateIn + self.update(transition: .animated(duration: 0.5, curve: .spring)) + } + + func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) { + self.currentPresentationStateTransition = .animateOut(result: result, completion: completion) + if let _ = self.validLayout { + if case let .custom(transition) = result { + self.delayLayoutUpdate = true + Queue.mainQueue().after(0.1) { + self.delayLayoutUpdate = false + self.update(transition: transition) + self.isAnimatingOut = true + } + } else { + self.update(transition: .animated(duration: 0.35, curve: .easeInOut)) + } + } + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + self.presentationNode.addRelativeContentOffset(offset, transition: transition) + } + + func cancelReactionAnimation() { + self.presentationNode.cancelReactionAnimation() + } + + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { + self.presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) + } + + func setItems(items: Signal, animated: Bool) { + self.itemsDisposable.set((items + |> deliverOnMainQueue).start(next: { [weak self] items in + guard let self else { + return + } + self.setItems(items: items, animated: animated) + })) + } + + func setItems(items: ContextController.Items, animated: Bool?) { + self.presentationNode.replaceItems(items: items, animated: animated) + } + + func pushItems(items: Signal) { + self.itemsDisposable.set((items + |> deliverOnMainQueue).start(next: { [weak self] items in + guard let self else { + return + } + self.presentationNode.pushItems(items: items) + })) + } + + func popItems() { + self.presentationNode.popItems() + } + + func update(transition: ContainedViewLayoutTransition) { + guard let validLayout = self.validLayout else { + return + } + guard let presentationData = self.presentationData else { + return + } + self.update(presentationData: presentationData, layout: validLayout, transition: transition) + } + + func update( + presentationData: PresentationData, + layout: ContainerViewLayout, + transition: ContainedViewLayoutTransition + ) { + if self.isAnimatingOut || self.delayLayoutUpdate { + return + } + + self.validLayout = layout + self.presentationData = presentationData + + let presentationStateTransition = self.currentPresentationStateTransition + self.currentPresentationStateTransition = .none + + self.presentationNode.update( + presentationData: presentationData, + layout: layout, + transition: transition, + stateTransition: presentationStateTransition + ) + } + } + + private struct PanState { + var fraction: CGFloat + + init(fraction: CGFloat) { + self.fraction = fraction + } + } + + private weak var controller: ContextController? + + private let backgroundNode: NavigationBackgroundNode + + var sources: [Source] = [] + var activeIndex: Int = 0 + + private var tabSelector: ComponentView? + + private var presentationData: PresentationData? + private var validLayout: ContainerViewLayout? + private var panState: PanState? + + let ready = Promise() + + var activeSource: Source? { + if self.activeIndex >= self.sources.count { + return nil + } + return self.sources[self.activeIndex] + } + + var overlayWantsToBeBelowKeyboard: Bool { + return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false + } + + init(controller: ContextController, configuration: ContextController.Configuration) { + self.controller = controller + + self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false) + + super.init() + + self.addSubnode(self.backgroundNode) + + for i in 0 ..< configuration.sources.count { + let source = configuration.sources[i] + + let mappedSource = Source( + controller: controller, + id: source.id, + title: source.title, + source: source.source, + items: source.items + ) + self.sources.append(mappedSource) + self.addSubnode(mappedSource.presentationNode) + + if source.id == configuration.initialId { + self.activeIndex = i + } + } + + self.ready.set(self.sources[self.activeIndex].ready.get()) + + self.view.addGestureRecognizer(InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in + guard let self else { + return [] + } + if self.sources.count <= 1 { + return [] + } + return [.left, .right] + })) + } + + @objc private func panGesture(_ recognizer: InteractiveTransitionGestureRecognizer) { + switch recognizer.state { + case .began, .changed: + if let validLayout = self.validLayout { + var translationX = recognizer.translation(in: self.view).x + if self.activeIndex == 0 && translationX > 0.0 { + translationX = scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0) + } else if self.activeIndex == self.sources.count - 1 && translationX < 0.0 { + translationX = -scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0) + } + + self.panState = PanState(fraction: translationX / validLayout.size.width) + self.update(transition: .immediate) + } + case .cancelled, .ended: + if let panState = self.panState { + self.panState = nil + + let velocity = recognizer.velocity(in: self.view) + + var nextIndex = self.activeIndex + if panState.fraction < -0.4 { + nextIndex += 1 + } else if panState.fraction > 0.4 { + nextIndex -= 1 + } else if abs(velocity.x) >= 200.0 { + if velocity.x < 0.0 { + nextIndex += 1 + } else { + nextIndex -= 1 + } + } + if nextIndex < 0 { + nextIndex = 0 + } + if nextIndex > self.sources.count - 1 { + nextIndex = self.sources.count - 1 + } + if nextIndex != self.activeIndex { + self.activeIndex = nextIndex + } + + self.update(transition: .animated(duration: 0.4, curve: .spring)) + } + default: + break + } + } + + func animateIn() { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if let activeSource = self.activeSource { + activeSource.animateIn() + } + if let tabSelectorView = self.tabSelector?.view { + tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) { + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + if let tabSelectorView = self.tabSelector?.view { + tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + if let activeSource = self.activeSource { + activeSource.animateOut(result: result, completion: completion) + } else { + completion() + } + } + + func highlightGestureMoved(location: CGPoint, hover: Bool) { + if self.activeIndex >= self.sources.count { + return + } + self.sources[self.activeIndex].presentationNode.highlightGestureMoved(location: location, hover: hover) + } + + func highlightGestureFinished(performAction: Bool) { + if self.activeIndex >= self.sources.count { + return + } + self.sources[self.activeIndex].presentationNode.highlightGestureFinished(performAction: performAction) + } + + func performHighlightedAction() { + self.activeSource?.presentationNode.highlightGestureFinished(performAction: true) + } + + func decreaseHighlightedIndex() { + self.activeSource?.presentationNode.decreaseHighlightedIndex() + } + + func increaseHighlightedIndex() { + self.activeSource?.presentationNode.increaseHighlightedIndex() + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if let activeSource = self.activeSource { + activeSource.addRelativeContentOffset(offset, transition: transition) + } + } + + func cancelReactionAnimation() { + if let activeSource = self.activeSource { + activeSource.cancelReactionAnimation() + } + } + + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { + if let activeSource = self.activeSource { + activeSource.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) + } else { + completion() + } + } + + func setItems(items: Signal, animated: Bool) { + if let activeSource = self.activeSource { + activeSource.setItems(items: items, animated: animated) + } + } + + func pushItems(items: Signal) { + if let activeSource = self.activeSource { + activeSource.pushItems(items: items) + } + } + + func popItems() { + if let activeSource = self.activeSource { + activeSource.popItems() + } + } + + private func update(transition: ContainedViewLayoutTransition) { + if let presentationData = self.presentationData, let validLayout = self.validLayout { + self.update(presentationData: presentationData, layout: validLayout, transition: transition) + } + } + + func update( + presentationData: PresentationData, + layout: ContainerViewLayout, + transition: ContainedViewLayoutTransition + ) { + self.presentationData = presentationData + self.validLayout = layout + + var childLayout = layout + + if let activeSource = self.activeSource { + switch activeSource.source { + case .location, .reference: + self.backgroundNode.updateColor( + color: .clear, + enableBlur: false, + forceKeepBlur: false, + transition: .immediate + ) + case .extracted: + self.backgroundNode.updateColor( + color: presentationData.theme.contextMenu.dimColor, + enableBlur: true, + forceKeepBlur: true, + transition: .immediate + ) + case .controller: + if case .regular = layout.metrics.widthClass { + self.backgroundNode.updateColor( + color: UIColor(white: 0.0, alpha: 0.4), + enableBlur: false, + forceKeepBlur: false, + transition: .immediate + ) + } else { + self.backgroundNode.updateColor( + color: presentationData.theme.contextMenu.dimColor, + enableBlur: true, + forceKeepBlur: true, + transition: .immediate + ) + } + } + } + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) + self.backgroundNode.update(size: layout.size, transition: transition) + + if self.sources.count > 1 { + let tabSelector: ComponentView + if let current = self.tabSelector { + tabSelector = current + } else { + tabSelector = ComponentView() + self.tabSelector = tabSelector + } + let mappedItems = self.sources.map { source -> TabSelectorComponent.Item in + return TabSelectorComponent.Item(id: source.id, title: source.title) + } + let tabSelectorSize = tabSelector.update( + transition: Transition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.8), + selection: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1) + ), + customLayout: TabSelectorComponent.CustomLayout( + spacing: 9.0 + ), + items: mappedItems, + selectedId: self.activeSource?.id, + setSelectedId: { [weak self] id in + guard let self else { + return + } + if let index = self.sources.firstIndex(where: { $0.id == id }) { + self.activeIndex = index + self.update(transition: .animated(duration: 0.4, curve: .spring)) + } + } + )), + environment: {}, + containerSize: CGSize(width: layout.size.width, height: 44.0) + ) + childLayout.intrinsicInsets.bottom += 30.0 + + if let tabSelectorView = tabSelector.view { + if tabSelectorView.superview == nil { + self.view.addSubview(tabSelectorView) + } + transition.updateFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - tabSelectorSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height), size: tabSelectorSize)) + } + } else if let tabSelector = self.tabSelector { + self.tabSelector = nil + tabSelector.view?.removeFromSuperview() + } + + for i in 0 ..< self.sources.count { + var itemFrame = CGRect(origin: CGPoint(), size: childLayout.size) + itemFrame.origin.x += CGFloat(i - self.activeIndex) * childLayout.size.width + if let panState = self.panState { + itemFrame.origin.x += panState.fraction * childLayout.size.width + } + + let itemTransition = transition + itemTransition.updateFrame(node: self.sources[i].presentationNode, frame: itemFrame) + self.sources[i].update( + presentationData: presentationData, + layout: childLayout, + transition: itemTransition + ) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let tabSelectorView = self.tabSelector?.view { + if let result = tabSelectorView.hitTest(self.view.convert(point, to: tabSelectorView), with: event) { + return result + } + } + + guard let activeSource = self.activeSource else { + return nil + } + return activeSource.presentationNode.view.hitTest(point, with: event) + } +} diff --git a/submodules/ContextUI/Sources/PeekController.swift b/submodules/ContextUI/Sources/PeekController.swift index 615979d884a..3d9c459a333 100644 --- a/submodules/ContextUI/Sources/PeekController.swift +++ b/submodules/ContextUI/Sources/PeekController.swift @@ -38,7 +38,7 @@ public final class PeekController: ViewController, ContextControllerProtocol { return nil } - public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?) { + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) { } public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { @@ -126,4 +126,8 @@ public final class PeekController: ViewController, ContextControllerProtocol { self?.presentingViewController?.dismiss(animated: false, completion: nil) }) } + + public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { + self.dismiss(completion: completion) + } } diff --git a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift index 8e5cadf795c..9c9cd26cf9e 100644 --- a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift +++ b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift @@ -60,7 +60,7 @@ private func loadCountryCodes() -> [(String, Int)] { private let countryCodes: [(String, Int)] = loadCountryCodes() -func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, [Int])] { +public func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, [Int])] { let locale = localeWithStrings(strings) var result: [((String, String), String, [Int])] = [] for country in AuthorizationSequenceCountrySelectionController.countries() { @@ -159,7 +159,7 @@ private func matchStringTokens(_ tokens: [Data], with other: [Data]) -> Bool { return false } -private func searchCountries(items: [((String, String), String, [Int])], query: String) -> [((String, String), String, Int)] { +public func searchCountries(items: [((String, String), String, [Int])], query: String) -> [((String, String), String, Int)] { let queryTokens = stringTokens(query.lowercased()) var result: [((String, String), String, Int)] = [] diff --git a/submodules/CountrySelectionUI/Sources/CountryList.swift b/submodules/CountrySelectionUI/Sources/CountryList.swift index 5d2dd965c34..87efdf89bbd 100644 --- a/submodules/CountrySelectionUI/Sources/CountryList.swift +++ b/submodules/CountrySelectionUI/Sources/CountryList.swift @@ -1,5 +1,6 @@ import Foundation import AppBundle +import TelegramStringFormatting public func emojiFlagForISOCountryCode(_ countryCode: String) -> String { if countryCode.count != 2 { @@ -18,12 +19,7 @@ public func emojiFlagForISOCountryCode(_ countryCode: String) -> String { return "" } - let base : UInt32 = 127397 - var s = "" - for v in countryCode.unicodeScalars { - s.unicodeScalars.append(UnicodeScalar(base + v.value)!) - } - return String(s) + return flagEmoji(countryCode: countryCode) } private func loadCountriesInfo() -> [(Int, String, String)] { diff --git a/submodules/Crc32/Package.swift b/submodules/Crc32/Package.swift index 9a73c17566e..aec41a7bf59 100644 --- a/submodules/Crc32/Package.swift +++ b/submodules/Crc32/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "Crc32", - platforms: [.macOS(.v10_12)], + 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/CryptoUtils/Package.swift b/submodules/CryptoUtils/Package.swift index 42be8a32567..40e68bc9bb4 100644 --- a/submodules/CryptoUtils/Package.swift +++ b/submodules/CryptoUtils/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "CryptoUtils", - platforms: [.macOS(.v10_12)], + 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/DatePickerNode/Sources/DatePickerNode.swift b/submodules/DatePickerNode/Sources/DatePickerNode.swift index 54d9a85b3fd..1395eb7b2b7 100644 --- a/submodules/DatePickerNode/Sources/DatePickerNode.swift +++ b/submodules/DatePickerNode/Sources/DatePickerNode.swift @@ -17,8 +17,9 @@ public final class DatePickerTheme: Equatable { public let selectionTextColor: UIColor public let separatorColor: UIColor public let segmentedControlTheme: SegmentedControlTheme + public let overallDarkAppearance: Bool - public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectionTextColor: UIColor, separatorColor: UIColor, segmentedControlTheme: SegmentedControlTheme) { + public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectionTextColor: UIColor, separatorColor: UIColor, segmentedControlTheme: SegmentedControlTheme, overallDarkAppearance: Bool) { self.backgroundColor = backgroundColor self.textColor = textColor self.secondaryTextColor = secondaryTextColor @@ -28,6 +29,7 @@ public final class DatePickerTheme: Equatable { self.selectionTextColor = selectionTextColor self.separatorColor = separatorColor self.segmentedControlTheme = segmentedControlTheme + self.overallDarkAppearance = overallDarkAppearance } public static func ==(lhs: DatePickerTheme, rhs: DatePickerTheme) -> Bool { @@ -52,13 +54,16 @@ public final class DatePickerTheme: Equatable { if lhs.separatorColor != rhs.separatorColor { return false } + if lhs.overallDarkAppearance != rhs.overallDarkAppearance { + return false + } return true } } public extension DatePickerTheme { convenience init(theme: PresentationTheme) { - self.init(backgroundColor: theme.list.itemBlocksBackgroundColor, textColor: theme.list.itemPrimaryTextColor, secondaryTextColor: theme.list.itemSecondaryTextColor, accentColor: theme.list.itemAccentColor, disabledColor: theme.list.itemDisabledTextColor, selectionColor: theme.list.itemCheckColors.fillColor, selectionTextColor: theme.list.itemCheckColors.foregroundColor, separatorColor: theme.list.itemBlocksSeparatorColor, segmentedControlTheme: SegmentedControlTheme(theme: theme)) + self.init(backgroundColor: theme.list.itemBlocksBackgroundColor, textColor: theme.list.itemPrimaryTextColor, secondaryTextColor: theme.list.itemSecondaryTextColor, accentColor: theme.list.itemAccentColor, disabledColor: theme.list.itemDisabledTextColor, selectionColor: theme.list.itemCheckColors.fillColor, selectionTextColor: theme.list.itemCheckColors.foregroundColor, separatorColor: theme.list.itemBlocksSeparatorColor, segmentedControlTheme: SegmentedControlTheme(theme: theme), overallDarkAppearance: theme.overallDarkAppearance) } } @@ -948,6 +953,7 @@ private class TimeInputView: UIView, UIKeyInput { } var keyboardType: UIKeyboardType = .numberPad + var keyboardAppearance: UIKeyboardAppearance = .default var text: String = "" var hasText: Bool { @@ -1284,7 +1290,7 @@ private final class TimePickerNode: ASDisplayNode { self.update() } - + private func updateTime() { switch self.dateTimeFormat.timeFormat { case .military: @@ -1338,6 +1344,8 @@ private final class TimePickerNode: ASDisplayNode { self.view.disablesInteractiveModalDismiss = true self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))) + + (self.inputNode.view as? TimeInputView)?.keyboardAppearance = self.theme.overallDarkAppearance ? .dark : .default } private func handleTextInput(_ input: String) { diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index a92e6a2790e..429e4688e7d 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -81,6 +81,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case redactSensitiveData(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool) + case unidirectionalSwipeToReply(Bool) case crashOnSlowQueries(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool) case clearTips(PresentationTheme) @@ -134,7 +135,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logs.rawValue case .logToFile, .logToConsole, .redactSensitiveData: return DebugControllerSection.logging.rawValue - case .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries, .crashOnMemoryPressure: + case .keepChatNavigationStack, .skipReadHistory, .unidirectionalSwipeToReply, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: return DebugControllerSection.experiments.rawValue @@ -187,46 +188,48 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 14 case .skipReadHistory: return 15 - case .crashOnSlowQueries: + case .unidirectionalSwipeToReply: return 16 - case .crashOnMemoryPressure: + case .crashOnSlowQueries: return 17 - case .clearTips: + case .crashOnMemoryPressure: return 18 - case .resetNotifications: + case .clearTips: return 19 - case .crash: + case .resetNotifications: return 20 - case .resetData: + case .crash: return 21 - case .resetDatabase: + case .resetData: return 22 - case .resetDatabaseAndCache: + case .resetDatabase: return 23 - case .resetHoles: + case .resetDatabaseAndCache: return 24 - case .reindexUnread: + case .resetHoles: return 25 - case .resetCacheIndex: + case .reindexUnread: return 26 - case .reindexCache: + case .resetCacheIndex: return 27 - case .resetBiometricsData: + case .reindexCache: return 28 - case .resetWebViewCache: + case .resetBiometricsData: return 29 - case .optimizeDatabase: + case .resetWebViewCache: return 30 - case .photoPreview: + case .optimizeDatabase: return 31 - case .knockoutWallpaper: + case .photoPreview: return 32 - case .experimentalCompatibility: + case .knockoutWallpaper: return 33 - case .enableDebugDataDisplay: + case .experimentalCompatibility: return 34 - case .acceleratedStickers: + case .enableDebugDataDisplay: return 35 + case .acceleratedStickers: + return 36 case .inlineForums: return 37 case .localTranscription: @@ -348,7 +351,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -428,7 +431,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -514,7 +517,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -598,7 +601,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -683,7 +686,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -730,7 +733,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let messages = logs.map { (name, path) -> EnqueueMessage in let id = Int64.random(in: Int64.min ... Int64.max) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() } @@ -761,7 +764,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let messages = logs.map { (name, path) -> EnqueueMessage in let id = Int64.random(in: Int64.min ... Int64.max) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + return .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() } @@ -870,7 +873,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-All.txt.zip")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -925,7 +928,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: allStatsData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(allStatsData.count), attributes: [.FileName(fileName: "StorageReport.txt")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -983,6 +986,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { 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 + var settings = settings + settings.unidirectionalSwipeToReply = value + return settings + }).start() + }) case let .crashOnSlowQueries(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in @@ -1450,6 +1461,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present if isMainApp { entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) + entries.append(.unidirectionalSwipeToReply(experimentalSettings.unidirectionalSwipeToReply)) } entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure)) @@ -1661,7 +1673,7 @@ public func triggerDebugSendLogsUI(context: AccountContext, additionalInfo: Stri context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 14efefd1136..6dedb8bc910 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -738,7 +738,11 @@ public extension ContainedViewLayoutTransition { case .immediate: layer.removeAnimation(forKey: "position") layer.removeAnimation(forKey: "bounds") - layer.frame = frame + if let view = layer.delegate as? UIView { + view.frame = frame + } else { + layer.frame = frame + } if let completion = completion { completion(true) } @@ -789,7 +793,7 @@ public extension ContainedViewLayoutTransition { } } - func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + func updateAlpha(layer: CALayer, alpha: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.opacity.isEqual(to: Float(alpha)) { if let completion = completion { completion(true) @@ -804,7 +808,12 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - let previousAlpha = layer.opacity + let previousAlpha: Float + if beginWithCurrentState, let presentation = layer.presentation() { + previousAlpha = presentation.opacity + } else { + previousAlpha = layer.opacity + } layer.opacity = Float(alpha) layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { @@ -1133,7 +1142,7 @@ public extension ContainedViewLayoutTransition { previousTransform = layer.transform } layer.transform = transform - layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in + layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in completion?(value) }) } @@ -1198,37 +1207,13 @@ public extension ContainedViewLayoutTransition { } } - func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, delay: Double = 0.0, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) completion?(true) return } - let t = node.layer.sublayerTransform - let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) - if currentScale.isEqual(to: scale) { - if let completion = completion { - completion(true) - } - return - } - - switch self { - case .immediate: - node.layer.removeAnimation(forKey: "sublayerTransform") - node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) - if let completion = completion { - completion(true) - } - case let .animated(duration, curve): - node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) - node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { - result in - if let completion = completion { - completion(result) - } - }) - } + self.updateSublayerTransformScale(layer: node.layer, scale: CGPoint(x: scale, y: scale), beginWithCurrentState: beginWithCurrentState, completion: completion) } func updateSublayerTransformScaleAdditive(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { @@ -2085,7 +2070,21 @@ public final class ControlledTransition { if layer.bounds == bounds { return } - let fromValue = layer.presentation()?.bounds ?? layer.bounds + let fromValue: CGRect + if let animationKeys = layer.animationKeys(), animationKeys.contains(where: { key in + guard let animation = layer.animation(forKey: key) as? CAPropertyAnimation else { + return false + } + if animation.keyPath == "bounds" { + return true + } else { + return false + } + }) { + fromValue = layer.presentation()?.bounds ?? layer.bounds + } else { + fromValue = layer.bounds + } layer.bounds = bounds self.add(animation: ControlledTransitionProperty( layer: layer, diff --git a/submodules/Display/Source/ContextControllerSourceNode.swift b/submodules/Display/Source/ContextControllerSourceNode.swift index 0bfd0ce673f..b01479bfe59 100644 --- a/submodules/Display/Source/ContextControllerSourceNode.swift +++ b/submodules/Display/Source/ContextControllerSourceNode.swift @@ -2,6 +2,12 @@ import Foundation import AsyncDisplayKit open class ContextControllerSourceNode: ContextReferenceContentNode { + public enum ShouldBegin { + case none + case `default` + case customActivationProcess + } + public private(set) var contextGesture: ContextGesture? public var isGestureEnabled: Bool = true { @@ -18,15 +24,19 @@ open class ContextControllerSourceNode: ContextReferenceContentNode { public var activated: ((ContextGesture, CGPoint) -> Void)? public var shouldBegin: ((CGPoint) -> Bool)? + public var shouldBeginWithCustomActivationProcess: ((CGPoint) -> ShouldBegin)? public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)? public weak var additionalActivationProgressLayer: CALayer? public var targetNodeForActivationProgress: ASDisplayNode? public var targetNodeForActivationProgressContentRect: CGRect? + private var ignoreCurrentActivationProcess: Bool = false + public func cancelGesture() { self.contextGesture?.cancel() self.contextGesture?.isEnabled = false self.contextGesture?.isEnabled = self.isGestureEnabled + self.ignoreCurrentActivationProcess = false } override open func didLoad() { @@ -40,10 +50,26 @@ open class ContextControllerSourceNode: ContextReferenceContentNode { contextGesture.isEnabled = self.isGestureEnabled contextGesture.shouldBegin = { [weak self] point in - guard let strongSelf = self, !strongSelf.bounds.width.isZero else { + guard let self, !self.bounds.width.isZero else { return false } - return strongSelf.shouldBegin?(point) ?? true + if let shouldBeginWithCustomActivationProcess = self.shouldBeginWithCustomActivationProcess { + let result = shouldBeginWithCustomActivationProcess(point) + switch result { + case .none: + self.ignoreCurrentActivationProcess = false + return false + case .default: + self.ignoreCurrentActivationProcess = false + return true + case .customActivationProcess: + self.ignoreCurrentActivationProcess = true + return true + } + } else { + self.ignoreCurrentActivationProcess = false + return self.shouldBegin?(point) ?? true + } } contextGesture.activationProgress = { [weak self] progress, update in @@ -52,7 +78,7 @@ open class ContextControllerSourceNode: ContextReferenceContentNode { } if let customActivationProgress = strongSelf.customActivationProgress { customActivationProgress(progress, update) - } else if strongSelf.animateScale { + } else if strongSelf.animateScale && !strongSelf.ignoreCurrentActivationProcess { let targetNode: ASDisplayNode let targetContentRect: CGRect if let targetNodeForActivationProgress = strongSelf.targetNodeForActivationProgress { diff --git a/submodules/Display/Source/ContextMenuContainerNode.swift b/submodules/Display/Source/ContextMenuContainerNode.swift index 85472178db0..445654a9443 100644 --- a/submodules/Display/Source/ContextMenuContainerNode.swift +++ b/submodules/Display/Source/ContextMenuContainerNode.swift @@ -17,22 +17,32 @@ private final class ContextMenuContainerMaskView: UIView { public final class ContextMenuContainerNode: ASDisplayNode { private var cachedMaskParams: CachedMaskParams? private let maskView = ContextMenuContainerMaskView() + public let containerNode: ASDisplayNode public var relativeArrowPosition: (CGFloat, Bool)? private var effectView: UIVisualEffectView? - public init(blurred: Bool) { + public init(isBlurred: Bool, isDark: Bool) { + self.containerNode = ASDisplayNode() + super.init() - if blurred { - let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - self.view.addSubview(effectView) + if isBlurred { + let effectView = UIVisualEffectView(effect: UIBlurEffect(style: isDark ? .dark : .light)) + self.containerNode.view.addSubview(effectView) self.effectView = effectView } else { - self.backgroundColor = UIColor(rgb: 0x8c8e8e) + self.containerNode.backgroundColor = isDark ? UIColor(rgb: 0x2f2f2f) : UIColor(rgb: 0xF8F8F6) } - self.view.mask = self.maskView + + self.layer.shadowColor = UIColor.black.cgColor + self.layer.shadowRadius = 10.0 + self.layer.shadowOpacity = 0.2 + self.layer.shadowOffset = CGSize(width: 0.0, height: 5.0) + + self.containerNode.view.mask = self.maskView + self.addSubnode(self.containerNode) } override public func didLoad() { @@ -48,6 +58,8 @@ public final class ContextMenuContainerNode: ASDisplayNode { } public func updateLayout(transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.containerNode, frame: self.bounds) + self.effectView?.frame = self.bounds let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true) @@ -87,6 +99,13 @@ public final class ContextMenuContainerNode: ASDisplayNode { } layer.path = path.cgPath } + + if case let .animated(duration, curve) = transition, let previousPath = self.layer.shadowPath { + self.layer.shadowPath = path.cgPath + self.layer.animate(from: previousPath, to: path.cgPath, keyPath: "shadowPath", timingFunction: curve.timingFunction, duration: duration) + } else { + self.layer.shadowPath = path.cgPath + } } } } diff --git a/submodules/Display/Source/ContextMenuController.swift b/submodules/Display/Source/ContextMenuController.swift index b19ae87d7b3..f48e5b69e4a 100644 --- a/submodules/Display/Source/ContextMenuController.swift +++ b/submodules/Display/Source/ContextMenuController.swift @@ -3,8 +3,8 @@ import UIKit import AsyncDisplayKit public final class ContextMenuControllerPresentationArguments { - fileprivate let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)? - fileprivate let bounce: Bool + public let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)? + public let bounce: Bool public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?, bounce: Bool = true) { self.sourceNodeAndRect = sourceNodeAndRect @@ -12,91 +12,46 @@ public final class ContextMenuControllerPresentationArguments { } } -public final class ContextMenuController: ViewController, KeyShortcutResponder, StandalonePresentableController { - private var contextMenuNode: ContextMenuNode { - return self.displayNode as! ContextMenuNode - } - - public var keyShortcuts: [KeyShortcut] { - return [KeyShortcut(input: UIKeyCommand.inputEscape, action: { [weak self] in - if let strongSelf = self { - strongSelf.dismiss() - } - })] - } - private let actions: [ContextMenuAction] - private let catchTapsOutside: Bool - private let hasHapticFeedback: Bool - private let blurred: Bool - - private var layout: ContainerViewLayout? - - public var centerHorizontally = false - public var dismissed: (() -> Void)? - - public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false, blurred: Bool = false) { +public protocol ContextMenuController: ViewController, StandalonePresentableController { + var centerHorizontally: Bool { get set } + var dismissed: (() -> Void)? { get set } + var dismissOnTap: ((UIView, CGPoint) -> Bool)? { get set } +} + +public struct ContextMenuControllerArguments { + public var actions: [ContextMenuAction] + public var catchTapsOutside: Bool + public var hasHapticFeedback: Bool + public var blurred: Bool + public var skipCoordnateConversion: Bool + public var isDark: Bool + + public init(actions: [ContextMenuAction], catchTapsOutside: Bool, hasHapticFeedback: Bool, blurred: Bool, skipCoordnateConversion: Bool, isDark: Bool) { self.actions = actions self.catchTapsOutside = catchTapsOutside self.hasHapticFeedback = hasHapticFeedback self.blurred = blurred - - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func loadDisplayNode() { - self.displayNode = ContextMenuNode(actions: self.actions, dismiss: { [weak self] in - self?.dismissed?() - self?.contextMenuNode.animateOut(bounce: (self?.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { - self?.presentingViewController?.dismiss(animated: false) - }) - }, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback, blurred: self.blurred) - self.displayNodeDidLoad() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.contextMenuNode.animateIn(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true) - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.dismissed?() - self.contextMenuNode.animateOut(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false) - }) - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.contextMenuNode.centerHorizontally = self.centerHorizontally - if self.layout != nil && self.layout! != layout { - self.dismissed?() - self.contextMenuNode.animateOut(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false) - }) - } else { - self.layout = layout - - if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() { - self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil) - self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil) - } else { - self.contextMenuNode.sourceRect = nil - self.contextMenuNode.containerRect = nil - } - - self.contextMenuNode.containerLayoutUpdated(layout, transition: transition) - } - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + self.skipCoordnateConversion = skipCoordnateConversion + self.isDark = isDark } } + +private var contextMenuControllerProvider: ((ContextMenuControllerArguments) -> ContextMenuController)? + +public func setContextMenuControllerProvider(_ f: @escaping (ContextMenuControllerArguments) -> ContextMenuController) { + contextMenuControllerProvider = f +} + +public func makeContextMenuController(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false, blurred: Bool = false, isDark: Bool = true, skipCoordnateConversion: Bool = false) -> ContextMenuController { + guard let contextMenuControllerProvider = contextMenuControllerProvider else { + preconditionFailure() + } + return contextMenuControllerProvider(ContextMenuControllerArguments( + actions: actions, + catchTapsOutside: catchTapsOutside, + hasHapticFeedback: hasHapticFeedback, + blurred: blurred, + skipCoordnateConversion: skipCoordnateConversion, + isDark: isDark + )) +} diff --git a/submodules/Display/Source/ContextMenuNode.swift b/submodules/Display/Source/ContextMenuNode.swift deleted file mode 100644 index 57c63520ad9..00000000000 --- a/submodules/Display/Source/ContextMenuNode.swift +++ /dev/null @@ -1,275 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -private func generateShadowImage() -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 1.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(white: 0.18, alpha: 1.0).cgColor) - context.setFillColor(UIColor(white: 0.18, alpha: 1.0).cgColor) - context.fill(CGRect(origin: CGPoint(x: -15.0, y: 0.0), size: CGSize(width: 30.0, height: 1.0))) - }) -} - -private final class ContextMenuContentScrollNode: ASDisplayNode { - var contentWidth: CGFloat = 0.0 - - private var initialOffset: CGFloat = 0.0 - - private let leftShadow: ASImageNode - private let rightShadow: ASImageNode - private let leftOverscrollNode: ASDisplayNode - private let rightOverscrollNode: ASDisplayNode - let contentNode: ASDisplayNode - - override init() { - self.contentNode = ASDisplayNode() - - let shadowImage = generateShadowImage() - - self.leftShadow = ASImageNode() - self.leftShadow.displaysAsynchronously = false - self.leftShadow.image = shadowImage - self.rightShadow = ASImageNode() - self.rightShadow.displaysAsynchronously = false - self.rightShadow.image = shadowImage - self.rightShadow.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) - - self.leftOverscrollNode = ASDisplayNode() - //self.leftOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) - self.rightOverscrollNode = ASDisplayNode() - //self.rightOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) - - super.init() - - self.contentNode.addSubnode(self.leftOverscrollNode) - self.contentNode.addSubnode(self.rightOverscrollNode) - self.addSubnode(self.contentNode) - - self.addSubnode(self.leftShadow) - self.addSubnode(self.rightShadow) - } - - override func didLoad() { - super.didLoad() - - //let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - //self.view.addGestureRecognizer(panRecognizer) - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .began: - self.initialOffset = self.contentNode.bounds.origin.x - case .changed: - var bounds = self.contentNode.bounds - bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x - if bounds.origin.x > self.contentWidth - bounds.size.width { - let delta = bounds.origin.x - (self.contentWidth - bounds.size.width) - bounds.origin.x = self.contentWidth - bounds.size.width + ((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) - } - if bounds.origin.x < 0.0 { - let delta = -bounds.origin.x - bounds.origin.x = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0) - } - self.contentNode.bounds = bounds - self.updateShadows(.immediate) - case .ended, .cancelled: - var bounds = self.contentNode.bounds - bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x - - var duration = 0.4 - - if abs(bounds.origin.x - self.initialOffset) > 10.0 || abs(recognizer.velocity(in: self.view).x) > 100.0 { - duration = 0.2 - if bounds.origin.x < self.initialOffset { - bounds.origin.x = 0.0 - } else { - bounds.origin.x = self.contentWidth - bounds.size.width - } - } else { - bounds.origin.x = self.initialOffset - } - - if bounds.origin.x > self.contentWidth - bounds.size.width { - bounds.origin.x = self.contentWidth - bounds.size.width - } - if bounds.origin.x < 0.0 { - bounds.origin.x = 0.0 - } - let previousBounds = self.contentNode.bounds - self.contentNode.bounds = bounds - self.contentNode.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - self.updateShadows(.animated(duration: duration, curve: .spring)) - default: - break - } - } - - override func layout() { - let bounds = self.bounds - self.contentNode.frame = bounds - self.leftShadow.frame = CGRect(origin: CGPoint(), size: CGSize(width: 30.0, height: bounds.height)) - self.rightShadow.frame = CGRect(origin: CGPoint(x: bounds.size.width - 30.0, y: 0.0), size: CGSize(width: 30.0, height: bounds.height)) - self.leftOverscrollNode.frame = bounds.offsetBy(dx: -bounds.width, dy: 0.0) - self.rightOverscrollNode.frame = bounds.offsetBy(dx: self.contentWidth, dy: 0.0) - self.updateShadows(.immediate) - } - - private func updateShadows(_ transition: ContainedViewLayoutTransition) { - let bounds = self.contentNode.bounds - - let leftAlpha = max(0.0, min(1.0, bounds.minX / 20.0)) - transition.updateAlpha(node: self.leftShadow, alpha: leftAlpha) - - let rightAlpha = max(0.0, min(1.0, (self.contentWidth - bounds.maxX) / 20.0)) - transition.updateAlpha(node: self.rightShadow, alpha: rightAlpha) - } -} - -final class ContextMenuNode: ASDisplayNode { - private let actions: [ContextMenuAction] - private let dismiss: () -> Void - - private let containerNode: ContextMenuContainerNode - private let scrollNode: ContextMenuContentScrollNode - private let actionNodes: [ContextMenuActionNode] - - var sourceRect: CGRect? - var containerRect: CGRect? - var arrowOnBottom: Bool = true - var centerHorizontally: Bool = false - - private var dismissedByTouchOutside = false - private let catchTapsOutside: Bool - - private let feedback: HapticFeedback? - - init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, catchTapsOutside: Bool, hasHapticFeedback: Bool = false, blurred: Bool = false) { - self.actions = actions - self.dismiss = dismiss - self.catchTapsOutside = catchTapsOutside - - self.containerNode = ContextMenuContainerNode(blurred: blurred) - self.scrollNode = ContextMenuContentScrollNode() - - self.actionNodes = actions.map { action in - return ContextMenuActionNode(action: action, blurred: blurred) - } - - if hasHapticFeedback { - self.feedback = HapticFeedback() - self.feedback?.prepareImpact(.light) - } else { - self.feedback = nil - } - - super.init() - - self.containerNode.addSubnode(self.scrollNode) - - self.addSubnode(self.containerNode) - let dismissNode = { - dismiss() - } - for actionNode in self.actionNodes { - actionNode.dismiss = dismissNode - self.scrollNode.contentNode.addSubnode(actionNode) - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - var unboundActionsWidth: CGFloat = 0.0 - let actionSeparatorWidth: CGFloat = UIScreenPixel - for actionNode in self.actionNodes { - if !unboundActionsWidth.isZero { - unboundActionsWidth += actionSeparatorWidth - } - let actionSize = actionNode.measure(CGSize(width: layout.size.width, height: 54.0)) - actionNode.frame = CGRect(origin: CGPoint(x: unboundActionsWidth, y: 0.0), size: actionSize) - unboundActionsWidth += actionSize.width - } - - let maxActionsWidth = layout.size.width - 20.0 - let actionsWidth = min(unboundActionsWidth, maxActionsWidth) - - let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize()) - let containerRect: CGRect = self.containerRect ?? self.bounds - - let insets = layout.insets(options: [.statusBar, .input]) - - let verticalOrigin: CGFloat - var arrowOnBottom = true - if sourceRect.minY - 54.0 > containerRect.minY + insets.top { - verticalOrigin = sourceRect.minY - 54.0 - } else { - verticalOrigin = min(containerRect.maxY - insets.bottom - 54.0, sourceRect.maxY) - arrowOnBottom = false - } - self.arrowOnBottom = arrowOnBottom - - let horizontalOrigin: CGFloat = floor(max(8.0, min(self.centerHorizontally ? sourceRect.midX - actionsWidth / 2.0 : max(sourceRect.minX + 8.0, sourceRect.midX - actionsWidth / 2.0), layout.size.width - actionsWidth - 8.0))) - - self.containerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: CGSize(width: actionsWidth, height: 54.0)) - self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom) - - self.scrollNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: actionsWidth, height: 54.0)) - self.scrollNode.contentWidth = unboundActionsWidth - - self.containerNode.layout() - self.scrollNode.layout() - } - - func animateIn(bounce: Bool) { - if bounce { - self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4) - let containerPosition = self.containerNode.layer.position - 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 - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in - self?.allowsGroupOpacity = false - self?.layer.shouldRasterize = false - }) - - if let feedback = self.feedback { - feedback.impact(.light) - } - } - - func animateOut(bounce: Bool, completion: @escaping () -> Void) { - 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 - completion() - }) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let event = event { - var eventIsPresses = false - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - eventIsPresses = event.type == .presses - } - if event.type == .touches || eventIsPresses { - if !self.containerNode.frame.contains(point) { - if !self.dismissedByTouchOutside { - self.dismissedByTouchOutside = true - self.dismiss() - } - if self.catchTapsOutside { - return self.view - } - return nil - } - } - } - return super.hitTest(point, with: event) - } -} diff --git a/submodules/Display/Source/EditableTextNode.swift b/submodules/Display/Source/EditableTextNode.swift index a5f7d38b4a7..daf1fa18dae 100644 --- a/submodules/Display/Source/EditableTextNode.swift +++ b/submodules/Display/Source/EditableTextNode.swift @@ -48,4 +48,20 @@ public extension UITextView { } return numberOfLines } + + var isRTL: Bool { + if let text = self.text, !text.isEmpty { + let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0) + tagger.string = text + + let lang = tagger.tag(at: 0, scheme: .language, tokenRange: nil, sentenceRange: nil) + if let lang = lang?.rawValue, lang.contains("he") || lang.contains("ar") || lang.contains("fa") { + return true + } else { + return false + } + } else { + return false + } + } } diff --git a/submodules/Display/Source/LinkHighlightingNode.swift b/submodules/Display/Source/LinkHighlightingNode.swift index 131875494c9..22e128e0524 100644 --- a/submodules/Display/Source/LinkHighlightingNode.swift +++ b/submodules/Display/Source/LinkHighlightingNode.swift @@ -52,7 +52,7 @@ private func drawConnectingCorner(context: CGContext, color: UIColor, at point: } } -public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) { +public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, strokeWidth: CGFloat = 2.0, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) { if rects.isEmpty { return (CGPoint(), nil) } @@ -66,10 +66,15 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, bottomRight.y = max(bottomRight.y, rects[i].maxY) } - topLeft.x -= inset - topLeft.y -= inset - bottomRight.x += inset * 2.0 - bottomRight.y += inset * 2.0 + var drawingInset = inset + if stroke { + drawingInset += 2.0 + } + + topLeft.x -= drawingInset + topLeft.y -= drawingInset + bottomRight.x += drawingInset * 2.0 + bottomRight.y += drawingInset * 2.0 return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -84,7 +89,7 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, if stroke { context.setStrokeColor(color.cgColor) - context.setLineWidth(2.0) + context.setLineWidth(strokeWidth) context.strokePath() } else { context.fillPath() @@ -212,7 +217,7 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, if stroke { context.setStrokeColor(color.cgColor) - context.setLineWidth(2.0) + context.setLineWidth(strokeWidth) context.strokePath() } else { context.fillPath() @@ -306,6 +311,8 @@ public final class LinkHighlightingNode: ASDisplayNode { public var outerRadius: CGFloat = 4.0 public var inset: CGFloat = 2.0 public var useModernPathCalculation: Bool = false + public var borderOnly: Bool = false + public var strokeWidth: CGFloat = 1.0 private var _color: UIColor public var color: UIColor { @@ -352,7 +359,7 @@ public final class LinkHighlightingNode: ASDisplayNode { if self.rects.isEmpty { self.imageNode.image = nil } - let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, useModernPathCalculation: self.useModernPathCalculation) + let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, stroke: self.borderOnly, strokeWidth: self.strokeWidth, useModernPathCalculation: self.useModernPathCalculation) if let image = image { self.imageNode.image = image diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index c0b1854353d..54f60309962 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -1819,7 +1819,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if let index = node.index { nodes.append(.Node(index: index, frame: node.apparentFrame, referenceNode: QueueLocalObject(queue: Queue.mainQueue(), generate: { return node - }))) + }), newNode: nil)) } else { nodes.append(.Placeholder(frame: node.apparentFrame)) } @@ -2000,10 +2000,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } switch state.nodes[i] { - case let .Node(_, frame, referenceNode): - state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode) - case .Placeholder: - break + case let .Node(_, frame, referenceNode, newNode): + state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode, newNode: newNode) + case .Placeholder: + break } i += 1 } @@ -2039,10 +2039,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } switch state.nodes[i] { - case let .Node(_, frame, referenceNode): - state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode) - case .Placeholder: - break + case let .Node(_, frame, referenceNode, newNode): + state.nodes[i] = .Node(index: updatedIndex, frame: frame, referenceNode: referenceNode, newNode: newNode) + case .Placeholder: + break } } } @@ -2097,7 +2097,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture var updateIndices = updateAdjacentItemsIndices if widthUpdated { - for case let .Node(index, _, _) in updatedState.nodes { + for case let .Node(index, _, _, _) in updatedState.nodes { updateIndices.insert(index) } } @@ -2205,7 +2205,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture var i = 0 for node in state.nodes { - if case let .Node(index, _, referenceNode) = node , index == nodeIndex { + if case let .Node(index, _, referenceNode, _) = node, index == nodeIndex { if let referenceNode = referenceNode { continueWithoutNode = false var controlledTransition: ControlledTransition? @@ -2932,7 +2932,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if let index = itemNode.index, index == scrollToItem.index { let insets = self.insets// updateSizeAndInsets?.insets ?? self.insets - let offset: CGFloat + var offset: CGFloat switch scrollToItem.position { case let .bottom(additionalOffset): offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom + additionalOffset @@ -2944,10 +2944,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture offset = insets.top + floor(((self.visibleSize.height - insets.bottom - insets.top) - itemNode.frame.size.height) / 2.0) - itemNode.apparentFrame.minY } else { switch overflow { - case .top: - offset = insets.top - itemNode.apparentFrame.minY - case .bottom: - offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + case .top: + offset = insets.top - itemNode.apparentFrame.minY + case .bottom: + offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + case let .custom(getOverflow): + let overflow = getOverflow(itemNode) + offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.insets.top + offset += overflow + offset -= floor((self.visibleSize.height - insets.bottom - insets.top) * 0.5) } } case .visible: diff --git a/submodules/Display/Source/ListViewIntermediateState.swift b/submodules/Display/Source/ListViewIntermediateState.swift index ede0f7509e5..70680497eba 100644 --- a/submodules/Display/Source/ListViewIntermediateState.swift +++ b/submodules/Display/Source/ListViewIntermediateState.swift @@ -2,9 +2,33 @@ import Foundation import UIKit import SwiftSignalKit -public enum ListViewCenterScrollPositionOverflow { +public enum ListViewCenterScrollPositionOverflow: Equatable { case top case bottom + case custom((ListViewItemNode) -> CGFloat) + + public static func ==(lhs: ListViewCenterScrollPositionOverflow, rhs: ListViewCenterScrollPositionOverflow) -> Bool { + switch lhs { + case .top: + if case .top = rhs { + return true + } else { + return false + } + case .bottom: + if case .bottom = rhs { + return true + } else { + return false + } + case .custom: + if case .custom = rhs { + return true + } else { + return false + } + } + } } public enum ListViewScrollPosition: Equatable { @@ -216,12 +240,12 @@ struct PendingNode { } enum ListViewStateNode { - case Node(index: Int, frame: CGRect, referenceNode: QueueLocalObject?) + case Node(index: Int, frame: CGRect, referenceNode: QueueLocalObject?, newNode: QueueLocalObject?) case Placeholder(frame: CGRect) var index: Int? { switch self { - case .Node(let index, _, _): + case let .Node(index, _, _, _): return index case .Placeholder(_): return nil @@ -231,15 +255,15 @@ enum ListViewStateNode { var frame: CGRect { get { switch self { - case .Node(_, let frame, _): + case let .Node(_, frame, _, _): return frame case .Placeholder(let frame): return frame } } set(value) { switch self { - case let .Node(index, _, referenceNode): - self = .Node(index: index, frame: value, referenceNode: referenceNode) + case let .Node(index, _, referenceNode, newNode): + self = .Node(index: index, frame: value, referenceNode: referenceNode, newNode: newNode) case .Placeholder(_): self = .Placeholder(frame: value) } @@ -289,7 +313,7 @@ struct ListViewState { if let (fixedIndex, fixedPosition) = self.scrollPosition { for node in self.nodes { if let index = node.index, index == fixedIndex { - let offset: CGFloat + var offset: CGFloat switch fixedPosition { case let .bottom(additionalOffset): offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + additionalOffset @@ -301,10 +325,23 @@ struct ListViewState { offset = self.insets.top + floor((contentAreaHeight - node.frame.size.height) / 2.0) - node.frame.minY } else { switch overflow { - case .top: + case .top: + offset = self.insets.top - node.frame.minY + case .bottom: + offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + case let .custom(getOverflow): + if Thread.isMainThread, case let .Node(_, _, referenceNode, newNode) = node, let listNode = referenceNode?.syncWith({ $0 }) ?? newNode?.syncWith({ $0 }) { + let overflow = getOverflow(listNode) + if overflow == 0.0 { + offset = self.insets.top - node.frame.minY + } else { + offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + offset += overflow + offset -= floor((self.visibleSize.height - self.insets.bottom - self.insets.top) * 0.5) + } + } else { offset = self.insets.top - node.frame.minY - case .bottom: - offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + } } } case .visible: @@ -717,7 +754,7 @@ struct ListViewState { let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: animated ? 0.0 : layout.size.height)) operations.append(.InsertNode(index: insertionIndex, offsetDirection: offsetDirection, animated: animated, node: node, layout: layout, apply: apply)) - self.nodes.insert(.Node(index: itemIndex, frame: nodeFrame, referenceNode: nil), at: insertionIndex) + self.nodes.insert(.Node(index: itemIndex, frame: nodeFrame, referenceNode: nil, newNode: node), at: insertionIndex) if !animated { switch offsetDirection { @@ -762,7 +799,7 @@ struct ListViewState { mutating func removeNodeAtIndex(_ index: Int, direction: ListViewItemOperationDirectionHint?, animated: Bool, operations: inout [ListViewStateOperation]) { let node = self.nodes[index] - if case let .Node(_, _, referenceNode) = node { + if case let .Node(_, _, referenceNode, _) = node { let nodeFrame = node.frame self.nodes.remove(at: index) let offsetDirection: ListViewInsertionOffsetDirection diff --git a/submodules/Display/Source/ListViewItemHeader.swift b/submodules/Display/Source/ListViewItemHeader.swift index 01f0f96e8c9..a2551cdb0d5 100644 --- a/submodules/Display/Source/ListViewItemHeader.swift +++ b/submodules/Display/Source/ListViewItemHeader.swift @@ -29,7 +29,7 @@ open class ListViewItemHeaderNode: ASDisplayNode { private var isFlashingOnScrolling = false weak var attachedToItemNode: ListViewItemNode? - var item: ListViewItemHeader? + public var item: ListViewItemHeader? func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) { self.internalStickLocationDistanceFactor = factor @@ -124,7 +124,7 @@ open class ListViewItemHeaderNode: ASDisplayNode { private var cachedLayout: (CGSize, CGFloat, CGFloat)? - func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + public func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { var update = false if let cachedLayout = self.cachedLayout { if cachedLayout.0 != size || cachedLayout.1 != leftInset || cachedLayout.2 != rightInset { diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index 2d4bf47b6a3..b71fb7f1ffe 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -142,6 +142,12 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega } return .right }) + /*panRecognizer.dynamicEdgeWidth = { [weak self] _ in + guard let self, let controller = self.controllers.last, let value = controller.interactiveNavivationGestureEdgeWidth else { + return .constant(16.0) + } + return value + }*/ if #available(iOS 13.4, *) { panRecognizer.allowedScrollTypesMask = .continuous } diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index ed71d68591c..d31ac9f6c16 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -59,7 +59,10 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL modalStack[modalStack.count - 1].controllers.append(controller) } } else if !modalStack.isEmpty { - controller._presentedInModal = true + if modalStack[modalStack.count - 1].isFlat { + } else { + controller._presentedInModal = true + } if modalStack[modalStack.count - 1].isStandalone { modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) } else { diff --git a/submodules/Display/Source/PresentationContext.swift b/submodules/Display/Source/PresentationContext.swift index b396a44582a..804cabd50a1 100644 --- a/submodules/Display/Source/PresentationContext.swift +++ b/submodules/Display/Source/PresentationContext.swift @@ -306,7 +306,7 @@ public final class PresentationContext { UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil) } - func hitTest(view: UIView, point: CGPoint, with event: UIEvent?) -> UIView? { + public func hitTest(view: UIView, point: CGPoint, with event: UIEvent?) -> UIView? { for (controller, _) in self.controllers.reversed() { if controller.isViewLoaded { if let result = controller.view.hitTest(view.convert(point, to: controller.view), with: event) { diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 32517738dfc..6d3c386c9fa 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -2,9 +2,14 @@ import Foundation import UIKit import AsyncDisplayKit import CoreText +import AppBundle private let defaultFont = UIFont.systemFont(ofSize: 15.0) +private let quoteIcon: UIImage = { + return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed() +}() + private final class TextNodeStrikethrough { let range: NSRange let frame: CGRect @@ -62,21 +67,74 @@ public struct TextRangeRectEdge: Equatable { } } +public final class TextNodeBlockQuoteData: NSObject { + public let title: NSAttributedString? + public let color: UIColor + public let secondaryColor: UIColor? + public let tertiaryColor: UIColor? + + public init(title: NSAttributedString?, color: UIColor, secondaryColor: UIColor?, tertiaryColor: UIColor?) { + self.title = title + self.color = color + self.secondaryColor = secondaryColor + self.tertiaryColor = tertiaryColor + + super.init() + } + + override public func isEqual(_ object: Any?) -> Bool { + guard let other = object as? TextNodeBlockQuoteData else { + return false + } + + if let lhsTitle = self.title, let rhsTitle = other.title { + if !lhsTitle.isEqual(to: rhsTitle) { + return false + } + } else if (self.title == nil) != (other.title == nil) { + return false + } + if !self.color.isEqual(other.color) { + return false + } + if let lhsSecondaryColor = self.secondaryColor, let rhsSecondaryColor = other.secondaryColor { + if !lhsSecondaryColor.isEqual(rhsSecondaryColor) { + return false + } + } else if (self.secondaryColor == nil) != (other.secondaryColor == nil) { + return false + } + if let lhsTertiaryColor = self.tertiaryColor, let rhsTertiaryColor = other.tertiaryColor { + if !lhsTertiaryColor.isEqual(rhsTertiaryColor) { + return false + } + } else if (self.tertiaryColor == nil) != (other.tertiaryColor == nil) { + return false + } + + return true + } +} + private final class TextNodeLine { let line: CTLine - let frame: CGRect - let range: NSRange + var frame: CGRect + let ascent: CGFloat + let descent: CGFloat + let range: NSRange? let isRTL: Bool - let strikethroughs: [TextNodeStrikethrough] - let spoilers: [TextNodeSpoiler] - let spoilerWords: [TextNodeSpoiler] - let embeddedItems: [TextNodeEmbeddedItem] - let attachments: [TextNodeAttachment] + var strikethroughs: [TextNodeStrikethrough] + var spoilers: [TextNodeSpoiler] + var spoilerWords: [TextNodeSpoiler] + var embeddedItems: [TextNodeEmbeddedItem] + var attachments: [TextNodeAttachment] let additionalTrailingLine: (CTLine, Double)? - init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem], attachments: [TextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { + init(line: CTLine, frame: CGRect, ascent: CGFloat, descent: CGFloat, range: NSRange?, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem], attachments: [TextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { self.line = line self.frame = frame + self.ascent = ascent + self.descent = descent self.range = range self.isRTL = isRTL self.strikethroughs = strikethroughs @@ -90,9 +148,15 @@ private final class TextNodeLine { private final class TextNodeBlockQuote { let frame: CGRect + let tintColor: UIColor + let secondaryTintColor: UIColor? + let tertiaryTintColor: UIColor? - init(frame: CGRect) { + init(frame: CGRect, tintColor: UIColor, secondaryTintColor: UIColor?, tertiaryTintColor: UIColor?) { self.frame = frame + self.tintColor = tintColor + self.secondaryTintColor = secondaryTintColor + self.tertiaryTintColor = tertiaryTintColor } } @@ -402,7 +466,14 @@ public final class TextNodeLayout: NSObject { public var trailingLineWidth: CGFloat { if let lastLine = self.lines.last { - return lastLine.frame.maxX + var width = lastLine.frame.maxX + + for blockQuote in self.blockQuotes { + if lastLine.frame.intersects(blockQuote.frame) { + width = max(width, ceil(blockQuote.frame.maxX) + 2.0) + } + } + return width } else { return 0.0 } @@ -424,7 +495,7 @@ public final class TextNodeLayout: NSObject { var closestLine: (Int, CGRect, CGFloat)? for line in self.lines { lineIndex += 1 - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -492,7 +563,7 @@ public final class TextNodeLayout: NSObject { var lineIndex = -1 for line in self.lines { lineIndex += 1 - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -559,7 +630,7 @@ public final class TextNodeLayout: NSObject { } } if index >= 0 && index < attributedString.length { - if index < line.range.location + line.range.length { + if let range = line.range, index < range.location + range.length { return (index, attributedString.attributes(at: index, effectiveRange: nil)) } } @@ -569,7 +640,7 @@ public final class TextNodeLayout: NSObject { lineIndex = -1 for line in self.lines { lineIndex += 1 - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -636,7 +707,7 @@ public final class TextNodeLayout: NSObject { } } if index >= 0 && index < attributedString.length { - if index < line.range.location + line.range.length { + if let range = line.range, index < range.location + range.length { return (index, attributedString.attributes(at: index, effectiveRange: nil)) } } @@ -667,14 +738,17 @@ public final class TextNodeLayout: NSObject { var rects: [CGRect] = [] let range = NSRange(stringRange, in: searchText) for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location { + if lineRange.location != rangeValue.location { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.length { + if lineRange.location + lineRange.length != rangeValue.length { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -682,7 +756,7 @@ public final class TextNodeLayout: NSObject { rightOffset = ceil(secondaryOffset) } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) let width = abs(rightOffset - leftOffset) @@ -707,6 +781,17 @@ public final class TextNodeLayout: NSObject { return nil } + public func attributeSubstringWithRange(name: String, index: Int) -> (String, String, NSRange)? { + if let attributedString = self.attributedString { + var range = NSRange() + let _ = attributedString.attribute(NSAttributedString.Key(rawValue: name), at: index, effectiveRange: &range) + if range.length != 0 { + return ((attributedString.string as NSString).substring(with: range), attributedString.string, range) + } + } + return nil + } + public func allAttributeRects(name: String) -> [(Any, CGRect)] { guard let attributedString = self.attributedString else { return [] @@ -716,14 +801,17 @@ public final class TextNodeLayout: NSObject { if let value = value, range.length != 0 { var coveringRect = CGRect() for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location { + if lineRange.location != rangeValue.location { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.length { + if lineRange.location + lineRange.length != rangeValue.length { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -732,7 +820,7 @@ public final class TextNodeLayout: NSObject { } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -765,14 +853,17 @@ public final class TextNodeLayout: NSObject { if range.length != 0 { var rects: [(CGRect, CGRect)] = [] for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location || line.isRTL { + if lineRange.location != rangeValue.location || line.isRTL { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.length || line.isRTL { + if lineRange.location + lineRange.length != rangeValue.length || line.isRTL { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -780,7 +871,7 @@ public final class TextNodeLayout: NSObject { rightOffset = ceil(secondaryOffset) } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) @@ -806,14 +897,17 @@ public final class TextNodeLayout: NSObject { var startEdge: TextRangeRectEdge? var endEdge: TextRangeRectEdge? for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location || line.isRTL { + if lineRange.location != rangeValue.location || line.isRTL { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.upperBound || line.isRTL { + if lineRange.location + lineRange.length != rangeValue.upperBound || line.isRTL { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -821,19 +915,19 @@ public final class TextNodeLayout: NSObject { rightOffset = ceil(secondaryOffset) } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) let width = max(0.0, abs(rightOffset - leftOffset)) - if line.range.contains(range.lowerBound) { + if rangeValue.contains(range.lowerBound) { let offsetX = floor(CTLineGetOffsetForStringIndex(line.line, range.lowerBound, nil)) startEdge = TextRangeRectEdge(x: lineFrame.minX + offsetX, y: lineFrame.minY, height: lineFrame.height) } - if line.range.contains(range.upperBound - 1) { + if rangeValue.contains(range.upperBound - 1) { let offsetX: CGFloat - if line.range.upperBound == range.upperBound { + if rangeValue.upperBound == range.upperBound { offsetX = lineFrame.maxX } else { var secondaryOffset: CGFloat = 0.0 @@ -967,6 +1061,78 @@ public final class TextAccessibilityOverlayNode: ASDisplayNode { } } +private func addSpoiler(line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + line.spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) +} + +private func addSpoilerWord(line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + line.spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) +} + +private func addEmbeddedItem(item: AnyHashable, line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + line.embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) +} + +private func addAttachment(attachment: UIImage, line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + line.attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment)) +} + open class TextNode: ASDisplayNode { public internal(set) var cachedLayout: TextNodeLayout? @@ -998,6 +1164,10 @@ open class TextNode: ASDisplayNode { return self.cachedLayout?.attributeSubstring(name: name, index: index) } + public func attributeSubstringWithRange(name: String, index: Int) -> (String, String, NSRange)? { + return self.cachedLayout?.attributeSubstringWithRange(name: name, index: index) + } + public func attributeRects(name: String, at index: Int) -> [CGRect]? { if let cachedLayout = self.cachedLayout { return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 } @@ -1022,532 +1192,956 @@ open class TextNode: ASDisplayNode { } } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { - if let attributedString = attributedString { - let stringLength = attributedString.length - - let font: CTFont - let resolvedAlignment: NSTextAlignment - - if stringLength != 0 { - if let stringFont = attributedString.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) { - font = stringFont as! CTFont - } else { - font = defaultFont + private static func calculateLayoutV2( + attributedString: NSAttributedString, + minimumNumberOfLines: Int, + maximumNumberOfLines: Int, + truncationType: CTLineTruncationType, + backgroundColor: UIColor?, + constrainedSize: CGSize, + alignment: NSTextAlignment, + verticalAlignment: TextVerticalAlignment, + lineSpacingFactor: CGFloat, + cutout: TextNodeCutout?, + insets: UIEdgeInsets, + lineColor: UIColor?, + textShadowColor: UIColor?, + textShadowBlur: CGFloat?, + textStroke: (UIColor, CGFloat)?, + displaySpoilers: Bool, + displayEmbeddedItemsUnderSpoilers: Bool, + customTruncationToken: NSAttributedString? + ) -> TextNodeLayout { + let blockQuoteLeftInset: CGFloat = 9.0 + let blockQuoteRightInset: CGFloat = 0.0 + let blockQuoteIconInset: CGFloat = 7.0 + + struct StringSegment { + let title: NSAttributedString? + let substring: NSAttributedString + let firstCharacterOffset: Int + let isBlockQuote: Bool + let tintColor: UIColor? + let secondaryTintColor: UIColor? + let tertiaryTintColor: UIColor? + } + var stringSegments: [StringSegment] = [] + + let rawWholeString = attributedString.string as NSString + let wholeStringLength = rawWholeString.length + + var segmentCharacterOffset = 0 + while true { + var found = false + attributedString.enumerateAttribute(NSAttributedString.Key("Attribute__Blockquote"), in: NSRange(location: segmentCharacterOffset, length: wholeStringLength - segmentCharacterOffset), using: { value, effectiveRange, stop in + found = true + stop.pointee = ObjCBool(true) + + if segmentCharacterOffset != effectiveRange.location { + stringSegments.append(StringSegment( + title: nil, + substring: attributedString.attributedSubstring(from: NSRange( + location: segmentCharacterOffset, + length: effectiveRange.location - segmentCharacterOffset + )), + firstCharacterOffset: segmentCharacterOffset, + isBlockQuote: false, + tintColor: nil, + secondaryTintColor: nil, + tertiaryTintColor: nil + )) } - if alignment == .center { - resolvedAlignment = .center - } else { - if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { - resolvedAlignment = paragraphStyle.alignment - } else { - resolvedAlignment = alignment + + if let value = value as? TextNodeBlockQuoteData { + if effectiveRange.length != 0 { + stringSegments.append(StringSegment( + title: value.title, + substring: attributedString.attributedSubstring(from: effectiveRange), + firstCharacterOffset: effectiveRange.location, + isBlockQuote: true, + tintColor: value.color, + secondaryTintColor: value.secondaryColor, + tertiaryTintColor: value.tertiaryColor + )) + } + segmentCharacterOffset = effectiveRange.location + effectiveRange.length + if segmentCharacterOffset < wholeStringLength && rawWholeString.character(at: segmentCharacterOffset) == 0x0a { + segmentCharacterOffset += 1 } + } else { + stringSegments.append(StringSegment( + title: nil, + substring: attributedString.attributedSubstring(from: effectiveRange), + firstCharacterOffset: effectiveRange.location, + isBlockQuote: false, + tintColor: nil, + secondaryTintColor: nil, + tertiaryTintColor: nil + )) + segmentCharacterOffset = effectiveRange.location + effectiveRange.length } - } else { - font = defaultFont - resolvedAlignment = alignment + }) + if !found { + if segmentCharacterOffset != wholeStringLength { + stringSegments.append(StringSegment( + title: nil, + substring: attributedString.attributedSubstring(from: NSRange( + location: segmentCharacterOffset, + length: wholeStringLength - segmentCharacterOffset + )), + firstCharacterOffset: segmentCharacterOffset, + isBlockQuote: false, + tintColor: nil, + secondaryTintColor: nil, + tertiaryTintColor: nil + )) + } + + break } - - let fontAscent = CTFontGetAscent(font) - let fontDescent = CTFontGetDescent(font) - let fontLineHeight = floor(fontAscent + fontDescent) - let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) - + } + + struct CalculatedSegment { + var titleLine: TextNodeLine? var lines: [TextNodeLine] = [] - var blockQuotes: [TextNodeBlockQuote] = [] - - var maybeTypesetter: CTTypesetter? - maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) - if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) - } + var tintColor: UIColor? + var secondaryTintColor: UIColor? + var tertiaryTintColor: UIColor? + var isBlockQuote: Bool = false + var additionalWidth: CGFloat = 0.0 + } + + var calculatedSegments: [CalculatedSegment] = [] + + for segment in stringSegments { + var calculatedSegment = CalculatedSegment() + calculatedSegment.isBlockQuote = segment.isBlockQuote + calculatedSegment.tintColor = segment.tintColor + calculatedSegment.secondaryTintColor = segment.secondaryTintColor + calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor - let typesetter = maybeTypesetter! + let rawSubstring = segment.substring.string as NSString + let substringLength = rawSubstring.length - var lastLineCharacterIndex: CFIndex = 0 - var layoutSize = CGSize() + let segmentTypesetterString = attributedString.attributedSubstring(from: NSRange(location: 0, length: segment.firstCharacterOffset + substringLength)) + let typesetter = CTTypesetterCreateWithAttributedString(segmentTypesetterString as CFAttributedString) - var cutoutEnabled = false - var cutoutMinY: CGFloat = 0.0 - var cutoutMaxY: CGFloat = 0.0 - var cutoutWidth: CGFloat = 0.0 - var cutoutOffset: CGFloat = 0.0 + var currentLineStartIndex = segment.firstCharacterOffset + let segmentEndIndex = segment.firstCharacterOffset + substringLength - var bottomCutoutEnabled = false - var bottomCutoutSize = CGSize() - - if let topLeft = cutout?.topLeft { - cutoutMinY = -fontLineSpacing - cutoutMaxY = topLeft.height + fontLineSpacing - cutoutWidth = topLeft.width - cutoutOffset = cutoutWidth - cutoutEnabled = true - } else if let topRight = cutout?.topRight { - cutoutMinY = -fontLineSpacing - cutoutMaxY = topRight.height + fontLineSpacing - cutoutWidth = topRight.width - cutoutEnabled = true + var constrainedSegmentWidth = constrainedSize.width + var additionalOffsetX: CGFloat = 0.0 + if segment.isBlockQuote { + additionalOffsetX += blockQuoteLeftInset + constrainedSegmentWidth -= additionalOffsetX + blockQuoteLeftInset + blockQuoteRightInset + calculatedSegment.additionalWidth += blockQuoteLeftInset + blockQuoteRightInset } - if let bottomRight = cutout?.bottomRight { - bottomCutoutSize = bottomRight - bottomCutoutEnabled = true - } + var additionalSegmentRightInset = blockQuoteIconInset - let firstLineOffset = floorToScreenPixels(fontDescent) + if let title = segment.title { + let rawTitleLine = CTLineCreateWithAttributedString(title) + if let titleLine = CTLineCreateTruncatedLine(rawTitleLine, constrainedSegmentWidth - additionalSegmentRightInset, .end, nil) { + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = CTLineGetTypographicBounds(titleLine, &lineAscent, &lineDescent, nil) + calculatedSegment.titleLine = TextNodeLine( + line: titleLine, + frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), + ascent: lineAscent, + descent: lineDescent, + range: nil, + isRTL: false, + strikethroughs: [], + spoilers: [], + spoilerWords: [], + embeddedItems: [], + attachments: [], + additionalTrailingLine: nil + ) + additionalSegmentRightInset = 0.0 + } + } - var truncated = false - var first = true while true { - var strikethroughs: [TextNodeStrikethrough] = [] - var spoilers: [TextNodeSpoiler] = [] - var spoilerWords: [TextNodeSpoiler] = [] - var embeddedItems: [TextNodeEmbeddedItem] = [] - var attachments: [TextNodeAttachment] = [] - - var lineConstrainedWidth = constrainedSize.width - var lineConstrainedWidthDelta: CGFloat = 0.0 - var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) - if !first { - lineOriginY += fontLineSpacing - } - var lineCutoutOffset: CGFloat = 0.0 - var lineAdditionalWidth: CGFloat = 0.0 + let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedSegmentWidth - additionalSegmentRightInset) - if cutoutEnabled { - if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { - lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) - lineConstrainedWidthDelta = -cutoutWidth - lineCutoutOffset = cutoutOffset - lineAdditionalWidth = cutoutWidth + if lineCharacterCount != 0 { + let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount)) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil) + + var isRTL = false + let glyphRuns = CTLineGetGlyphRuns(line) as NSArray + if glyphRuns.count != 0 { + let run = glyphRuns[0] as! CTRun + if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { + isRTL = true + } } + + calculatedSegment.lines.append(TextNodeLine( + line: line, + frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), + ascent: lineAscent, + descent: lineDescent, + range: NSRange(location: currentLineStartIndex, length: lineCharacterCount), + isRTL: isRTL && !segment.isBlockQuote, + strikethroughs: [], + spoilers: [], + spoilerWords: [], + embeddedItems: [], + attachments: [], + additionalTrailingLine: nil + )) } - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) + additionalSegmentRightInset = 0.0 - func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) + currentLineStartIndex += lineCharacterCount + + if currentLineStartIndex >= segmentEndIndex { + break + } + } + + calculatedSegments.append(calculatedSegment) + } + + var size = CGSize() + let isTruncated = false + + for segment in calculatedSegments { + if let titleLine = segment.titleLine { + size.width = max(size.width, titleLine.frame.origin.x + titleLine.frame.width + segment.additionalWidth) + } + for line in segment.lines { + size.width = max(size.width, line.frame.origin.x + line.frame.width + segment.additionalWidth) + } + } + + var lines: [TextNodeLine] = [] + + var blockQuotes: [TextNodeBlockQuote] = [] + + for i in 0 ..< calculatedSegments.count { + let segment = calculatedSegments[i] + if i != 0 { + if segment.isBlockQuote { + size.height += 6.0 + } + } else { + if segment.isBlockQuote { + size.height += 7.0 } + } + + let blockMinY = size.height - insets.bottom + var blockWidth: CGFloat = 0.0 + + if let titleLine = segment.titleLine { + titleLine.frame = CGRect(origin: CGPoint(x: titleLine.frame.origin.x, y: -insets.bottom + size.height + titleLine.frame.size.height), size: titleLine.frame.size) + titleLine.frame.size.width += max(0.0, segment.additionalWidth - 2.0) + size.height += titleLine.frame.height + blockWidth = max(blockWidth, titleLine.frame.origin.x + titleLine.frame.width) - func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) + lines.append(titleLine) + } + + for line in segment.lines { + line.frame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: -insets.bottom + size.height + line.frame.size.height), size: line.frame.size) + line.frame.size.width += max(0.0, segment.additionalWidth - 2.0) + //line.frame.size.width = max(blockWidth, line.frame.size.width) + size.height += line.frame.height + blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width) + + if let range = line.range { + attributedString.enumerateAttributes(in: range, options: []) { attributes, range, _ in + if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(line.line, &ascent, &descent, nil) + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedString.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoilerWord(line: line, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + } + } else if startIndex == nil { + startIndex = range.location + } + currentIndex = range.location + range.length + } + + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoilerWord(line: line, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: 0.0) + } + + addSpoiler(line: line, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(line.line, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + line.strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) + } + + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(line.line, &ascent, &descent, nil) + + addEmbeddedItem(item: embeddedItem, line: line, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } + } + + if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(line.line, &ascent, &descent, nil) + + addAttachment(attachment: attachment, line: line, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } } - - spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) } - func addEmbeddedItem(item: AnyHashable, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } + lines.append(line) + } + + let blockMaxY = size.height - insets.bottom + + if i != calculatedSegments.count - 1 { + if segment.isBlockQuote { + size.height += 8.0 + } + } else { + if segment.isBlockQuote { + size.height += 6.0 + } + } + + if segment.isBlockQuote, let tintColor = segment.tintColor { + blockQuotes.append(TextNodeBlockQuote(frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY - 2.0), size: CGSize(width: blockWidth, height: blockMaxY - (blockMinY - 2.0) + 4.0)), tintColor: tintColor, secondaryTintColor: segment.secondaryTintColor, tertiaryTintColor: segment.tertiaryTintColor)) + } + } + + size.width = ceil(size.width) + size.height = ceil(size.height) + + let rawTextSize = size + size.width += insets.left + insets.right + size.height += insets.top + insets.bottom + + return TextNodeLayout( + attributedString: attributedString, + maximumNumberOfLines: maximumNumberOfLines, + truncationType: truncationType, + constrainedSize: constrainedSize, + explicitAlignment: alignment, + resolvedAlignment: alignment, + verticalAlignment: verticalAlignment, + lineSpacing: lineSpacingFactor, + cutout: cutout, + insets: insets, + size: size, + rawTextSize: rawTextSize, + truncated: isTruncated, + firstLineOffset: lines.first?.descent ?? 0.0, + lines: lines, + blockQuotes: blockQuotes, + backgroundColor: backgroundColor, + lineColor: lineColor, + textShadowColor: textShadowColor, + textShadowBlur: textShadowBlur, + textStroke: textStroke, + displaySpoilers: displaySpoilers + ) + } + + static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { + guard let attributedString else { + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) + } + + if maximumNumberOfLines == 0 { + var found = false + attributedString.enumerateAttribute(NSAttributedString.Key("Attribute__Blockquote"), in: NSRange(location: 0, length: attributedString.length), using: { value, effectiveRange, _ in + if let _ = value as? TextNodeBlockQuoteData { + found = true + } + }) + + if found { + return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers, displayEmbeddedItemsUnderSpoilers: displayEmbeddedItemsUnderSpoilers, customTruncationToken: customTruncationToken) + } + } + + let stringLength = attributedString.length + + let font: CTFont + let resolvedAlignment: NSTextAlignment + + if stringLength != 0 { + if let stringFont = attributedString.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) { + font = stringFont as! CTFont + } else { + font = defaultFont + } + if alignment == .center { + resolvedAlignment = .center + } else { + if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { + resolvedAlignment = paragraphStyle.alignment + } else { + resolvedAlignment = alignment + } + } + } else { + font = defaultFont + resolvedAlignment = alignment + } + + let fontAscent = CTFontGetAscent(font) + let fontDescent = CTFontGetDescent(font) + let fontLineHeight = floor(fontAscent + fontDescent) + let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) + + var lines: [TextNodeLine] = [] + let blockQuotes: [TextNodeBlockQuote] = [] + + var maybeTypesetter: CTTypesetter? + maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) + if maybeTypesetter == nil { + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) + } + + let typesetter = maybeTypesetter! + + var lastLineCharacterIndex: CFIndex = 0 + var layoutSize = CGSize() + + var cutoutEnabled = false + var cutoutMinY: CGFloat = 0.0 + var cutoutMaxY: CGFloat = 0.0 + var cutoutWidth: CGFloat = 0.0 + var cutoutOffset: CGFloat = 0.0 + + var bottomCutoutEnabled = false + var bottomCutoutSize = CGSize() - embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) + if let topLeft = cutout?.topLeft { + cutoutMinY = -fontLineSpacing + cutoutMaxY = topLeft.height + fontLineSpacing + cutoutWidth = topLeft.width + cutoutOffset = cutoutWidth + cutoutEnabled = true + } else if let topRight = cutout?.topRight { + cutoutMinY = -fontLineSpacing + cutoutMaxY = topRight.height + fontLineSpacing + cutoutWidth = topRight.width + cutoutEnabled = true + } + + if let bottomRight = cutout?.bottomRight { + bottomCutoutSize = bottomRight + bottomCutoutEnabled = true + } + + let firstLineOffset = floorToScreenPixels(fontDescent) + + var truncated = false + var first = true + while true { + var strikethroughs: [TextNodeStrikethrough] = [] + var spoilers: [TextNodeSpoiler] = [] + var spoilerWords: [TextNodeSpoiler] = [] + var embeddedItems: [TextNodeEmbeddedItem] = [] + var attachments: [TextNodeAttachment] = [] + + var lineConstrainedWidth = constrainedSize.width + var lineConstrainedWidthDelta: CGFloat = 0.0 + var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) + if !first { + lineOriginY += fontLineSpacing + } + var lineCutoutOffset: CGFloat = 0.0 + var lineAdditionalWidth: CGFloat = 0.0 + + if cutoutEnabled { + if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { + lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) + lineConstrainedWidthDelta = -cutoutWidth + lineCutoutOffset = cutoutOffset + lineAdditionalWidth = cutoutWidth + } + } + + let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) + + func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) } - func addAttachment(attachment: UIImage, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) + } + + func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) + } + + func addEmbeddedItem(item: AnyHashable, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) + } + + func addAttachment(attachment: UIImage, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, isAtEndOfTheLine: Bool, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var rightOffset: CGFloat = leftOffset + if isAtEndOfTheLine { + let rawRightOffset = CTLineGetTypographicBounds(line, nil, nil, nil) + rightOffset = floor(rawRightOffset) + } else { var secondaryRightOffset: CGFloat = 0.0 let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) + rightOffset = ceil(rawRightOffset) if !rawRightOffset.isEqual(to: secondaryRightOffset) { rightOffset = ceil(secondaryRightOffset) } - - attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment)) } - var isLastLine = false - if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { - isLastLine = true - } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { - isLastLine = true + attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment)) + } + + var isLastLine = false + if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { + isLastLine = true + } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { + isLastLine = true + } + if isLastLine { + if first { + first = false + } else { + layoutSize.height += fontLineSpacing } - if isLastLine { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - var didClipLinebreak = false - var lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) - let nsString = (attributedString.string as NSString) - for i in lineRange.location ..< (lineRange.location + lineRange.length) { - if nsString.character(at: i) == 0x0a { - lineRange.length = max(0, i - lineRange.location) - didClipLinebreak = true - break - } - } - - var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) - if brokenLineRange.location + brokenLineRange.length > attributedString.length { - brokenLineRange.length = attributedString.length - brokenLineRange.location - } - if lineRange.length == 0 && !didClipLinebreak { + + var didClipLinebreak = false + var lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + let nsString = (attributedString.string as NSString) + for i in lineRange.location ..< (lineRange.location + lineRange.length) { + if nsString.character(at: i) == 0x0a { + lineRange.length = max(0, i - lineRange.location) + didClipLinebreak = true break } - - let coreTextLine: CTLine - let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) - - var lineConstrainedSize = constrainedSize - lineConstrainedSize.width += lineConstrainedWidthDelta - if bottomCutoutEnabled { - lineConstrainedSize.width -= bottomCutoutSize.width - } - - let truncatedTokenString: NSAttributedString - if let customTruncationToken { - if lineRange.length == 0 && customTruncationToken.string.hasPrefix("\u{2026} ") { - truncatedTokenString = customTruncationToken.attributedSubstring(from: NSRange(location: 2, length: customTruncationToken.length - 2)) - } else { - truncatedTokenString = customTruncationToken - } + } + + var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } + if lineRange.length == 0 && !didClipLinebreak { + break + } + + let coreTextLine: CTLine + let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) + + var lineConstrainedSize = constrainedSize + lineConstrainedSize.width += lineConstrainedWidthDelta + if bottomCutoutEnabled { + lineConstrainedSize.width -= bottomCutoutSize.width + } + + let truncatedTokenString: NSAttributedString + if let customTruncationToken { + if lineRange.length == 0 && customTruncationToken.string.hasPrefix("\u{2026} ") { + truncatedTokenString = customTruncationToken.attributedSubstring(from: NSRange(location: 2, length: customTruncationToken.length - 2)) } else { - var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] - truncationTokenAttributes[NSAttributedString.Key.font] = font - truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber - let tokenString = "\u{2026}" - - truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) - } - let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) - let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(truncationToken) - - var effectiveLineRange = brokenLineRange - var additionalTrailingLine: (CTLine, Double)? - - var measureFitWidth = CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) - if customTruncationToken != nil && lineRange.location + lineRange.length < attributedString.length { - measureFitWidth += truncationTokenWidth + truncatedTokenString = customTruncationToken } + } else { + var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] + truncationTokenAttributes[NSAttributedString.Key.font] = font + truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber + let tokenString = "\u{2026}" - if lineRange.length == 0 || measureFitWidth < Double(lineConstrainedSize.width) { - if didClipLinebreak { - if lineRange.length == 0 { - coreTextLine = CTLineCreateWithAttributedString(NSAttributedString()) - } else { - coreTextLine = originalLine - } - additionalTrailingLine = (truncationToken, truncationTokenWidth) - - truncated = true + truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) + } + let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) + let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(truncationToken) + + var effectiveLineRange = brokenLineRange + var additionalTrailingLine: (CTLine, Double)? + + var measureFitWidth = CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) + if customTruncationToken != nil && lineRange.location + lineRange.length < attributedString.length { + measureFitWidth += truncationTokenWidth + } + + if lineRange.length == 0 || measureFitWidth < Double(lineConstrainedSize.width) { + if didClipLinebreak { + if lineRange.length == 0 { + coreTextLine = CTLineCreateWithAttributedString(NSAttributedString()) } else { coreTextLine = originalLine } + additionalTrailingLine = (truncationToken, truncationTokenWidth) + + truncated = true } else { - if customTruncationToken != nil { - let coreTextLine1 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken - let runs = (CTLineGetGlyphRuns(coreTextLine1) as [AnyObject]) as! [CTRun] - var hasTruncationToken = false - for run in runs { - let runRange = CTRunGetStringRange(run) - if runRange.location + runRange.length >= nsString.length { - hasTruncationToken = true - break - } - } - - if hasTruncationToken { - coreTextLine = coreTextLine1 - } else { - let coreTextLine2 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken - coreTextLine = coreTextLine2 - } - } else { - coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken - } - let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] - for run in runs { - let runAttributes: NSDictionary = CTRunGetAttributes(run) - if let _ = runAttributes["CTForegroundColorFromContext"] { - brokenLineRange.length = CTRunGetStringRange(run).location - brokenLineRange.location - break - } - } - if customTruncationToken != nil { - assert(true) - } - effectiveLineRange = CFRange(location: effectiveLineRange.location, length: 0) + coreTextLine = originalLine + } + } else { + if customTruncationToken != nil { + let coreTextLine1 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken + let runs = (CTLineGetGlyphRuns(coreTextLine1) as [AnyObject]) as! [CTRun] + var hasTruncationToken = false for run in runs { let runRange = CTRunGetStringRange(run) - if runRange.location + runRange.length > brokenLineRange.location + brokenLineRange.length { - continue + if runRange.location + runRange.length >= nsString.length { + hasTruncationToken = true + break } - effectiveLineRange.length = max(effectiveLineRange.length, (runRange.location + runRange.length) - effectiveLineRange.location) } - if brokenLineRange.location + brokenLineRange.length > attributedString.length { - brokenLineRange.length = attributedString.length - brokenLineRange.location - } - if effectiveLineRange.location + effectiveLineRange.length > attributedString.length { - effectiveLineRange.length = attributedString.length - effectiveLineRange.location + if hasTruncationToken { + coreTextLine = coreTextLine1 + } else { + let coreTextLine2 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken + coreTextLine = coreTextLine2 } - truncated = true + } else { + coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken } - - var headIndent: CGFloat = 0.0 - if brokenLineRange.location >= 0 && brokenLineRange.length > 0 && brokenLineRange.location + brokenLineRange.length <= attributedString.length { - attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in - if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - var startIndex: Int? - var currentIndex: Int? - - let nsString = (attributedString.string as NSString) - nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in - if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { - if let currentStartIndex = startIndex { - startIndex = nil - let endIndex = range.location - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - } else if startIndex == nil { - startIndex = range.location - } - currentIndex = range.location + range.length - } - - if let currentStartIndex = startIndex, let currentIndex = currentIndex { - startIndex = nil - let endIndex = currentIndex - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) - } - - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { - let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) - let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { - headIndent = paragraphStyle.headIndent - } - - if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { - if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } - } - - if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } + let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] + for run in runs { + let runAttributes: NSDictionary = CTRunGetAttributes(run) + if let _ = runAttributes["CTForegroundColorFromContext"] { + brokenLineRange.length = CTRunGetStringRange(run).location - brokenLineRange.location + break } } - - let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) - let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight + fontLineSpacing - - if let (_, additionalTrailingLineWidth) = additionalTrailingLine { - lineAdditionalWidth += additionalTrailingLineWidth - } - - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + if customTruncationToken != nil { + assert(true) } - - var isRTL = false - let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray - if glyphRuns.count != 0 { - let run = glyphRuns[0] as! CTRun - if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { - isRTL = true + effectiveLineRange = CFRange(location: effectiveLineRange.location, length: 0) + for run in runs { + let runRange = CTRunGetStringRange(run) + if runRange.location + runRange.length > brokenLineRange.location + brokenLineRange.length { + continue } + effectiveLineRange.length = max(effectiveLineRange.length, (runRange.location + runRange.length) - effectiveLineRange.location) } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(effectiveLineRange.location, effectiveLineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems, attachments: attachments, additionalTrailingLine: additionalTrailingLine)) - break - } else { - if lineCharacterCount > 0 { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - var lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) - if lineRange.location + lineRange.length > attributedString.length { - lineRange.length = attributedString.length - lineRange.location - } - if lineRange.length < 0 { - break - } - - let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) - lastLineCharacterIndex += lineCharacterCount - - var headIndent: CGFloat = 0.0 - attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in - if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - var startIndex: Int? - var currentIndex: Int? - - let nsString = (attributedString.string as NSString) - nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in - if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { - if let currentStartIndex = startIndex { - startIndex = nil - let endIndex = range.location - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - } else if startIndex == nil { - startIndex = range.location + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } + if effectiveLineRange.location + effectiveLineRange.length > attributedString.length { + effectiveLineRange.length = attributedString.length - effectiveLineRange.location + } + truncated = true + } + + var headIndent: CGFloat = 0.0 + if brokenLineRange.location >= 0 && brokenLineRange.length > 0 && brokenLineRange.location + brokenLineRange.length <= attributedString.length { + attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in + if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedString.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } - currentIndex = range.location + range.length - } - - if let currentStartIndex = startIndex, let currentIndex = currentIndex { - startIndex = nil - let endIndex = currentIndex - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + } else if startIndex == nil { + startIndex = range.location } - - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { - let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) - let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { - headIndent = paragraphStyle.headIndent + currentIndex = range.location + range.length } - if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { - if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) } - if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { + headIndent = paragraphStyle.headIndent + } + + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } } - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) - let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length - 1, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) + } + } + } + + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) + layoutSize.height += fontLineHeight + fontLineSpacing + + if let (_, additionalTrailingLineWidth) = additionalTrailingLine { + lineAdditionalWidth += additionalTrailingLineWidth + } + + layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) + + if headIndent > 0.0 { + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + } + + var isRTL = false + let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray + if glyphRuns.count != 0 { + let run = glyphRuns[0] as! CTRun + if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { + isRTL = true + } + } + + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(effectiveLineRange.location, effectiveLineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: embeddedItems, + attachments: attachments, + additionalTrailingLine: additionalTrailingLine + )) + break + } else { + if lineCharacterCount > 0 { + if first { + first = false + } else { + layoutSize.height += fontLineSpacing + } + + var lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) + if lineRange.location + lineRange.length > attributedString.length { + lineRange.length = attributedString.length - lineRange.location + } + if lineRange.length < 0 { + break + } + + let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) + lastLineCharacterIndex += lineCharacterCount + + var headIndent: CGFloat = 0.0 + attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedString.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + } + } else if startIndex == nil { + startIndex = range.location + } + currentIndex = range.location + range.length + } + + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + } + + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { + headIndent = paragraphStyle.headIndent } - var isRTL = false - let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray - if glyphRuns.count != 0 { - let run = glyphRuns[0] as! CTRun - if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { - isRTL = true + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems, attachments: attachments, additionalTrailingLine: nil)) - } else { - if !lines.isEmpty { - layoutSize.height += fontLineSpacing + if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length - 1, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) } - break } - } - } - - let rawLayoutSize = layoutSize - if !lines.isEmpty && bottomCutoutEnabled { - let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width - if proposedWidth > layoutSize.width { - if proposedWidth <= constrainedSize.width + .ulpOfOne { - layoutSize.width = proposedWidth - } else { - layoutSize.height += bottomCutoutSize.height + + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) + let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) + layoutSize.height += fontLineHeight + layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth + headIndent) + + if headIndent > 0.0 { + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) } - } - } - - if lines.count < minimumNumberOfLines { - var lineCount = lines.count - while lineCount < minimumNumberOfLines { - if lineCount != 0 { + + var isRTL = false + let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray + if glyphRuns.count != 0 { + let run = glyphRuns[0] as! CTRun + if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { + isRTL = true + } + } + + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(lineRange.location, lineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: embeddedItems, + attachments: attachments, + additionalTrailingLine: nil + )) + } else { + if !lines.isEmpty { layoutSize.height += fontLineSpacing } - layoutSize.height += fontLineHeight - lineCount += 1 + break } } - - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) - } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) } + + let rawLayoutSize = layoutSize + if !lines.isEmpty && bottomCutoutEnabled { + let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width + if proposedWidth > layoutSize.width { + if proposedWidth <= constrainedSize.width + .ulpOfOne { + layoutSize.width = proposedWidth + } else { + layoutSize.height += bottomCutoutSize.height + } + } + } + + if lines.count < minimumNumberOfLines { + var lineCount = lines.count + while lineCount < minimumNumberOfLines { + if lineCount != 0 { + layoutSize.height += fontLineSpacing + } + layoutSize.height += fontLineHeight + lineCount += 1 + } + } + + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { @@ -1582,6 +2176,128 @@ open class TextNode: ASDisplayNode { context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor) context.fill(bounds) + + context.setBlendMode(.normal) + blendMode = .normal + } + + let alignment = layout.resolvedAlignment + var offset = CGPoint(x: layout.insets.left, y: layout.insets.top) + switch layout.verticalAlignment { + case .top: + break + case .middle: + offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top + case .bottom: + offset.y = floor(bounds.height - layout.size.height) + layout.insets.top + } + + if !layout.lines.isEmpty { + offset.y += layout.lines[0].descent + } + + for blockQuote in layout.blockQuotes { + let radius: CGFloat = 4.0 + let lineWidth: CGFloat = 3.0 + + var blockFrame = blockQuote.frame.offsetBy(dx: offset.x + 2.0, dy: offset.y) + if blockFrame.origin.x + blockFrame.size.width > bounds.width - layout.insets.right - 2.0 - 30.0 { + blockFrame.size.width = bounds.width - layout.insets.right - blockFrame.origin.x - 2.0 + } + blockFrame.size.width += 4.0 + blockFrame.origin.x -= 2.0 + + context.setFillColor(blockQuote.tintColor.withMultipliedAlpha(0.1).cgColor) + context.addPath(UIBezierPath(roundedRect: blockFrame, cornerRadius: radius).cgPath) + context.fillPath() + + context.setFillColor(blockQuote.tintColor.cgColor) + + let quoteRect = CGRect(origin: CGPoint(x: blockFrame.maxX - 4.0 - quoteIcon.size.width, y: blockFrame.minY + 4.0), size: quoteIcon.size) + context.saveGState() + context.translateBy(x: quoteRect.midX, y: quoteRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -quoteRect.midX, y: -quoteRect.midY) + context.clip(to: quoteRect, mask: quoteIcon.cgImage!) + context.fill(quoteRect) + context.restoreGState() + context.resetClip() + + let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: lineWidth, height: blockFrame.height)) + context.move(to: CGPoint(x: lineFrame.minX, y: lineFrame.minY + radius)) + context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.minY), tangent2End: CGPoint(x: lineFrame.minX + radius, y: lineFrame.minY), radius: radius) + context.addLine(to: CGPoint(x: lineFrame.minX + radius, y: lineFrame.maxY)) + context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY), tangent2End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY - radius), radius: radius) + context.closePath() + context.clip() + + if let secondaryTintColor = blockQuote.secondaryTintColor { + let isMonochrome = secondaryTintColor.alpha == 0.0 + + let tertiaryTintColor = blockQuote.tertiaryTintColor + let dashHeight: CGFloat = tertiaryTintColor != nil ? 6.0 : 9.0 + + do { + context.saveGState() + + let dashOffset: CGFloat + if let _ = tertiaryTintColor { + dashOffset = isMonochrome ? -7.0 : 5.0 + } else { + dashOffset = isMonochrome ? -4.0 : 5.0 + } + + if isMonochrome { + context.setFillColor(blockQuote.tintColor.withMultipliedAlpha(0.2).cgColor) + context.fill(lineFrame) + context.setFillColor(blockQuote.tintColor.cgColor) + } else { + context.setFillColor(blockQuote.tintColor.cgColor) + context.fill(lineFrame) + context.setFillColor(secondaryTintColor.cgColor) + } + + if let _ = tertiaryTintColor { + context.translateBy(x: 0.0, y: dashHeight) + } + + func drawDashes() { + context.translateBy(x: blockFrame.minX, y: blockFrame.minY + dashOffset) + + var offset = 0.0 + while offset < blockFrame.height { + context.move(to: CGPoint(x: 0.0, y: 3.0)) + context.addLine(to: CGPoint(x: lineWidth, y: 0.0)) + context.addLine(to: CGPoint(x: lineWidth, y: dashHeight)) + context.addLine(to: CGPoint(x: 0.0, y: dashHeight + 3.0)) + context.closePath() + context.fillPath() + + context.translateBy(x: 0.0, y: 18.0) + offset += 18.0 + } + } + + drawDashes() + context.restoreGState() + + if let tertiaryTintColor { + context.saveGState() + if isMonochrome { + context.setFillColor(blockQuote.tintColor.withAlphaComponent(0.4).cgColor) + } else { + context.setFillColor(tertiaryTintColor.cgColor) + } + drawDashes() + context.restoreGState() + } + } + } else { + context.setFillColor(blockQuote.tintColor.cgColor) + context.fill(lineFrame) + } + + context.resetClip() } if let textShadowColor = layout.textShadowColor { @@ -1605,17 +2321,6 @@ open class TextNode: ASDisplayNode { let textPosition = context.textPosition context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) - let alignment = layout.resolvedAlignment - var offset = CGPoint(x: layout.insets.left, y: layout.insets.top) - switch layout.verticalAlignment { - case .top: - break - case .middle: - offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top - case .bottom: - offset.y = floor(bounds.height - layout.size.height) + layout.insets.top - } - for i in 0 ..< layout.lines.count { let line = layout.lines[i] @@ -1632,6 +2337,12 @@ open class TextNode: ASDisplayNode { lineFrame.origin.x += offset.x } } + + //context.setStrokeColor(UIColor.red.cgColor) + //context.stroke(lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.height)) + + lineFrame.origin.y += -line.descent + context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY) if layout.displaySpoilers && !line.spoilers.isEmpty { @@ -1646,7 +2357,9 @@ open class TextNode: ASDisplayNode { } let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray + if glyphRuns.count != 0 { + let hasAttachments = !line.attachments.isEmpty for run in glyphRuns { let run = run as! CTRun let glyphCount = CTRunGetGlyphCount(run) @@ -1679,17 +2392,52 @@ open class TextNode: ASDisplayNode { if fixDoubleEmoji { context.setBlendMode(.normal) } - CTRunDraw(run, context, CFRangeMake(0, glyphCount)) + + if hasAttachments { + let stringRange = CTRunGetStringRange(run) + if line.attachments.contains(where: { $0.range.contains(stringRange.location) }) { + } else { + CTRunDraw(run, context, CFRangeMake(0, glyphCount)) + } + } else { + CTRunDraw(run, context, CFRangeMake(0, glyphCount)) + } + if fixDoubleEmoji { context.setBlendMode(blendMode) } } } + for attachment in line.attachments { + let image = attachment.attachment + var textColor: UIColor? + layout.attributedString?.enumerateAttributes(in: attachment.range, options: []) { attributes, range, _ in + if let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color + } + } + if let textColor { + if let tintedImage = generateTintedImage(image: image, color: textColor) { + let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - tintedImage.size.width * 0.5, y: attachment.frame.midY - tintedImage.size.height * 0.5 + 1.0), size: tintedImage.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + 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(tintedImage.cgImage!, in: imageRect) + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + } + } + } + if !line.strikethroughs.isEmpty { for strikethrough in line.strikethroughs { + guard let lineRange = line.range else { + continue + } var textColor: UIColor? - layout.attributedString?.enumerateAttributes(in: NSMakeRange(line.range.location, line.range.length), options: []) { attributes, range, _ in + layout.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { textColor = color } @@ -1760,34 +2508,6 @@ open class TextNode: ASDisplayNode { } } - var blockQuoteFrames: [CGRect] = [] - var currentBlockQuoteFrame: CGRect? - for blockQuote in layout.blockQuotes { - if let frame = currentBlockQuoteFrame { - if blockQuote.frame.minY - frame.maxY < 20.0 { - currentBlockQuoteFrame = frame.union(blockQuote.frame) - } else { - blockQuoteFrames.append(frame) - currentBlockQuoteFrame = frame - } - } else { - currentBlockQuoteFrame = blockQuote.frame - } - } - - if let frame = currentBlockQuoteFrame { - blockQuoteFrames.append(frame) - } - - for frame in blockQuoteFrames { - if let lineColor = layout.lineColor { - context.setFillColor(lineColor.cgColor) - } - let rect = UIBezierPath(roundedRect: CGRect(x: frame.minX - 9.0, y: frame.minY - 14.0, width: 2.0, height: frame.height), cornerRadius: 1.0) - context.addPath(rect.cgPath) - context.fillPath() - } - context.textMatrix = textMatrix context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y) } @@ -1944,7 +2664,7 @@ open class TextView: UIView { let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) var lines: [TextNodeLine] = [] - var blockQuotes: [TextNodeBlockQuote] = [] + let blockQuotes: [TextNodeBlockQuote] = [] var maybeTypesetter: CTTypesetter? maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) @@ -2175,13 +2895,15 @@ open class TextView: UIView { } } - let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) layoutSize.height += fontLineHeight + fontLineSpacing layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) } var isRTL = false @@ -2193,7 +2915,20 @@ open class TextView: UIView { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [], attachments: attachments, additionalTrailingLine: nil)) + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(lineRange.location, lineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: [], + attachments: attachments, + additionalTrailingLine: nil + )) break } else { if lineCharacterCount > 0 { @@ -2263,13 +2998,15 @@ open class TextView: UIView { } } - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) layoutSize.height += fontLineHeight layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) } var isRTL = false @@ -2281,7 +3018,20 @@ open class TextView: UIView { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [], attachments: attachments, additionalTrailingLine: nil)) + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(lineRange.location, lineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: [], + attachments: attachments, + additionalTrailingLine: nil + )) } else { if !lines.isEmpty { layoutSize.height += fontLineSpacing @@ -2343,6 +3093,7 @@ open class TextView: UIView { context.setBlendMode(.copy) context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor) context.fill(bounds) + context.setBlendMode(.copy) } if let textShadowColor = layout.textShadowColor { diff --git a/submodules/Display/Source/TooltipControllerNode.swift b/submodules/Display/Source/TooltipControllerNode.swift index c21deca4ffd..8740ca13d09 100644 --- a/submodules/Display/Source/TooltipControllerNode.swift +++ b/submodules/Display/Source/TooltipControllerNode.swift @@ -33,8 +33,8 @@ final class TooltipControllerNode: ASDisplayNode { self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource - self.containerNode = ContextMenuContainerNode(blurred: false) - self.containerNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + self.containerNode = ContextMenuContainerNode(isBlurred: false, isDark: true) + self.containerNode.containerNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) self.imageNode = ASImageNode() self.imageNode.image = content.image @@ -57,9 +57,9 @@ final class TooltipControllerNode: ASDisplayNode { super.init() - self.containerNode.addSubnode(self.imageNode) - self.containerNode.addSubnode(self.textNode) - self.contentNode.flatMap { self.containerNode.addSubnode($0) } + self.containerNode.containerNode.addSubnode(self.imageNode) + self.containerNode.containerNode.addSubnode(self.textNode) + self.contentNode.flatMap { self.containerNode.containerNode.addSubnode($0) } self.addSubnode(self.containerNode) } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 619ae34fa3a..8bce9989dd5 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -227,6 +227,10 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } private var navigationBarOrigin: CGFloat = 0.0 + + open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? { + return nil + } open func navigationLayout(layout: ContainerViewLayout) -> NavigationLayout { let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 @@ -574,7 +578,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { (self.navigationController as? NavigationController)?.pushViewController(controller) } - public func replace(with controller: ViewController) { + open func replace(with controller: ViewController) { if let navigationController = self.navigationController as? NavigationController { var controllers = navigationController.viewControllers controllers.removeAll(where: { $0 === self }) diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index ae8323a7084..cb446a9c475 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -737,9 +737,7 @@ private final class DrawingScreenComponent: CombinedComponent { animationCache: context.animationCache, animationRenderer: context.animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: false, - isEmojiSelection: true, + subject: .emoji, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, @@ -3132,7 +3130,7 @@ public final class DrawingToolsInteraction { } } let entityFrame = entityView.convert(entityView.selectionBounds, to: node.view).offsetBy(dx: 0.0, dy: -6.0) - let controller = ContextMenuController(actions: actions) + let controller = makeContextMenuController(actions: actions) let bounds = node.bounds.insetBy(dx: 0.0, dy: 160.0) self.present( controller, diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index 441d475912d..0aafe33323b 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -449,9 +449,7 @@ public final class DrawingStickerEntityView: DrawingEntityView { animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: true, - isEmojiSelection: false, + subject: .reaction, hasTrending: false, topReactionItems: mappedReactionItems, areUnicodeEmojiEnabled: false, diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 20cde13ec5c..0388e89aeb5 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -147,11 +147,12 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate return true } - private var emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] + private var emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = [] func updateEntities() { self.textView.drawingLayoutManager.ensureLayout(for: self.textView.textContainer) - var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] + var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = [] + let fontSize = self.displayFontSize * 0.78 var shouldRepeat = false if let attributedText = self.textView.attributedText { @@ -160,7 +161,11 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { if let start = self.textView.position(from: beginning, offset: range.location), let end = self.textView.position(from: start, offset: range.length), let textRange = self.textView.textRange(from: start, to: end) { let rect = self.textView.firstRect(for: textRange) - customEmojiRects.append((rect, value)) + var emojiFontSize = fontSize + if let font = attributes[.font] as? UIFont { + emojiFontSize = font.pointSize + } + customEmojiRects.append((rect, value, emojiFontSize)) if rect.origin.x.isInfinite { shouldRepeat = true } @@ -202,7 +207,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate self.customEmojiContainerView = customEmojiContainerView } - customEmojiContainerView.update(fontSize: self.displayFontSize * 0.78, textColor: textColor, emojiRects: customEmojiRects) + customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects) } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil @@ -739,13 +744,12 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate let scale = self.textEntity.scale let rotation = self.textEntity.rotation - let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0) - var entities: [DrawingEntity] = [] - for (emojiRect, emojiAttribute) in self.emojiRects { + for (emojiRect, emojiAttribute, fontSize) in self.emojiRects { guard let file = emojiAttribute.file else { continue } + let itemSize: CGFloat = floor(24.0 * fontSize * 0.78 / 17.0) let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0) let entity = DrawingStickerEntity(content: .file(file, .sticker)) diff --git a/submodules/Emoji/Package.swift b/submodules/Emoji/Package.swift index 98ba6f1eb0e..0c4617ccda7 100644 --- a/submodules/Emoji/Package.swift +++ b/submodules/Emoji/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "Emoji", - platforms: [.macOS(.v10_12)], + 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/EncryptionProvider/Package.swift b/submodules/EncryptionProvider/Package.swift index 2bbc806847b..7571dda7616 100644 --- a/submodules/EncryptionProvider/Package.swift +++ b/submodules/EncryptionProvider/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "EncryptionProvider", - platforms: [.macOS(.v10_12)], + 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/FFMpegBinding/BUILD b/submodules/FFMpegBinding/BUILD index 094b363bd2b..7edfc881d92 100644 --- a/submodules/FFMpegBinding/BUILD +++ b/submodules/FFMpegBinding/BUILD @@ -10,6 +10,9 @@ objc_library( hdrs = glob([ "Public/**/*.h", ]), + copts = [ + "-Werror", + ], includes = [ "Public", ], diff --git a/submodules/FFMpegBinding/Package.swift b/submodules/FFMpegBinding/Package.swift index a7ddae8d599..da926bdafc9 100644 --- a/submodules/FFMpegBinding/Package.swift +++ b/submodules/FFMpegBinding/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "FFMpegBinding", - platforms: [.macOS(.v10_12)], + 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/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h index 4aebfa02df0..8f4f8314b19 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h @@ -4,6 +4,8 @@ NS_ASSUME_NONNULL_BEGIN #define FFMPEG_AVSEEK_SIZE 0x10000 +extern int FFMPEG_CONSTANT_AVERROR_EOF; + @interface FFMpegAVIOContext : NSObject - (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m b/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m index 857462cfeca..0ea2b101a6e 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVCodec.m @@ -3,14 +3,14 @@ #import "libavcodec/avcodec.h" @interface FFMpegAVCodec () { - AVCodec *_impl; + AVCodec const *_impl; } @end @implementation FFMpegAVCodec -- (instancetype)initWithImpl:(AVCodec *)impl { +- (instancetype)initWithImpl:(AVCodec const *)impl { self = [super init]; if (self != nil) { _impl = impl; @@ -19,7 +19,7 @@ - (instancetype)initWithImpl:(AVCodec *)impl { } + (FFMpegAVCodec * _Nullable)findForId:(int)codecId { - AVCodec *codec = avcodec_find_decoder(codecId); + AVCodec const *codec = avcodec_find_decoder(codecId); if (codec) { return [[FFMpegAVCodec alloc] initWithImpl:codec]; } else { @@ -28,7 +28,7 @@ + (FFMpegAVCodec * _Nullable)findForId:(int)codecId { } - (void *)impl { - return _impl; + return (void *)_impl; } @end diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m index 52f9e6541e4..623c7d2f426 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVCodecContext.m @@ -35,7 +35,11 @@ - (void *)impl { } - (int32_t)channels { +#if LIBAVFORMAT_VERSION_MAJOR >= 59 + return (int32_t)_impl->ch_layout.nb_channels; +#else return (int32_t)_impl->channels; +#endif } - (int32_t)sampleRate { diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m index 992e6dabacc..6cb0a92bf01 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m @@ -4,6 +4,7 @@ #import #import +#import "libavcodec/avcodec.h" #import "libavformat/avformat.h" int FFMpegCodecIdH264 = AV_CODEC_ID_H264; @@ -115,9 +116,9 @@ - (FFMpegFpsAndTimebase)fpsAndTimebaseForStreamIndex:(int32_t)streamIndex defaul if (stream->time_base.den != 0 && stream->time_base.num != 0) { timebase = CMTimeMake((int64_t)stream->time_base.num, stream->time_base.den); - } else if (stream->codec->time_base.den != 0 && stream->codec->time_base.num != 0) { + }/* else if (stream->codec->time_base.den != 0 && stream->codec->time_base.num != 0) { timebase = CMTimeMake((int64_t)stream->codec->time_base.num, stream->codec->time_base.den); - } else { + }*/ else { timebase = defaultTimeBase; } diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m index 9c8218e0fe9..cab3d75bebf 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m @@ -20,7 +20,7 @@ - (instancetype)init { - (void)dealloc { if (_impl) { - av_frame_unref(_impl); + av_frame_free(&_impl); } } @@ -45,7 +45,11 @@ - (int64_t)pts { } - (int64_t)duration { +#if LIBAVFORMAT_VERSION_MAJOR >= 59 + return _impl->duration; +#else return _impl->pkt_duration; +#endif } - (FFMpegAVFrameColorRange)colorRange { diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m index 3473ed7ff56..b0e54a15ac3 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m @@ -2,6 +2,8 @@ #import "libavformat/avformat.h" +int FFMPEG_CONSTANT_AVERROR_EOF = AVERROR_EOF; + @interface FFMpegAVIOContext () { AVIOContext *_impl; } diff --git a/submodules/FFMpegBinding/Sources/FFMpegGlobals.m b/submodules/FFMpegBinding/Sources/FFMpegGlobals.m index 8729f8a3a91..f0ff9091bbb 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegGlobals.m +++ b/submodules/FFMpegBinding/Sources/FFMpegGlobals.m @@ -10,7 +10,6 @@ + (void)initializeGlobals { #else av_log_set_level(AV_LOG_QUIET); #endif - av_register_all(); } @end diff --git a/submodules/FFMpegBinding/Sources/FFMpegPacket.m b/submodules/FFMpegBinding/Sources/FFMpegPacket.m index bf647d23709..30e4d8f92f3 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegPacket.m +++ b/submodules/FFMpegBinding/Sources/FFMpegPacket.m @@ -2,10 +2,11 @@ #import +#import "libavcodec/avcodec.h" #import "libavformat/avformat.h" @interface FFMpegPacket () { - AVPacket _impl; + AVPacket *_impl; } @end @@ -15,49 +16,49 @@ @implementation FFMpegPacket - (instancetype)init { self = [super init]; if (self != nil) { - av_init_packet(&_impl); + _impl = av_packet_alloc(); } return self; } - (void)dealloc { - av_packet_unref(&_impl); + av_packet_free(&_impl); } - (void *)impl { - return &_impl; + return _impl; } - (int64_t)pts { - if (_impl.pts == 0x8000000000000000) { - return _impl.dts; + if (_impl->pts == 0x8000000000000000) { + return _impl->dts; } else { - return _impl.pts; + return _impl->pts; } } - (int64_t)dts { - return _impl.dts; + return _impl->dts; } - (int64_t)duration { - return _impl.duration; + return _impl->duration; } - (int32_t)streamIndex { - return (int32_t)_impl.stream_index; + return (int32_t)_impl->stream_index; } - (int32_t)size { - return (int32_t)_impl.size; + return (int32_t)_impl->size; } - (uint8_t *)data { - return _impl.data; + return _impl->data; } - (int32_t)sendToDecoder:(FFMpegAVCodecContext *)codecContext { - return avcodec_send_packet((AVCodecContext *)[codecContext impl], &_impl); + return avcodec_send_packet((AVCodecContext *)[codecContext impl], _impl); } @end diff --git a/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m b/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m index b25f557222d..df3c1adfb8e 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m +++ b/submodules/FFMpegBinding/Sources/FFMpegRemuxer.m @@ -34,18 +34,18 @@ - (void)dealloc { @end -static int readPacketImpl(void * _Nullable opaque, uint8_t * _Nullable buffer, int length) { +/*static int readPacketImpl(void * _Nullable opaque, uint8_t * _Nullable buffer, int length) { FFMpegRemuxerContext *context = (__bridge FFMpegRemuxerContext *)opaque; context->_offset += length; - printf("read %lld bytes (offset is now %lld)\n", length, context->_offset); - return read(context->_fd, buffer, length); + printf("read %lld bytes (offset is now %lld)\n", (int64_t)length, context->_offset); + return (int)read(context->_fd, buffer, length); } static int writePacketImpl(void * _Nullable opaque, uint8_t * _Nullable buffer, int length) { FFMpegRemuxerContext *context = (__bridge FFMpegRemuxerContext *)opaque; context->_offset += length; - printf("write %lld bytes (offset is now %lld)\n", length, context->_offset); - return write(context->_fd, buffer, length); + printf("write %lld bytes (offset is now %lld)\n", (int64_t)length, context->_offset); + return (int)write(context->_fd, buffer, length); } static int64_t seekImpl(void * _Nullable opaque, int64_t offset, int whence) { @@ -57,7 +57,7 @@ static int64_t seekImpl(void * _Nullable opaque, int64_t offset, int whence) { context->_offset = offset; return lseek(context->_fd, offset, SEEK_SET); } -} +}*/ @implementation FFMpegRemuxer @@ -99,7 +99,7 @@ + (bool)remux:(NSString * _Nonnull)path to:(NSString * _Nonnull)outPath { } number_of_streams = input_format_context->nb_streams; - streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list)); + streams_list = av_malloc_array(number_of_streams, sizeof(*streams_list)); if (!streams_list) { ret = AVERROR(ENOMEM); diff --git a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m index f8e68d51b0e..42f4e46525e 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m +++ b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m @@ -52,7 +52,9 @@ - (void)resetContextForChannelCount:(int)channelCount { swr_free(&_context); _context = NULL; } - + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" _context = swr_alloc_set_opts(NULL, av_get_default_channel_layout((int)_destinationChannelCount), (enum AVSampleFormat)_destinationSampleFormat, @@ -62,6 +64,7 @@ - (void)resetContextForChannelCount:(int)channelCount { (int)_sourceSampleRate, 0, NULL); +#pragma clang diagnostic pop _currentSourceChannelCount = channelCount; _ratio = MAX(1, _destinationSampleRate / MAX(_sourceSampleRate, 1)) * MAX(1, _destinationChannelCount / channelCount) * 2; if (_context) { @@ -72,7 +75,11 @@ - (void)resetContextForChannelCount:(int)channelCount { - (NSData * _Nullable)resample:(FFMpegAVFrame *)frame { AVFrame *frameImpl = (AVFrame *)[frame impl]; +#if LIBAVFORMAT_VERSION_MAJOR >= 59 + int numChannels = frameImpl->ch_layout.nb_channels; +#else int numChannels = frameImpl->channels; +#endif if (numChannels != _currentSourceChannelCount) { [self resetContextForChannelCount:numChannels]; } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index db2f6ca8f6e..fac4bcfb8fd 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -429,7 +429,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll ) self.textNode.visibility = true - let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: defaultDarkPresentationTheme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: defaultDarkPresentationTheme.list.itemAccentColor), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: defaultDarkPresentationTheme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: defaultDarkPresentationTheme.list.itemAccentColor, isDark: true), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in guard let self else { return } @@ -534,6 +534,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll //component.controller()?.present(translateController, in: .window(.root)) self.controllerInteraction?.presentController(translateController, nil) }) + case .quote: + break } }) @@ -554,6 +556,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll return false } else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { return false + } else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] { + return false } else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return false } else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { @@ -729,6 +733,21 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + + if let textSelectionNode = self.textSelectionNode, result === textSelectionNode.view { + let localPoint = self.view.convert(point, to: textSelectionNode.view) + if !textSelectionNode.canBeginSelection(localPoint) { + if let result = self.textNode.view.hitTest(self.view.convert(point, to: self.textNode.view), with: event) { + return result + } + } + } + + return result + } + private func actionForAttributes(_ attributes: [NSAttributedString.Key: Any], _ index: Int) -> GalleryControllerInteractionTapAction? { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true @@ -1062,9 +1081,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.fullscreenButton.isHidden = true } - var textFrame = CGRect() - var visibleTextHeight: CGFloat = 0.0 if !self.textNode.isHidden { + var textFrame = CGRect() + var visibleTextHeight: CGFloat = 0.0 + let sideInset: CGFloat = 8.0 + leftInset let topInset: CGFloat = 8.0 let textBottomInset: CGFloat = 8.0 @@ -1092,7 +1112,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.scrollNode.frame = scrollNodeFrame } - textOffset = min(400.0, self.scrollNode.view.contentOffset.y) + var maxTextOffset: CGFloat = size.height - bottomInset - 238.0 - UIScreenPixel + if let _ = self.scrubberView { + maxTextOffset -= 44.0 + } + textOffset = min(maxTextOffset, self.scrollNode.view.contentOffset.y) panelHeight = max(0.0, panelHeight + visibleTextPanelHeight + textOffset) if self.scrollNode.view.isScrollEnabled { diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 5af5ea129e1..0a372dca8cf 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -139,7 +139,7 @@ private let boldItalicFont = Font.semiboldItalic(16.0) private let fixedFont = UIFont(name: "Menlo-Regular", size: 15.0) ?? textFont public func galleryCaptionStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], message: Message?) -> NSAttributedString { - return stringWithAppliedEntities(text, entities: entities, baseColor: .white, linkColor: UIColor(rgb: 0x5ac8fa), baseFont: textFont, linkFont: textFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + return stringWithAppliedEntities(text, entities: entities, baseColor: .white, linkColor: UIColor(rgb: 0x5ac8fa), baseFont: textFont, linkFont: textFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: textFont, underlineLinks: false, message: message, adjustQuoteFontSize: true) } private func galleryMessageCaptionText(_ message: Message) -> String { diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 771ebbcecf7..5f837beadf4 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -516,7 +516,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { if let navigationController = strongSelf.baseNavigationController() { strongSelf.beginCustomDismiss(true) - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: true, timecode: nil))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) Queue.mainQueue().after(0.3) { strongSelf.completeCustomDismiss() diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index a8b05c2d627..fd8eb1795f7 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2505,7 +2505,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } - c.setItems(strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) items.append(.separator) @@ -2522,7 +2522,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let navigationController = strongSelf.baseNavigationController() { strongSelf.beginCustomDismiss(true) - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: true, timecode: nil))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) Queue.mainQueue().after(0.3) { strongSelf.completeCustomDismiss() @@ -2634,7 +2634,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { c.dismiss(completion: nil) return } - c.setItems(strongSelf.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) let sliderValuePromise = ValuePromise(nil) diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift index 6aac0ba01fd..21e1ff78689 100644 --- a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -527,7 +527,7 @@ public final class RecognizedTextSelectionNode: ASDisplayNode { let _ = self?.dismissSelection() })) - self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + self.present(makeContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in guard let strongSelf = self, let rootNode = strongSelf.rootNode else { return nil } diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 9948e1141f8..775de8c0313 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -420,7 +420,7 @@ public final class SecretMediaPreviewController: ViewController { |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self, strongSelf.traceVisibility() { if strongSelf.messageId.peerId.namespace == Namespaces.Peer.CloudUser { - let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.messageId.peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.messageId.peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() } else if strongSelf.messageId.peerId.namespace == Namespaces.Peer.SecretChat { let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(peerId: strongSelf.messageId.peerId).start() } diff --git a/submodules/GraphCore/Package.swift b/submodules/GraphCore/Package.swift index f0ee70d8de4..a82d5a1123e 100644 --- a/submodules/GraphCore/Package.swift +++ b/submodules/GraphCore/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "GraphCore", - platforms: [.macOS(.v10_12)], + 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/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 2e3bf3d894e..ca46f6d3567 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -67,7 +67,7 @@ public final class HashtagSearchController: TelegramBaseController { if let strongSelf = self { strongSelf.openMessageFromSearchDisposable.set((strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer) |> deliverOnMainQueue).start(next: { actualPeer in if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: message.id.peerId == actualPeer.id ? .message(id: .id(message.id), highlight: true, timecode: nil) : nil, keepStack: .always)) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: message.id.peerId == actualPeer.id ? .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) : nil, keepStack: .always)) } })) strongSelf.controllerNode.listNode.clearHighlightAnimated(true) diff --git a/submodules/ImageCompression/Sources/ImageCompression.swift b/submodules/ImageCompression/Sources/ImageCompression.swift index ee84db8bb3b..36aacab26f3 100644 --- a/submodules/ImageCompression/Sources/ImageCompression.swift +++ b/submodules/ImageCompression/Sources/ImageCompression.swift @@ -18,8 +18,8 @@ public func extractImageExtraScans(_ data: Data) -> [Int] { } } -public func compressImageToJPEG(_ image: UIImage, quality: Float) -> Data? { - if let result = compressJPEGData(image) { +public func compressImageToJPEG(_ image: UIImage, quality: Float, tempFilePath: String) -> Data? { + if let result = compressJPEGData(image, tempFilePath) { return result } diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 81cd39413ea..62b03688e1c 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -3,6 +3,7 @@ import CoreLocation import SwiftSignalKit import StoreKit import TelegramCore +import Postbox import TelegramStringFormatting import TelegramUIPreferences import PersistentStringHash @@ -13,12 +14,20 @@ private let productIdentifiers = [ "org.telegram.telegramPremium.monthly", "org.telegram.telegramPremium.twelveMonths", "org.telegram.telegramPremium.sixMonths", - "org.telegram.telegramPremium.threeMonths" -] + "org.telegram.telegramPremium.threeMonths", -private func isSubscriptionProductId(_ id: String) -> Bool { - return id.hasSuffix(".monthly") || id.hasSuffix(".annual") || id.hasSuffix(".semiannual") -} + "org.telegram.telegramPremium.threeMonths.code_x1", + "org.telegram.telegramPremium.sixMonths.code_x1", + "org.telegram.telegramPremium.twelveMonths.code_x1", + + "org.telegram.telegramPremium.threeMonths.code_x5", + "org.telegram.telegramPremium.sixMonths.code_x5", + "org.telegram.telegramPremium.twelveMonths.code_x5", + + "org.telegram.telegramPremium.threeMonths.code_x10", + "org.telegram.telegramPremium.sixMonths.code_x10", + "org.telegram.telegramPremium.twelveMonths.code_x10" +] private extension NSDecimalNumber { func round(_ decimals: Int) -> NSDecimalNumber { @@ -103,6 +112,25 @@ public final class InAppPurchaseManager: NSObject { return self.numberFormatter.string(from: prettierPrice) ?? "" } + public func multipliedPrice(count: Int) -> String { + let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2) + let prettierPrice = price + .multiplying(by: NSDecimalNumber(value: 2)) + .rounding(accordingToBehavior: + NSDecimalNumberHandler( + roundingMode: .up, + scale: Int16(0), + raiseOnExactness: false, + raiseOnOverflow: false, + raiseOnUnderflow: false, + raiseOnDivideByZero: false + ) + ) + .dividing(by: NSDecimalNumber(value: 2)) + .subtracting(NSDecimalNumber(value: 0.01)) + return self.numberFormatter.string(from: prettierPrice) ?? "" + } + public var priceValue: NSDecimalNumber { return self.skProduct.price } @@ -151,13 +179,11 @@ public final class InAppPurchaseManager: NSObject { private final class PaymentTransactionContext { var state: SKPaymentTransactionState? - var isUpgrade: Bool - var targetPeerId: EnginePeer.Id? + let purpose: PendingInAppPurchaseState.Purpose let subscriber: (TransactionState) -> Void - init(isUpgrade: Bool, targetPeerId: EnginePeer.Id?, subscriber: @escaping (TransactionState) -> Void) { - self.isUpgrade = isUpgrade - self.targetPeerId = targetPeerId + init(purpose: PendingInAppPurchaseState.Purpose, subscriber: @escaping (TransactionState) -> Void) { + self.purpose = purpose self.subscriber = subscriber } } @@ -235,21 +261,20 @@ public final class InAppPurchaseManager: NSObject { } } - public func buyProduct(_ product: Product, isUpgrade: Bool = false, targetPeerId: EnginePeer.Id? = nil) -> Signal { + public func buyProduct(_ product: Product, quantity: Int32 = 1, purpose: AppStoreTransactionPurpose) -> Signal { if !self.canMakePayments { return .fail(.cantMakePayments) } - - if !product.isSubscription && targetPeerId == nil { - return .fail(.cantMakePayments) - } - + let accountPeerId = "\(self.engine.account.peerId.toInt64())" Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)") + let purpose = PendingInAppPurchaseState.Purpose(appStorePurpose: purpose) + let payment = SKMutablePayment(product: product.skProduct) payment.applicationUsername = accountPeerId + payment.quantity = Int(quantity) SKPaymentQueue.default().add(payment) let productIdentifier = payment.productIdentifier @@ -257,7 +282,7 @@ public final class InAppPurchaseManager: NSObject { let disposable = MetaDisposable() self.stateQueue.async { - let paymentContext = PaymentTransactionContext(isUpgrade: isUpgrade, targetPeerId: targetPeerId, subscriber: { state in + let paymentContext = PaymentTransactionContext(purpose: purpose, subscriber: { state in switch state { case let .purchased(transactionId), let .restored(transactionId): if let transactionId = transactionId { @@ -381,8 +406,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { productId: transaction.payment.productIdentifier, content: PendingInAppPurchaseState( productId: transaction.payment.productIdentifier, - isUpgrade: paymentContext.isUpgrade, - targetPeerId: paymentContext.targetPeerId + purpose: paymentContext.purpose ) ).start() } @@ -410,64 +434,61 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { var completion: Signal = .never() - let purpose: Signal - if !isSubscriptionProductId(productIdentifier) { - let peerId: Signal - if let targetPeerId = paymentContexts[productIdentifier]?.targetPeerId { - peerId = .single(targetPeerId) + let products = self.availableProducts + |> filter { products in + return !products.isEmpty + } + |> take(1) + + + let product: Signal = products + |> map { products in + if let product = products.first(where: { $0.id == productIdentifier }) { + return product } else { - peerId = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier) - |> mapToSignal { state -> Signal in - if let state = state, let peerId = state.targetPeerId { - return .single(peerId) - } else { - return .complete() - } - } - } - completion = updatePendingInAppPurchaseState(engine: self.engine, productId: productIdentifier, content: nil) - - let products = self.availableProducts - |> filter { products in - return !products.isEmpty + return nil } - |> take(1) - - purpose = combineLatest(products, peerId) - |> map { products, peerId -> AppStoreTransactionPurpose in - if let product = products.first(where: { $0.id == productIdentifier }) { - let (currency, amount) = product.priceCurrencyAndAmount - return .gift(peerId: peerId, currency: currency, amount: amount) - } else { - return .gift(peerId: peerId, currency: "", amount: 0) - } + } + + let purpose: Signal + if let paymentContext = paymentContexts[productIdentifier] { + purpose = product + |> map { product in + return paymentContext.purpose.appStorePurpose(product: product) } } else { - let isUpgrade: Signal - if let isUpgradeValue = paymentContexts[productIdentifier]?.isUpgrade { - isUpgrade = .single(isUpgradeValue) - } else { - isUpgrade = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier) - |> mapToSignal { state -> Signal in - if let state = state { - return .single(state.isUpgrade) - } else { - return .single(false) - } + purpose = combineLatest( + product, + pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier) + ) + |> mapToSignal { product, state -> Signal in + if let state { + return .single(state.purpose.appStorePurpose(product: product)) + } else { + return .complete() } } - purpose = isUpgrade - |> map { isUpgrade in - return isUpgrade ? .upgrade : .subscription - } } - + completion = updatePendingInAppPurchaseState(engine: self.engine, productId: productIdentifier, content: nil) + let receiptData = getReceiptData() ?? Data() + +#if DEBUG + let id = Int64.random(in: Int64.min ... Int64.max) + let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false) + self.engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) + + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")]) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + + let _ = enqueueMessages(account: self.engine.account, peerId: self.engine.account.peerId, messages: [message]).start() +#endif + self.disposableSet.set( (purpose |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in - self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) + return self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) }).start(error: { [weak self] _ in Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign") for transaction in transactions { @@ -535,32 +556,163 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { } private final class PendingInAppPurchaseState: Codable { + enum CodingKeys: String, CodingKey { + case productId + case purpose + case storeProductId + } + + enum Purpose: Codable { + enum DecodingError: Error { + case generic + } + + enum CodingKeys: String, CodingKey { + case type + case peer + case peers + case boostPeer + case additionalPeerIds + case countries + case onlyNewSubscribers + case randomId + case untilDate + } + + enum PurposeType: Int32 { + case subscription + case upgrade + case restore + case gift + case giftCode + case giveaway + } + + case subscription + case upgrade + 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) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = PurposeType(rawValue: try container.decode(Int32.self, forKey: .type)) + switch type { + case .subscription: + self = .subscription + case .upgrade: + self = .upgrade + case .restore: + self = .restore + case .gift: + self = .gift( + peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer)) + ) + case .giftCode: + self = .giftCode( + peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) }, + boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }) + ) + case .giveaway: + self = .giveaway( + boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)), + 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), + randomId: try container.decode(Int64.self, forKey: .randomId), + untilDate: try container.decode(Int32.self, forKey: .untilDate) + ) + default: + throw DecodingError.generic + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .subscription: + try container.encode(PurposeType.subscription.rawValue, forKey: .type) + case .upgrade: + try container.encode(PurposeType.upgrade.rawValue, forKey: .type) + case .restore: + try container.encode(PurposeType.restore.rawValue, forKey: .type) + case let .gift(peerId): + try container.encode(PurposeType.gift.rawValue, forKey: .type) + try container.encode(peerId.toInt64(), forKey: .peer) + case let .giftCode(peerIds, boostPeer): + 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): + 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(randomId, forKey: .randomId) + try container.encode(untilDate, forKey: .untilDate) + } + } + + init(appStorePurpose: AppStoreTransactionPurpose) { + switch appStorePurpose { + case .subscription: + self = .subscription + case .upgrade: + self = .upgrade + case .restore: + self = .restore + case let .gift(peerId, _, _): + 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) + } + } + + func appStorePurpose(product: InAppPurchaseManager.Product?) -> AppStoreTransactionPurpose { + let (currency, amount) = product?.priceCurrencyAndAmount ?? ("", 0) + switch self { + case .subscription: + return .subscription + case .upgrade: + return .upgrade + case .restore: + return .restore + case let .gift(peerId): + 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) + } + } + } + public let productId: String - public let isUpgrade: Bool - public let targetPeerId: EnginePeer.Id? + public let purpose: Purpose - public init(productId: String, isUpgrade: Bool, targetPeerId: EnginePeer.Id?) { + public init(productId: String, purpose: Purpose) { self.productId = productId - self.isUpgrade = isUpgrade - self.targetPeerId = targetPeerId + self.purpose = purpose } public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: StringCodingKey.self) + let container = try decoder.container(keyedBy: CodingKeys.self) - self.productId = try container.decode(String.self, forKey: "productId") - self.isUpgrade = try container.decodeIfPresent(Bool.self, forKey: "isUpgrade") ?? false - self.targetPeerId = (try container.decodeIfPresent(Int64.self, forKey: "targetPeerId")).flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) } + self.productId = try container.decode(String.self, forKey: .productId) + self.purpose = try container.decode(Purpose.self, forKey: .purpose) } public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: StringCodingKey.self) + var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.productId, forKey: "productId") - try container.encode(self.isUpgrade, forKey: "isUpgrade") - if let targetPeerId = self.targetPeerId { - try container.encode(targetPeerId.id._internalGetInt64Value(), forKey: "targetPeerId") - } + try container.encode(self.productId, forKey: .productId) + try container.encode(self.purpose, forKey: .purpose) } } diff --git a/submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift b/submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift index efea3cbf912..aa5308d68c0 100644 --- a/submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift +++ b/submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift @@ -53,12 +53,18 @@ private func cachedInternalInstantPage(context: AccountContext, url: String) -> return cachedInstantPage(engine: context.engine, url: cachedUrl) |> mapToSignal { cachedInstantPage -> Signal in let updated = resolveInstantViewUrl(account: context.account, url: url) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> afterNext { result in if case let .instantView(webPage, _) = result, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage { if instantPage.isComplete { let _ = updateCachedInstantPage(engine: context.engine, url: cachedUrl, webPage: webPage).start() } else { - let _ = (actualizedWebpage(postbox: context.account.postbox, network: context.account.network, webpage: webPage) + let _ = (actualizedWebpage(account: context.account, webpage: webPage) |> mapToSignal { webPage -> Signal in if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete { return updateCachedInstantPage(engine: context.engine, url: cachedUrl, webPage: webPage) diff --git a/submodules/InstantPageUI/Sources/InstantPageController.swift b/submodules/InstantPageUI/Sources/InstantPageController.swift index 25cb5dcf64e..a17188bf80a 100644 --- a/submodules/InstantPageUI/Sources/InstantPageController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageController.swift @@ -106,7 +106,7 @@ public final class InstantPageController: ViewController { self.statusBar.statusBarStyle = .White - self.webpageDisposable = (actualizedWebpage(postbox: self.context.account.postbox, network: self.context.account.network, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in + self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.webPage = result if strongSelf.isNodeLoaded { diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index cadcd076e15..03788f0d6b2 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -1002,7 +1002,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } private func longPressMedia(_ media: InstantPageMedia) { - let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in + let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in if let strongSelf = self, case let .image(image) = media.media { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() @@ -1147,7 +1147,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { })) } - let controller = ContextMenuController(actions: actions) + let controller = makeContextMenuController(actions: actions) controller.dismissed = { [weak self] in self?.updateTextSelectionRects([], text: nil) } @@ -1412,7 +1412,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { }, openUrl: { _ in }, openPeer: { _ in }, showAll: false) - let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let controller = LocationViewController(context: self.context, subject: EngineMessage(message), params: controllerParams) diff --git a/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift b/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift index 9a756f19978..5376f7e65ce 100644 --- a/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift @@ -71,7 +71,14 @@ final class InstantPageFeedbackNode: ASDisplayNode, InstantPageNode { } @objc func buttonPressed() { - self.resolveDisposable.set((self.context.engine.peers.resolvePeerByName(name: "previews") |> deliverOnMainQueue).start(next: { [weak self] peer in + self.resolveDisposable.set((self.context.engine.peers.resolvePeerByName(name: "previews") + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let _ = peer, let webPageId = strongSelf.webPage.id?.id { strongSelf.openUrl(InstantPageUrlItem(url: "https://t.me/previews?start=webpage\(webPageId)", webpageId: nil)) } diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index d6323cd19cf..a47eb382ab8 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -632,7 +632,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size) let item: InstantPageItem if let url = url, let coverId = coverId, case let .image(image) = media[coverId] { - let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil) + let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, isMediaLargeByDefault: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil) let content = TelegramMediaWebpageContent.Loaded(loadedContent) item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false) diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift index af034e9f3fe..efb02b214e9 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift @@ -150,7 +150,10 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { |> mapToSignal({ peer -> Signal in if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil { return .single(.channel(peer)) |> then(engine.peers.resolvePeerByName(name: username) - |> mapToSignal({ updatedPeer -> Signal in + |> mapToSignal({ result -> Signal in + guard case let .result(updatedPeer) = result else { + return .complete() + } if let updatedPeer = updatedPeer { return .single(updatedPeer) } else { diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift index a393f70ef05..4f284bfcd52 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift @@ -410,7 +410,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie coveringRect = coveringRect.union(rects[i]) } - let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, case let .Loaded(content) = strongSelf.webPage.content { diff --git a/submodules/InstantPageUI/Sources/InstantPageTextSelectionNode.swift b/submodules/InstantPageUI/Sources/InstantPageTextSelectionNode.swift index 8595882e5b4..1c29d901c05 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextSelectionNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextSelectionNode.swift @@ -502,7 +502,7 @@ final class InstantPageTextSelectionNode: ASDisplayNode { self?.performAction(text, .share) self?.dismissSelection() })) - self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + self.present(makeContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in guard let strongSelf = self, let rootNode = strongSelf.rootNode else { return nil } diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index bc125a62665..1e3b637e4d7 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -58,6 +58,7 @@ swift_library( "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", "//submodules/QrCodeUI:QrCodeUI", "//submodules/PromptUI", + "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 3b0e437cf68..f2e72ec1f96 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -492,7 +492,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese didDisplayAddPeerNotice = true dismissTooltipsImpl?() - displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastNewChatAdded, timeout: 8), true) + displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastNewChatAdded, timeout: 8, customUndoText: nil), true) } } else { let text: String @@ -592,11 +592,11 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese dismissTooltipsImpl?() let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.FolderLinkScreen_SaveUnknownError, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.FolderLinkScreen_SaveUnknownError, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }, completed: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false)) - displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastLinkUpdated, timeout: 3), false) + displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastLinkUpdated, timeout: 3, customUndoText: nil), false) dismissImpl?() })) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 5179d173f96..0ae1175a9f8 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -16,6 +16,7 @@ import AppBundle import ContextUI import TelegramStringFormatting import UndoUI +import ItemListDatePickerItem private final class InviteLinkEditControllerArguments { let context: AccountContext diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index e6c2b8af97c..0146ab8af08 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -726,7 +726,7 @@ public final class InviteLinkViewController: ViewController { if requestsState.importers.isEmpty && requestsState.isLoadingMore { count = min(4, state.count) loading = true - let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil) for i in 0 ..< count { entries.append(.request(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true)) } @@ -760,7 +760,7 @@ public final class InviteLinkViewController: ViewController { if state.importers.isEmpty && state.isLoadingMore { count = min(4, state.count) loading = true - let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil) for i in 0 ..< count { entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true)) } diff --git a/submodules/ItemListPeerItem/BUILD b/submodules/ItemListPeerItem/BUILD index 784835efe7e..a4a85f437a3 100644 --- a/submodules/ItemListPeerItem/BUILD +++ b/submodules/ItemListPeerItem/BUILD @@ -27,6 +27,8 @@ swift_library( "//submodules/ComponentFlow:ComponentFlow", "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", "//submodules/CheckNode", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 440b8785a7e..4d8cebf90db 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -17,6 +17,8 @@ import AccountContext import ComponentFlow import EmojiStatusComponent import CheckNode +import AnimationCache +import MultiAnimationRenderer private final class ShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? @@ -319,10 +321,126 @@ public struct ItemListPeerItemShimmering { } public final class ItemListPeerItem: ListViewItem, ItemListItem { + public enum Context { + public final class Custom { + public let accountPeerId: EnginePeer.Id + public let postbox: Postbox + public let network: Network + public let animationCache: AnimationCache + public let animationRenderer: MultiAnimationRenderer + public let isPremiumDisabled: Bool + public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> + + public init( + accountPeerId: EnginePeer.Id, + postbox: Postbox, + network: Network, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + isPremiumDisabled: Bool, + resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> + ) { + self.accountPeerId = accountPeerId + self.postbox = postbox + self.network = network + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.isPremiumDisabled = isPremiumDisabled + self.resolveInlineStickers = resolveInlineStickers + } + } + + case account(AccountContext) + case custom(Custom) + + public var accountPeerId: EnginePeer.Id { + switch self { + case let .account(context): + return context.account.peerId + case let .custom(custom): + return custom.accountPeerId + } + } + + public var postbox: Postbox { + switch self { + case let .account(context): + return context.account.postbox + case let .custom(custom): + return custom.postbox + } + } + + public var network: Network { + switch self { + case let .account(context): + return context.account.network + case let .custom(custom): + return custom.network + } + } + + public var animationCache: AnimationCache { + switch self { + case let .account(context): + return context.animationCache + case let .custom(custom): + return custom.animationCache + } + } + + public var animationRenderer: MultiAnimationRenderer { + switch self { + case let .account(context): + return context.animationRenderer + case let .custom(custom): + return custom.animationRenderer + } + } + + public var isPremiumDisabled: Bool { + switch self { + case let .account(context): + return PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 })).isPremiumDisabled + case let .custom(custom): + return custom.isPremiumDisabled + } + } + + public var resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { + switch self { + case let .account(context): + return { fileIds in + return context.engine.stickers.resolveInlineStickers(fileIds: fileIds) + } + case let .custom(custom): + return custom.resolveInlineStickers + } + } + + public var energyUsageSettings: EnergyUsageSettings { + switch self { + case let .account(context): + return context.sharedContext.energyUsageSettings + case .custom: + return .default + } + } + + public var contentSettings: ContentSettings { + switch self { + case let .account(context): + return context.currentContentSettings.with { $0 } + case .custom: + return .default + } + } + } + let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder - let context: AccountContext + let context: Context let peer: EnginePeer let threadInfo: EngineMessageHistoryThread.Info? let height: ItemListPeerItemHeight @@ -358,7 +476,126 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let storyStats: PeerStoryStats? let openStories: ((UIView) -> Void)? - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, storyStats: PeerStoryStats? = nil, openStories: ((UIView) -> Void)? = nil) { + public init( + presentationData: ItemListPresentationData, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + context: AccountContext, + peer: EnginePeer, + threadInfo: EngineMessageHistoryThread.Info? = nil, + height: ItemListPeerItemHeight = .peerList, + aliasHandling: ItemListPeerItemAliasHandling = .standard, + nameColor: ItemListPeerItemNameColor = .primary, + nameStyle: ItemListPeerItemNameStyle = .distinctBold, + presence: EnginePeer.Presence?, + text: ItemListPeerItemText, + label: ItemListPeerItemLabel, + editing: ItemListPeerItemEditing, + revealOptions: ItemListPeerItemRevealOptions? = nil, + switchValue: ItemListPeerItemSwitch?, + enabled: Bool, + highlighted: Bool = false, + selectable: Bool, + highlightable: Bool = true, + animateFirstAvatarTransition: Bool = true, + sectionId: ItemListSectionId, + action: (() -> Void)?, + setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, + removePeer: @escaping (EnginePeer.Id) -> Void, + toggleUpdated: ((Bool) -> Void)? = nil, + contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, + hasTopStripe: Bool = true, + hasTopGroupInset: Bool = true, + noInsets: Bool = false, + noCorners: Bool = false, + tag: ItemListItemTag? = nil, + header: ListViewItemHeader? = nil, + shimmering: ItemListPeerItemShimmering? = nil, + displayDecorations: Bool = true, + disableInteractiveTransitionIfNecessary: Bool = false, + storyStats: PeerStoryStats? = nil, + openStories: ((UIView) -> Void)? = nil + ) { + self.presentationData = presentationData + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.context = .account(context) + self.peer = peer + self.threadInfo = threadInfo + self.height = height + self.aliasHandling = aliasHandling + self.nameColor = nameColor + self.nameStyle = nameStyle + self.presence = presence + self.text = text + self.label = label + self.editing = editing + self.revealOptions = revealOptions + self.switchValue = switchValue + self.enabled = enabled + self.highlighted = highlighted + self.selectable = selectable + self.highlightable = highlightable + self.animateFirstAvatarTransition = animateFirstAvatarTransition + self.sectionId = sectionId + self.action = action + self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + self.removePeer = removePeer + self.toggleUpdated = toggleUpdated + self.contextAction = contextAction + self.hasTopStripe = hasTopStripe + self.hasTopGroupInset = hasTopGroupInset + self.noInsets = noInsets + self.noCorners = noCorners + self.tag = tag + self.header = header + self.shimmering = shimmering + self.displayDecorations = displayDecorations + self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary + self.storyStats = storyStats + self.openStories = openStories + } + + public init( + presentationData: ItemListPresentationData, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + context: Context, + peer: EnginePeer, + threadInfo: EngineMessageHistoryThread.Info? = nil, + height: ItemListPeerItemHeight = .peerList, + aliasHandling: ItemListPeerItemAliasHandling = .standard, + nameColor: ItemListPeerItemNameColor = .primary, + nameStyle: ItemListPeerItemNameStyle = .distinctBold, + presence: EnginePeer.Presence?, + text: ItemListPeerItemText, + label: ItemListPeerItemLabel, + editing: ItemListPeerItemEditing, + revealOptions: ItemListPeerItemRevealOptions? = nil, + switchValue: ItemListPeerItemSwitch?, + enabled: Bool, + highlighted: Bool = false, + selectable: Bool, + highlightable: Bool = true, + animateFirstAvatarTransition: Bool = true, + sectionId: ItemListSectionId, + action: (() -> Void)?, + setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, + removePeer: @escaping (EnginePeer.Id) -> Void, + toggleUpdated: ((Bool) -> Void)? = nil, + contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, + hasTopStripe: Bool = true, + hasTopGroupInset: Bool = true, + noInsets: Bool = false, + noCorners: Bool = false, + tag: ItemListItemTag? = nil, + header: ListViewItemHeader? = nil, + shimmering: ItemListPeerItemShimmering? = nil, + displayDecorations: Bool = true, + disableInteractiveTransitionIfNecessary: Bool = false, + storyStats: PeerStoryStats? = nil, + openStories: ((UIView) -> Void)? = nil + ) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -666,9 +903,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var updatedLabelBadgeImage: UIImage? var credibilityIcon: EmojiStatusComponent.Content? - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) - - if case .threatSelfAsSaved = item.aliasHandling, item.peer.id == item.context.account.peerId { + if case .threatSelfAsSaved = item.aliasHandling, item.peer.id == item.context.accountPeerId { } else { if item.peer.isScam { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) @@ -678,7 +913,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo 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) - } else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled { + } else if item.peer.isPremium && !item.context.isPremiumDisabled { credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) } } @@ -799,7 +1034,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo if let threadInfo = item.threadInfo { titleAttributedString = NSAttributedString(string: threadInfo.title, font: currentBoldFont, textColor: titleColor) - } else if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling { + } else if item.peer.id == item.context.accountPeerId, case .threatSelfAsSaved = item.aliasHandling { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: currentBoldFont, textColor: titleColor) } else if item.peer.id.isReplies { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: currentBoldFont, textColor: titleColor) @@ -945,8 +1180,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let constrainedTitleSize = CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedTitleSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let constrainedStatusSize = CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedStatusSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var insets = itemListNeighborsGroupedInsets(neighbors, params) if !item.hasTopGroupInset { @@ -1157,7 +1395,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } let credibilityIconComponent = EmojiStatusComponent( - context: item.context, + postbox: item.context.postbox, + energyUsageSettings: item.context.energyUsageSettings, + resolveInlineStickers: item.context.resolveInlineStickers, animationCache: animationCache, animationRenderer: animationRenderer, content: credibilityIcon, @@ -1315,7 +1555,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } let avatarIconComponent = EmojiStatusComponent( - context: item.context, + postbox: item.context.postbox, + energyUsageSettings: item.context.energyUsageSettings, + resolveInlineStickers: item.context.resolveInlineStickers, animationCache: item.context.animationCache, animationRenderer: item.context.animationRenderer, content: avatarIconContent, @@ -1338,10 +1580,30 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo transition.updateFrame(view: avatarIconComponentView, frame: threadIconFrame) } } else { - if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling { - strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad) + if item.peer.id == item.context.accountPeerId, case .threatSelfAsSaved = item.aliasHandling { + strongSelf.avatarNode.setPeer( + accountPeerId: item.context.accountPeerId, + postbox: item.context.postbox, + network: item.context.network, + contentSettings: item.context.contentSettings, + theme: item.presentationData.theme, + peer: item.peer, + overrideImage: .savedMessagesIcon, + emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, + synchronousLoad: synchronousLoad + ) } else if item.peer.id.isReplies { - strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .repliesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad) + strongSelf.avatarNode.setPeer( + accountPeerId: item.context.accountPeerId, + postbox: item.context.postbox, + network: item.context.network, + contentSettings: item.context.contentSettings, + theme: item.presentationData.theme, + peer: item.peer, + overrideImage: .repliesIcon, + emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, + synchronousLoad: synchronousLoad + ) } else { var overrideImage: AvatarNodeImageOverride? if item.peer.isDeleted { @@ -1353,7 +1615,20 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo if case let .channel(channel) = item.peer, channel.isForum { clipStyle = .roundedRect } - strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad) + + strongSelf.avatarNode.setPeer( + accountPeerId: item.context.accountPeerId, + postbox: item.context.postbox, + network: item.context.network, + contentSettings: item.context.contentSettings, + theme: item.presentationData.theme, + peer: item.peer, + overrideImage: overrideImage, + emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, + clipStyle: clipStyle, + synchronousLoad: synchronousLoad + ) + strongSelf.avatarNode.setStoryStats(storyStats: item.storyStats.flatMap { storyStats in return AvatarNode.StoryStats( totalCount: storyStats.totalCount, diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index 23ab855131d..ea6a8cb6336 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -264,6 +264,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable self.isOpaqueWhenInOverlay = true self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style @@ -580,8 +581,8 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } - public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { - (self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated, curve: curve) + public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { + (self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve) } public func afterLayout(_ f: @escaping () -> Void) { diff --git a/submodules/ItemListUI/Sources/ItemListControllerFooterItem.swift b/submodules/ItemListUI/Sources/ItemListControllerFooterItem.swift index 9b0d4a0fa0c..282cfa12081 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerFooterItem.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerFooterItem.swift @@ -17,3 +17,19 @@ open class ItemListControllerFooterItemNode: ASDisplayNode { } } + + +public protocol ItemListControllerHeaderItem { + func isEqual(to: ItemListControllerHeaderItem) -> Bool + func node(current: ItemListControllerHeaderItemNode?) -> ItemListControllerHeaderItemNode +} + +open class ItemListControllerHeaderItemNode: ASDisplayNode { + open func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + return 0.0 + } + + open func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { + + } +} diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index b92dc66bf10..093ed9b76cb 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -127,6 +127,7 @@ private struct ItemListNodeTransition { let emptyStateItem: ItemListControllerEmptyStateItem? let searchItem: ItemListControllerSearch? let toolbarItem: ItemListToolbarItem? + let headerItem: ItemListControllerHeaderItem? let footerItem: ItemListControllerFooterItem? let focusItemTag: ItemListItemTag? let ensureVisibleItemTag: ItemListItemTag? @@ -146,6 +147,7 @@ public final class ItemListNodeState { let emptyStateItem: ItemListControllerEmptyStateItem? let searchItem: ItemListControllerSearch? let toolbarItem: ItemListToolbarItem? + let headerItem: ItemListControllerHeaderItem? let footerItem: ItemListControllerFooterItem? let animateChanges: Bool let crossfadeState: Bool @@ -154,13 +156,14 @@ public final class ItemListNodeState { let ensureVisibleItemTag: ItemListItemTag? let initialScrollToItem: ListViewScrollToItem? - public init(presentationData: ItemListPresentationData, entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, toolbarItem: ItemListToolbarItem? = nil, footerItem: ItemListControllerFooterItem? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) { + public init(presentationData: ItemListPresentationData, entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, toolbarItem: ItemListToolbarItem? = nil, headerItem: ItemListControllerHeaderItem? = nil, footerItem: ItemListControllerFooterItem? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) { self.presentationData = presentationData self.entries = entries.map { $0 } self.style = style self.emptyStateItem = emptyStateItem self.searchItem = searchItem self.toolbarItem = toolbarItem + self.headerItem = headerItem self.footerItem = footerItem self.crossfadeState = crossfadeState self.animateChanges = animateChanges @@ -230,6 +233,8 @@ public final class ItemListControllerNodeView: UITracingLayerView { } open class ItemListControllerNode: ASDisplayNode { + private weak var controller: ItemListController? + private var _ready = ValuePromise() open var ready: Signal { return self._ready.get() @@ -250,6 +255,9 @@ open class ItemListControllerNode: ASDisplayNode { private var searchNode: ItemListControllerSearchNode? private var toolbarItem: ItemListToolbarItem? + + private var headerItem: ItemListControllerHeaderItem? + private var headerItemNode: ItemListControllerHeaderItemNode? private var footerItem: ItemListControllerFooterItem? private var footerItemNode: ItemListControllerFooterItemNode? @@ -289,6 +297,7 @@ open class ItemListControllerNode: ASDisplayNode { private var previousContentOffset: ListViewVisibleContentOffset? public init(controller: ItemListController?, navigationBar: NavigationBar, state: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError>) { + self.controller = controller self.navigationBar = navigationBar self.listNode = ListView() @@ -375,9 +384,19 @@ open class ItemListControllerNode: ASDisplayNode { } else { transition = .immediate } - strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition) + if let headerItemNode = strongSelf.headerItemNode { + headerItemNode.updateContentOffset(value, transition: transition) + strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate) + } else { + strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition) + } case .unknown, .none: - strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate) + if let headerItemNode = strongSelf.headerItemNode { + headerItemNode.updateContentOffset(1000.0, transition: .immediate) + strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate) + } else { + strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate) + } } strongSelf.previousContentOffset = offset @@ -425,7 +444,7 @@ open class ItemListControllerNode: ASDisplayNode { scrollToItem = state.initialScrollToItem } - return ItemListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, toolbarItem: state.toolbarItem, footerItem: state.footerItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled) + return ItemListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, toolbarItem: state.toolbarItem, headerItem: state.headerItem, footerItem: state.footerItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled) }) |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { @@ -563,9 +582,16 @@ open class ItemListControllerNode: ASDisplayNode { } } + if let headerItemNode = self.headerItemNode { + let headerHeight = headerItemNode.updateLayout(layout: layout, transition: transition) + headerItemNode.frame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: 56.0)) + insets.top += headerHeight + } + + var footerHeight: CGFloat = 0.0 if let footerItemNode = self.footerItemNode { - let footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition) - insets.bottom += footerHeight + footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition) + insets.bottom = footerHeight } self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) @@ -594,6 +620,12 @@ open class ItemListControllerNode: ASDisplayNode { self.dequeueTransitions() } + var layout = layout + layout.intrinsicInsets.left = 4.0 + layout.intrinsicInsets.right = 4.0 + layout.intrinsicInsets.bottom = insets.bottom + self.controller?.presentationContext.containerLayoutUpdated(layout, transition: transition) + if !self.afterLayoutActions.isEmpty { let afterLayoutActions = self.afterLayoutActions self.afterLayoutActions = [] @@ -770,6 +802,64 @@ open class ItemListControllerNode: ASDisplayNode { self.toolbarItem = transition.toolbarItem } + var updateFooterItem = false + if let footerItem = self.footerItem, let updatedFooterItem = transition.footerItem { + updateFooterItem = !footerItem.isEqual(to: updatedFooterItem) + } else if (self.footerItem != nil) != (transition.footerItem != nil) { + updateFooterItem = true + } + if updateFooterItem { + let hadFooter = self.footerItem != nil + self.footerItem = transition.footerItem + if let footerItem = transition.footerItem { + let updatedNode = footerItem.node(current: self.footerItemNode) + if let footerItemNode = self.footerItemNode, updatedNode !== footerItemNode { + footerItemNode.removeFromSupernode() + } + if self.footerItemNode !== updatedNode { + self.footerItemNode = updatedNode + + let footerHeight: CGFloat + if let validLayout = self.validLayout { + footerHeight = updatedNode.updateLayout(layout: validLayout.0, transition: .immediate) + } else { + footerHeight = 100.0 + } + self.addSubnode(updatedNode) + + if !hadFooter && !transition.firstTime { + updatedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: footerHeight), to: .zero, duration: 0.25, additive: true) + } + + if !hadFooter, let (layout, navigationBarHeight, _) = self.validLayout { + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + insets.bottom = footerHeight + + let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) + if layout.size.width >= 375.0 { + insets.left += inset + insets.right += inset + } + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: .immediate) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + } else if let footerItemNode = self.footerItemNode { + let footerHeight: CGFloat + if let validLayout = self.validLayout { + footerHeight = footerItemNode.updateLayout(layout: validLayout.0, transition: .immediate) + } else { + footerHeight = 100.0 + } + footerItemNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: footerHeight), duration: 0.25, removeOnCompletion: false, additive: true, completion: { [weak footerItemNode] _ in + footerItemNode?.removeFromSupernode() + }) + self.footerItemNode = nil + } + } + self.listNode.transaction(deleteIndices: transition.entries.deletions, insertIndicesAndItems: transition.entries.insertions, updateIndicesAndItems: transition.entries.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: ItemListNodeOpaqueState(mergedEntries: transition.mergedEntries), completion: { [weak self] _ in if let strongSelf = self { if !strongSelf.didSetReady { @@ -855,46 +945,41 @@ open class ItemListControllerNode: ASDisplayNode { self.emptyStateNode = nil } } - var updateFooterItem = false - if let footerItem = self.footerItem, let updatedFooterItem = transition.footerItem { - updateFooterItem = !footerItem.isEqual(to: updatedFooterItem) - } else if (self.footerItem != nil) != (transition.footerItem != nil) { - updateFooterItem = true + var updateHeaderItem = false + if let headerItem = self.headerItem, let updatedHeaderItem = transition.headerItem { + updateHeaderItem = !headerItem.isEqual(to: updatedHeaderItem) + } else if (self.headerItem != nil) != (transition.headerItem != nil) { + updateHeaderItem = true } - if updateFooterItem { - let hadFooter = self.footerItem != nil - self.footerItem = transition.footerItem - if let footerItem = transition.footerItem { - let updatedNode = footerItem.node(current: self.footerItemNode) - if let footerItemNode = self.footerItemNode, updatedNode !== footerItemNode { - footerItemNode.removeFromSupernode() + if updateHeaderItem { + self.headerItem = transition.headerItem + if let headerItem = transition.headerItem { + let updatedNode = headerItem.node(current: self.headerItemNode) + if let headerItemNode = self.headerItemNode, updatedNode !== headerItemNode { + headerItemNode.removeFromSupernode() } - if self.footerItemNode !== updatedNode { - self.footerItemNode = updatedNode + if self.headerItemNode !== updatedNode { + self.headerItemNode = updatedNode - let footerHeight: CGFloat + let headerHeight: CGFloat if let validLayout = self.validLayout { - footerHeight = updatedNode.updateLayout(layout: validLayout.0, transition: .immediate) + headerHeight = updatedNode.updateLayout(layout: validLayout.0, transition: .immediate) } else { - footerHeight = 100.0 + headerHeight = 100.0 } + let _ = headerHeight self.addSubnode(updatedNode) - - if !hadFooter && !transition.firstTime { - updatedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: footerHeight), to: .zero, duration: 0.25, additive: true) - } } - } else if let footerItemNode = self.footerItemNode { - let footerHeight: CGFloat + } else if let headerItemNode = self.headerItemNode { + let headerHeight: CGFloat if let validLayout = self.validLayout { - footerHeight = footerItemNode.updateLayout(layout: validLayout.0, transition: .immediate) + headerHeight = headerItemNode.updateLayout(layout: validLayout.0, transition: .immediate) } else { - footerHeight = 100.0 + headerHeight = 100.0 } - footerItemNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: footerHeight), duration: 0.25, removeOnCompletion: false, additive: true, completion: { [weak footerItemNode] _ in - footerItemNode?.removeFromSupernode() - }) - self.footerItemNode = nil + let _ = headerHeight + headerItemNode.removeFromSupernode() + self.headerItemNode = nil } } self.listNode.scrollEnabled = transition.scrollEnabled diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index ae6f0a9275d..5dcf593bd1c 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -35,6 +35,7 @@ public enum ItemListDisclosureLabelStyle { case multilineDetailText case badge(UIColor) case color(UIColor) + case semitransparentBadge(UIColor) case image(image: UIImage, size: CGSize) } @@ -125,6 +126,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { } private let badgeFont = Font.regular(15.0) +private let boldBadgeFont = Font.semibold(14.0) public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode @@ -256,11 +258,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { var updatedLabelBadgeImage: UIImage? var updatedLabelImage: UIImage? + var badgeDiameter: CGFloat = 20.0 var badgeColor: UIColor? + var badgeColorUpdated = false if case let .badge(color) = item.labelStyle { if item.label.count > 0 { badgeColor = color } + } else if case let .semitransparentBadge(color) = item.labelStyle { + badgeDiameter = 24.0 + badgeColor = color.withAlphaComponent(0.1) + + badgeColorUpdated = true + if let currentItem = currentItem, case let .semitransparentBadge(previousColor) = currentItem.labelStyle, color.isEqual(previousColor) { + badgeColorUpdated = false + } } if case let .color(color) = item.labelStyle { var updatedColor = true @@ -277,7 +289,6 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { updatedLabelImage = image } - let badgeDiameter: CGFloat = 20.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme switch item.disclosureStyle { @@ -289,7 +300,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { if let badgeColor = badgeColor { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } - } else if let badgeColor = badgeColor, !currentHasBadge { + } else if let badgeColor = badgeColor, !currentHasBadge || badgeColorUpdated { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } @@ -313,7 +324,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { var additionalTextRightInset: CGFloat = 0.0 switch item.labelStyle { - case .badge: + case .badge, .semitransparentBadge: additionalTextRightInset += 44.0 default: break @@ -355,6 +366,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { case .badge: labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor labelFont = badgeFont + case let .semitransparentBadge(color): + labelBadgeColor = color + labelFont = boldBadgeFont case .detailText, .multilineDetailText: labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor labelFont = detailFont @@ -578,14 +592,19 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.labelBadgeNode.removeFromSupernode() } - let badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0) + var badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0) + if case .semitransparentBadge = item.labelStyle { + badgeWidth += 2.0 + } let badgeFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth, y: floor((contentSize.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) strongSelf.labelBadgeNode.frame = badgeFrame let labelFrame: CGRect switch item.labelStyle { case .badge: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1), size: labelLayout.size) + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0), size: labelLayout.size) + case .semitransparentBadge: + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0 - UIScreenPixel + floorToScreenPixels((badgeDiameter - labelLayout.size.height) / 2.0)), size: labelLayout.size) case .detailText, .multilineDetailText: labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) default: diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h index 16113759ffe..9fa2410cae6 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h @@ -48,7 +48,7 @@ typedef enum @class PGCameraDeviceAngleSampler; @class TGCameraPreviewView; -@interface PGCamera : NSObject +@interface PGCamera : NSObject @property (readonly, nonatomic) PGCameraCaptureSession *captureSession; @property (readonly, nonatomic) PGCameraDeviceAngleSampler *deviceAngleSampler; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h index ee08d0cb83b..2a816abd14f 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h @@ -9,10 +9,8 @@ @interface PGCameraCaptureSession : AVCaptureSession @property (nonatomic, readonly) AVCaptureDevice *videoDevice; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -@property (nonatomic, readonly) AVCaptureStillImageOutput *imageOutput; -#pragma clang diagnostic pop + +@property (nonatomic, readonly) AVCapturePhotoOutput *imageOutput; @property (nonatomic, readonly) AVCaptureVideoDataOutput *videoOutput; @property (nonatomic, readonly) AVCaptureAudioDataOutput *audioOutput; @property (nonatomic, readonly) AVCaptureMetadataOutput *metadataOutput; @@ -22,6 +20,7 @@ @property (nonatomic, assign) bool alwaysSetFlash; @property (nonatomic, assign) PGCameraMode currentMode; @property (nonatomic, assign) PGCameraFlashMode currentFlashMode; +@property (nonatomic, readonly) AVCaptureFlashMode currentDeviceFlashMode; @property (nonatomic, assign) PGCameraPosition currentCameraPosition; @property (nonatomic, readonly) PGCameraPosition preferredCameraPosition; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h index 9f9dbbbeec0..01f465c4a76 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h @@ -39,10 +39,11 @@ typedef enum @property (nonatomic, readonly) UIColor *badgeTextColor; @property (nonatomic, readonly) UIImage *sendIconImage; @property (nonatomic, readonly) UIImage *doneIconImage; +@property (nonatomic, readonly) UIImage *scheduleIconImage; @property (nonatomic, readonly) UIColor *maybeAccentColor; -+ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage maybeAccentColor:(UIColor *)maybeAccentColor; ++ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage scheduleIconImage:(UIImage *)scheduleIconImage maybeAccentColor:(UIColor *)maybeAccentColor; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h index ba7f57225c3..b354ce642b6 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h @@ -37,7 +37,7 @@ @property (nonatomic, readonly) UIView *timerButton; -- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName; +- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages; - (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel; - (void)setEditorTabPressed:(void (^)(TGPhotoEditorTab tab))editorTabPressed; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h index 4d4030a97d5..26d30bdef6d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h @@ -46,7 +46,7 @@ @property (nonatomic, readonly) TGMediaSelectionContext *selectionContext; @property (nonatomic, strong) id stickersContext; -- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName; +- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages; - (void)presentPhotoEditorForItem:(id)item tab:(TGPhotoEditorTab)tab; - (void)presentPhotoEditorForItem:(id)item tab:(TGPhotoEditorTab)tab snapshots:(NSArray *)snapshots fromRect:(CGRect)fromRect; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h index 7426a814abe..a5bee6a828e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h @@ -30,7 +30,8 @@ typedef enum { TGPhotoEditorDoneButtonSend, TGPhotoEditorDoneButtonCheck, - TGPhotoEditorDoneButtonDone + TGPhotoEditorDoneButtonDone, + TGPhotoEditorDoneButtonSchedule } TGPhotoEditorDoneButton; @interface TGPhotoToolbarView : UIView diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLRenderer.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLRenderer.h index fac303998c4..2adaf2234e9 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLRenderer.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLRenderer.h @@ -2,6 +2,8 @@ #import #import +@class TGVideoCameraRendererBuffer; + @interface TGVideoCameraGLRenderer : NSObject @property (nonatomic, readonly) __attribute__((NSObject)) CMFormatDescriptionRef outputFormatDescription; @@ -13,7 +15,7 @@ - (void)prepareForInputWithFormatDescription:(CMFormatDescriptionRef)inputFormatDescription outputRetainedBufferCountHint:(size_t)outputRetainedBufferCountHint; - (void)reset; -- (CVPixelBufferRef)copyRenderedPixelBuffer:(CVPixelBufferRef)pixelBuffer; -- (void)setPreviousPixelBuffer:(CVPixelBufferRef)previousPixelBuffer; +- (TGVideoCameraRendererBuffer *)copyRenderedPixelBuffer:(TGVideoCameraRendererBuffer *)pixelBuffer; +- (void)setPreviousPixelBuffer:(TGVideoCameraRendererBuffer *)previousPixelBuffer; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLView.h index c8513bd7c7a..a294481c4a8 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraGLView.h @@ -1,9 +1,11 @@ #import #import +@class TGVideoCameraRendererBuffer; + @interface TGVideoCameraGLView : UIView -- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer; +- (void)displayPixelBuffer:(TGVideoCameraRendererBuffer *)pixelBuffer; - (void)flushPixelBufferCache; - (void)reset; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraMovieRecorder.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraMovieRecorder.h index 00da00cc6e7..35d260f2ff9 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraMovieRecorder.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoCameraMovieRecorder.h @@ -2,6 +2,7 @@ #import @protocol TGVideoCameraMovieRecorderDelegate; +@class TGVideoCameraRendererBuffer; @interface TGVideoCameraMovieRecorder : NSObject @@ -15,7 +16,7 @@ - (void)prepareToRecord; -- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime; +- (void)appendVideoPixelBuffer:(TGVideoCameraRendererBuffer *)pixelBuffer withPresentationTime:(CMTime)presentationTime; - (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer; - (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer; diff --git a/submodules/LegacyComponents/Sources/PGCamera.m b/submodules/LegacyComponents/Sources/PGCamera.m index 446282eb11e..2e4348a9eb1 100644 --- a/submodules/LegacyComponents/Sources/PGCamera.m +++ b/submodules/LegacyComponents/Sources/PGCamera.m @@ -16,7 +16,7 @@ NSString *const PGCameraTorchAvailableKey = @"torchAvailable"; NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus"; -@interface PGCamera () +@interface PGCamera () { dispatch_queue_t cameraProcessingQueue; dispatch_queue_t audioProcessingQueue; @@ -34,11 +34,16 @@ @interface PGCamera () bool _capturing; bool _moment; - + TGCameraPreviewView *_previewView; + UIInterfaceOrientation _currentPhotoOrientation; + NSTimeInterval _captureStartTime; } + +@property (nonatomic, copy) void(^photoCaptureCompletionBlock)(UIImage *image, PGCameraShotMetadata *metadata); + @end @implementation PGCamera @@ -367,57 +372,58 @@ - (void)takePhotoWithCompletion:(void (^)(UIImage *result, PGCameraShotMetadata { bool videoMirrored = !self.disableResultMirroring ? _previewView.captureConnection.videoMirrored : false; - [[PGCameraCaptureSession cameraQueue] dispatch:^ + void (^takePhoto)(void) = ^ { - if (!self.captureSession.isRunning || self.captureSession.imageOutput.isCapturingStillImage || _invalidated) - return; - - void (^takePhoto)(void) = ^ + self.photoCaptureCompletionBlock = completion; + [[PGCameraCaptureSession cameraQueue] dispatch:^ { + if (!self.captureSession.isRunning || _invalidated) + return; + AVCaptureConnection *imageConnection = [self.captureSession.imageOutput connectionWithMediaType:AVMediaTypeVideo]; [imageConnection setVideoMirrored:videoMirrored]; UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait; if (self.requestedCurrentInterfaceOrientation != nil) orientation = self.requestedCurrentInterfaceOrientation(NULL); - [imageConnection setVideoOrientation:[PGCamera _videoOrientationForInterfaceOrientation:orientation mirrored:false]]; - [self.captureSession.imageOutput captureStillImageAsynchronouslyFromConnection:self.captureSession.imageOutput.connections.firstObject completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) - { - if (imageDataSampleBuffer != NULL && error == nil) - { - NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; - UIImage *image = [[UIImage alloc] initWithData:imageData]; - - if (self.cameraMode == PGCameraModeSquarePhoto || self.cameraMode == PGCameraModeSquareVideo || self.cameraMode == PGCameraModeSquareSwing) - { - CGFloat shorterSide = MIN(image.size.width, image.size.height); - CGFloat longerSide = MAX(image.size.width, image.size.height); - - CGRect cropRect = CGRectMake(CGFloor((longerSide - shorterSide) / 2.0f), 0, shorterSide, shorterSide); - CGImageRef croppedCGImage = CGImageCreateWithImageInRect(image.CGImage, cropRect); - image = [UIImage imageWithCGImage:croppedCGImage scale:image.scale orientation:image.imageOrientation]; - CGImageRelease(croppedCGImage); - } - - PGCameraShotMetadata *metadata = [[PGCameraShotMetadata alloc] init]; - metadata.deviceAngle = [PGCameraShotMetadata relativeDeviceAngleFromAngle:_deviceAngleSampler.currentDeviceAngle orientation:orientation]; - - image = [self normalizeImageOrientation:image]; - - if (completion != nil) - completion(image, metadata); - } - }]; - }; - - NSTimeInterval delta = CFAbsoluteTimeGetCurrent() - _captureStartTime; - if (CFAbsoluteTimeGetCurrent() - _captureStartTime > 0.4) - takePhoto(); - else - TGDispatchAfter(0.4 - delta, [[PGCameraCaptureSession cameraQueue] _dispatch_queue], takePhoto); - }]; + _currentPhotoOrientation = orientation; + + AVCapturePhotoSettings *photoSettings = [AVCapturePhotoSettings photoSettings]; + photoSettings.flashMode = self.captureSession.currentDeviceFlashMode; + [self.captureSession.imageOutput capturePhotoWithSettings:photoSettings delegate:self]; + }]; + }; + + NSTimeInterval delta = CFAbsoluteTimeGetCurrent() - _captureStartTime; + if (CFAbsoluteTimeGetCurrent() - _captureStartTime > 0.4) + takePhoto(); + else + TGDispatchAfter(0.4 - delta, [[PGCameraCaptureSession cameraQueue] _dispatch_queue], takePhoto); +} + +- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error { + if (error) { + NSLog(@"Error capturing photo: %@", error); + return; + } + + NSData *photoData = [photo fileDataRepresentation]; + UIImage *capturedImage = [UIImage imageWithData:photoData]; + + PGCameraShotMetadata *metadata = [[PGCameraShotMetadata alloc] init]; + metadata.deviceAngle = [PGCameraShotMetadata relativeDeviceAngleFromAngle:_deviceAngleSampler.currentDeviceAngle orientation:_currentPhotoOrientation]; + + UIImage *image = [self normalizeImageOrientation:capturedImage]; + + TGDispatchOnMainThread(^ + { + if (self.photoCaptureCompletionBlock != nil) { + self.photoCaptureCompletionBlock(image, metadata); + self.photoCaptureCompletionBlock = nil; + } + }); } - (void)startVideoRecordingForMoment:(bool)moment completion:(void (^)(NSURL *, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success))completion @@ -649,7 +655,7 @@ - (bool)flashActive if (self.cameraMode == PGCameraModeVideo || self.cameraMode == PGCameraModeSquareVideo || self.cameraMode == PGCameraModeSquareSwing) return self.captureSession.videoDevice.torchActive; - return self.captureSession.videoDevice.flashActive; + return self.captureSession.imageOutput.isFlashScene; } - (bool)flashAvailable diff --git a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m index e071d811169..28b7f4a23cc 100644 --- a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m +++ b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m @@ -130,8 +130,10 @@ - (void)performInitialConfigurationWithCompletion:(void (^)(void))completion [self setFrameRate:PGCameraFrameRate forDevice:_videoDevice]; } - AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init]; - [imageOutput setOutputSettings:@{AVVideoCodecKey : AVVideoCodecJPEG}]; + AVCapturePhotoOutput *imageOutput = [[AVCapturePhotoOutput alloc] init]; + if (@available(iOS 13.0, *)) { + [imageOutput setMaxPhotoQualityPrioritization:AVCapturePhotoQualityPrioritizationBalanced]; + } if ([self canAddOutput:imageOutput]) { #if !TARGET_IPHONE_SIMULATOR @@ -701,6 +703,10 @@ - (void)setExposureTargetBias:(CGFloat)bias #pragma mark - Flash +- (AVCaptureFlashMode)currentDeviceFlashMode { + return [PGCameraCaptureSession _deviceFlashModeForCameraFlashMode:self.currentFlashMode]; +} + - (PGCameraFlashMode)currentFlashMode { switch (self.currentMode) diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index 6cdb3342085..985cfbb9455 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1536,7 +1536,7 @@ - (void)presentResultControllerForItem:(id 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName]; + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:galleryItems focusItem:focusItem selectionContext:_items.count > 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName isScheduledMessages:false]; model.inhibitMute = self.inhibitMute; model.controller = galleryController; model.stickersContext = self.stickersContext; @@ -2407,13 +2407,23 @@ - (void)_dismissTransitionForResultController:(TGOverlayController *)resultContr self.view.hidden = true; + __weak TGCameraController *weakSelf = self; + __weak TGOverlayController *weakResultController = resultController; + [resultController.view.layer animatePositionFrom:resultController.view.layer.position to:CGPointMake(resultController.view.layer.position.x, resultController.view.layer.position.y + resultController.view.bounds.size.height) duration:0.3 timingFunction:kCAMediaTimingFunctionSpring removeOnCompletion:false completion:^(__unused bool finished) { - if (resultController.customDismissSelf) { - resultController.customDismissSelf(); - } else { - [resultController dismiss]; + TGCameraController *strongSelf = weakSelf; + TGOverlayController *strongResultController = weakResultController; + + if (strongResultController) { + if (strongResultController.customDismissSelf) { + strongResultController.customDismissSelf(); + } else { + [strongResultController dismiss]; + } + } + if (strongSelf) { + [strongSelf dismiss]; } - [self dismiss]; }]; } diff --git a/submodules/LegacyComponents/Sources/TGImageBlur.m b/submodules/LegacyComponents/Sources/TGImageBlur.m index 2b0017d0ad0..e175e5084b0 100644 --- a/submodules/LegacyComponents/Sources/TGImageBlur.m +++ b/submodules/LegacyComponents/Sources/TGImageBlur.m @@ -1387,6 +1387,14 @@ static void brightenImage(void *pixels, unsigned int width, unsigned int height, return TGSecretBlurredAttachmentWithCornerRadiusImage(source, size, averageColor, attachmentBorder, 13, position); } +#if DEBUG +@interface DebugTGSecretBlurredAttachmentWithCornerRadiusImage : UIImage +@end + +@implementation DebugTGSecretBlurredAttachmentWithCornerRadiusImage +@end +#endif + UIImage *TGSecretBlurredAttachmentWithCornerRadiusImage(UIImage *source, CGSize size, uint32_t *averageColor, bool attachmentBorder, CGFloat cornerRadius, int position) { CGFloat scale = TGScreenScaling(); //TGIsRetina() ? 2.0f : 1.0f; @@ -1496,7 +1504,11 @@ static void brightenImage(void *pixels, unsigned int width, unsigned int height, } CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext); +#if DEBUG + UIImage *image = [[DebugTGSecretBlurredAttachmentWithCornerRadiusImage alloc] initWithCGImage:bitmapImage]; +#else UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage]; +#endif CGImageRelease(bitmapImage); CGContextRelease(targetContext); diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 02bc4f651f4..90541a5d095 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -1894,7 +1894,7 @@ - (bool)allowGrouping @implementation TGMediaAssetsPallete -+ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage maybeAccentColor:(UIColor *)maybeAccentColor ++ (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)backgroundColor selectionColor:(UIColor *)selectionColor separatorColor:(UIColor *)separatorColor textColor:(UIColor *)textColor secondaryTextColor:(UIColor *)secondaryTextColor accentColor:(UIColor *)accentColor destructiveColor:(UIColor *)destructiveColor barBackgroundColor:(UIColor *)barBackgroundColor barSeparatorColor:(UIColor *)barSeparatorColor navigationTitleColor:(UIColor *)navigationTitleColor badge:(UIImage *)badge badgeTextColor:(UIColor *)badgeTextColor sendIconImage:(UIImage *)sendIconImage doneIconImage:(UIImage *)doneIconImage scheduleIconImage:(UIImage *)scheduleIconImage maybeAccentColor:(UIColor *)maybeAccentColor { TGMediaAssetsPallete *pallete = [[TGMediaAssetsPallete alloc] init]; pallete->_isDark = dark; @@ -1912,6 +1912,7 @@ + (instancetype)palleteWithDark:(bool)dark backgroundColor:(UIColor *)background pallete->_badgeTextColor = badgeTextColor; pallete->_sendIconImage = sendIconImage; pallete->_doneIconImage = doneIconImage; + pallete->_scheduleIconImage = scheduleIconImage; pallete->_maybeAccentColor = maybeAccentColor; return pallete; } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 983f7c3b012..5ed286a8fd0 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -125,7 +125,7 @@ @implementation TGMediaPickerGalleryInterfaceView @synthesize safeAreaInset = _safeAreaInset; -- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName +- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages { self = [super initWithFrame:CGRectZero]; if (self != nil) @@ -392,13 +392,15 @@ - (instancetype)initWithContext:(id)context focusItem:( _captionMixin.stickersContext = stickersContext; [_captionMixin createInputPanelIfNeeded]; - _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:TGPhotoEditorDoneButtonSend solidBackground:false]; + TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend; + + _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false]; _portraitToolbarView.cancelPressed = toolbarCancelPressed; _portraitToolbarView.donePressed = toolbarDonePressed; _portraitToolbarView.doneLongPressed = toolbarDoneLongPressed; [_wrapperView addSubview:_portraitToolbarView]; - _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:TGPhotoEditorDoneButtonSend solidBackground:false]; + _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false]; _landscapeToolbarView.cancelPressed = toolbarCancelPressed; _landscapeToolbarView.donePressed = toolbarDonePressed; _landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m index 6dd0f99aa7c..1f41d51c92e 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m @@ -39,6 +39,7 @@ @interface TGMediaPickerGalleryModel () bool _inhibitDocumentCaptions; NSString *_recipientName; bool _hasCamera; + bool _isScheduledMessages; } @property (nonatomic, weak) TGPhotoEditorController *editorController; @@ -47,7 +48,7 @@ @interface TGMediaPickerGalleryModel () @implementation TGMediaPickerGalleryModel -- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName +- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages { self = [super init]; if (self != nil) @@ -68,6 +69,7 @@ - (instancetype)initWithContext:(id)context items:(NSAr _inhibitDocumentCaptions = inhibitDocumentCaptions; _recipientName = recipientName; _hasCamera = hasCamera; + _isScheduledMessages = isScheduledMessages; __weak TGMediaPickerGalleryModel *weakSelf = self; if (selectionContext != nil) @@ -177,7 +179,7 @@ - (void)setCurrentItemWithIndex:(NSUInteger)index if (_interfaceView == nil) { __weak TGMediaPickerGalleryModel *weakSelf = self; - _interfaceView = [[TGMediaPickerGalleryInterfaceView alloc] initWithContext:_context focusItem:_initialFocusItem selectionContext:_selectionContext editingContext:_editingContext stickersContext:_stickersContext hasSelectionPanel:_hasSelectionPanel hasCameraButton:_hasCamera recipientName:_recipientName]; + _interfaceView = [[TGMediaPickerGalleryInterfaceView alloc] initWithContext:_context focusItem:_initialFocusItem selectionContext:_selectionContext editingContext:_editingContext stickersContext:_stickersContext hasSelectionPanel:_hasSelectionPanel hasCameraButton:_hasCamera recipientName:_recipientName isScheduledMessages:_isScheduledMessages]; _interfaceView.hasCaptions = _hasCaptions; _interfaceView.allowCaptionEntities = _allowCaptionEntities; _interfaceView.hasTimer = _hasTimer; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m index 72209d0a686..72127e4ac48 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m @@ -138,6 +138,8 @@ - (instancetype)initWithFrame:(CGRect)frame _playerItemDisposable = [[SMetaDisposable alloc] init]; _facesDisposable = [[SMetaDisposable alloc] init]; + _thumbnailsDisposable = [[SMetaDisposable alloc] init]; + _videoDurationVar = [[SVariable alloc] init]; _videoDurationDisposable = [[SMetaDisposable alloc] init]; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m index 48b742b426f..317f6cec14a 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m @@ -213,62 +213,8 @@ - (instancetype)initWithFrame:(CGRect)frame _trimView.startHandleMoved = ^(CGPoint translation) { __strong TGMediaPickerGalleryVideoScrubber *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - if (strongSelf->_animatingZoomIn) - return; - - UIView *trimView = strongSelf->_trimView; - - CGRect availableTrimRect = [strongSelf _scrubbingRect]; - CGRect normalScrubbingRect = [strongSelf _scrubbingRectZoomedIn:false]; - CGFloat originX = MAX(0, trimView.frame.origin.x + translation.x); - CGFloat delta = originX - trimView.frame.origin.x; - CGFloat maxWidth = availableTrimRect.size.width + normalScrubbingRect.origin.x * 2 - originX; - - CGRect trimViewRect = CGRectMake(originX, trimView.frame.origin.y, MIN(maxWidth, trimView.frame.size.width - delta), trimView.frame.size.height); - - NSTimeInterval trimStartPosition = 0.0; - NSTimeInterval trimEndPosition = 0.0; - [strongSelf _trimStartPosition:&trimStartPosition trimEndPosition:&trimEndPosition forTrimFrame:trimViewRect duration:strongSelf.duration]; - - NSTimeInterval duration = trimEndPosition - trimStartPosition; - - if (trimEndPosition - trimStartPosition < self.minimumLength) - return; - - if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength) - { - trimViewRect = CGRectMake(trimView.frame.origin.x + delta, trimView.frame.origin.y, trimView.frame.size.width, trimView.frame.size.height); - - [strongSelf _trimStartPosition:&trimStartPosition trimEndPosition:&trimEndPosition forTrimFrame:trimViewRect duration:strongSelf.duration]; - } - - trimView.frame = trimViewRect; - - [strongSelf _layoutTrimCurtainViews]; - - strongSelf->_trimStartValue = trimStartPosition; - strongSelf->_trimEndValue = trimEndPosition; - - [strongSelf setValue:trimStartPosition]; - - UIView *handle = strongSelf->_scrubberHandle; - handle.center = CGPointMake(trimView.frame.origin.x + 12 + handle.frame.size.width / 2, handle.center.y); - - UIView *dotHandle = strongSelf->_dotHandle; - dotHandle.center = CGPointMake(trimView.frame.origin.x + 12 + dotHandle.frame.size.width / 2, dotHandle.center.y); - - id delegate = strongSelf.delegate; - if ([delegate respondsToSelector:@selector(videoScrubber:editingStartValueDidChange:)]) - [delegate videoScrubber:strongSelf editingStartValueDidChange:trimStartPosition]; - - [strongSelf cancelZoomIn]; - if ([strongSelf zoomAvailable]) - { - strongSelf->_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceTrimStart; - [strongSelf performSelector:@selector(zoomIn) withObject:nil afterDelay:TGVideoScrubberZoomActivationInterval]; + if (strongSelf) { + [strongSelf startHandleMoved:translation]; } }; _trimView.endHandleMoved = ^(CGPoint translation) @@ -418,6 +364,65 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)startHandleMoved:(CGPoint)translation { + TGMediaPickerGalleryVideoScrubber *strongSelf = self; + + if (strongSelf->_animatingZoomIn) + return; + + UIView *trimView = strongSelf->_trimView; + + CGRect availableTrimRect = [strongSelf _scrubbingRect]; + CGRect normalScrubbingRect = [strongSelf _scrubbingRectZoomedIn:false]; + CGFloat originX = MAX(0, trimView.frame.origin.x + translation.x); + CGFloat delta = originX - trimView.frame.origin.x; + CGFloat maxWidth = availableTrimRect.size.width + normalScrubbingRect.origin.x * 2 - originX; + + CGRect trimViewRect = CGRectMake(originX, trimView.frame.origin.y, MIN(maxWidth, trimView.frame.size.width - delta), trimView.frame.size.height); + + NSTimeInterval trimStartPosition = 0.0; + NSTimeInterval trimEndPosition = 0.0; + [strongSelf _trimStartPosition:&trimStartPosition trimEndPosition:&trimEndPosition forTrimFrame:trimViewRect duration:strongSelf.duration]; + + NSTimeInterval duration = trimEndPosition - trimStartPosition; + + if (trimEndPosition - trimStartPosition < self.minimumLength) + return; + + if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength) + { + trimViewRect = CGRectMake(trimView.frame.origin.x + delta, trimView.frame.origin.y, trimView.frame.size.width, trimView.frame.size.height); + + [strongSelf _trimStartPosition:&trimStartPosition trimEndPosition:&trimEndPosition forTrimFrame:trimViewRect duration:strongSelf.duration]; + } + + trimView.frame = trimViewRect; + + [strongSelf _layoutTrimCurtainViews]; + + strongSelf->_trimStartValue = trimStartPosition; + strongSelf->_trimEndValue = trimEndPosition; + + [strongSelf setValue:trimStartPosition]; + + UIView *handle = strongSelf->_scrubberHandle; + handle.center = CGPointMake(trimView.frame.origin.x + 12 + handle.frame.size.width / 2, handle.center.y); + + UIView *dotHandle = strongSelf->_dotHandle; + dotHandle.center = CGPointMake(trimView.frame.origin.x + 12 + dotHandle.frame.size.width / 2, dotHandle.center.y); + + id delegate = strongSelf.delegate; + if ([delegate respondsToSelector:@selector(videoScrubber:editingStartValueDidChange:)]) + [delegate videoScrubber:strongSelf editingStartValueDidChange:trimStartPosition]; + + [strongSelf cancelZoomIn]; + if ([strongSelf zoomAvailable]) + { + strongSelf->_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceTrimStart; + [strongSelf performSelector:@selector(zoomIn) withObject:nil afterDelay:TGVideoScrubberZoomActivationInterval]; + } +} + - (void)setHasDotPicker:(bool)hasDotPicker { _hasDotPicker = hasDotPicker; _tapGestureRecognizer.enabled = hasDotPicker; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index c7080a2b7ec..40b4c39f7a2 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -84,7 +84,7 @@ - (instancetype)initWithContext:(id)context item:(id)it NSArray *galleryItems = [self prepareGalleryItemsForFetchResult:fetchResult selectionContext:selectionContext editingContext:editingContext stickersContext:stickersContext asFile:asFile enumerationBlock:enumerationBlock]; - TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:[_windowManager context] items:galleryItems focusItem:focusItem selectionContext:selectionContext editingContext:editingContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions hasSelectionPanel:true hasCamera:false recipientName:recipientName]; + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:[_windowManager context] items:galleryItems focusItem:focusItem selectionContext:selectionContext editingContext:editingContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions hasSelectionPanel:true hasCamera:false recipientName:recipientName isScheduledMessages:false]; _galleryModel = model; model.stickersContext = stickersContext; model.inhibitMute = inhibitMute; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerPhotoStripCell.m b/submodules/LegacyComponents/Sources/TGMediaPickerPhotoStripCell.m index 92e8eea200e..cad9fb880f4 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerPhotoStripCell.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerPhotoStripCell.m @@ -180,6 +180,9 @@ - (void)setItem:(NSObject *)item signal:(SSignal *)signal removable:(bool)remova { SSignal *adjustmentsSignal = [self.editingContext adjustmentsSignalForItem:video]; + if (_adjustmentsDisposable == nil) + _adjustmentsDisposable = [[SMetaDisposable alloc] init]; + __weak TGMediaPickerPhotoStripCell *weakSelf = self; [_adjustmentsDisposable setDisposable:[adjustmentsSignal startStrictWithNext:^(TGVideoEditAdjustments *next) { @@ -220,6 +223,9 @@ - (void)setItem:(NSObject *)item signal:(SSignal *)signal removable:(bool)remova { SSignal *adjustmentsSignal = [self.editingContext adjustmentsSignalForItem:asset]; + if (_adjustmentsDisposable == nil) + _adjustmentsDisposable = [[SMetaDisposable alloc] init]; + __weak TGMediaPickerPhotoStripCell *weakSelf = self; [_adjustmentsDisposable setDisposable:[adjustmentsSignal startStrictWithNext:^(TGVideoEditAdjustments *next) { diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m index bd8f581159e..8a8bd067e1c 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m @@ -112,6 +112,9 @@ - (void)setDoneButtonType:(TGPhotoEditorDoneButton)doneButtonType { doneImage = pallete != nil ? pallete.doneIconImage : TGTintedImage([UIImage imageNamed:@"Editor/Commit"], [UIColor whiteColor]); break; } + case TGPhotoEditorDoneButtonSchedule: + doneImage = pallete != nil ? pallete.scheduleIconImage : TGComponentsImageNamed(@"PhotoPickerSendIcon"); + break; default: { doneImage = pallete != nil ? pallete.sendIconImage : TGComponentsImageNamed(@"PhotoPickerSendIcon"); diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index ae56a52631c..386f2749f5e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -178,7 +178,7 @@ + (void)presentWithContext:(id)context controller:(TGVi galleryItem.editingContext = editingContext; galleryItem.stickersContext = stickersContext; - TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName]; + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName isScheduledMessages:false]; model.controller = galleryController; model.stickersContext = stickersContext; diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m b/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m index 9ea032f1d43..e4c58c03aad 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m +++ b/submodules/LegacyComponents/Sources/TGVideoCameraGLRenderer.m @@ -3,6 +3,56 @@ #import #import +#import "TGVideoCameraPipeline.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#ifndef GLES_SILENCE_DEPRECATION +#define GLES_SILENCE_DEPRECATION +#endif + +@interface TGVideoCameraGLRendererBufferPool : NSObject + +@property (nonatomic, assign) CVPixelBufferPoolRef pool; + +@end + +@implementation TGVideoCameraGLRendererBufferPool + +- (instancetype)initWithRetainedPool:(CVPixelBufferPoolRef)pool { + self = [super init]; + if (self != nil) { + _pool = pool; + } + return self; +} + +- (void)dealloc { + if (_pool) { + CVPixelBufferPoolRelease(_pool); + } +} + +@end + +@implementation TGVideoCameraRendererBuffer + +- (instancetype)initWithRetainedBuffer:(CVPixelBufferRef)buffer { + self = [super init]; + if (self != nil) { + _buffer = buffer; + } + return self; +} + +- (void)dealloc { + if (_buffer) { + CVPixelBufferRelease(_buffer); + } +} + +@end @interface TGVideoCameraGLRenderer () { @@ -10,11 +60,11 @@ @interface TGVideoCameraGLRenderer () CVOpenGLESTextureCacheRef _textureCache; CVOpenGLESTextureCacheRef _prevTextureCache; CVOpenGLESTextureCacheRef _renderTextureCache; - CVPixelBufferPoolRef _bufferPool; + TGVideoCameraGLRendererBufferPool *_bufferPool; CFDictionaryRef _bufferPoolAuxAttributes; CMFormatDescriptionRef _outputFormatDescription; - CVPixelBufferRef _previousPixelBuffer; + TGVideoCameraRendererBuffer *_previousPixelBuffer; TGPaintShader *_shader; GLint _frameUniform; @@ -194,20 +244,12 @@ - (bool)hasPreviousPixelbuffer return _previousPixelBuffer != NULL; } -- (void)setPreviousPixelBuffer:(CVPixelBufferRef)previousPixelBuffer +- (void)setPreviousPixelBuffer:(TGVideoCameraRendererBuffer *)previousPixelBuffer { - if (_previousPixelBuffer != NULL) - { - CFRelease(_previousPixelBuffer); - _previousPixelBuffer = NULL; - } - _previousPixelBuffer = previousPixelBuffer; - if (_previousPixelBuffer != NULL) - CFRetain(_previousPixelBuffer); } -- (CVPixelBufferRef)copyRenderedPixelBuffer:(CVPixelBufferRef)pixelBuffer +- (TGVideoCameraRendererBuffer *)copyRenderedPixelBuffer:(TGVideoCameraRendererBuffer *)pixelBuffer { static const GLfloat squareVertices[] = { @@ -217,13 +259,15 @@ - (CVPixelBufferRef)copyRenderedPixelBuffer:(CVPixelBufferRef)pixelBuffer 1.0f, 1.0f, }; - if (_offscreenBufferHandle == 0) - return NULL; + if (_offscreenBufferHandle == 0) { + return NULL; + } - if (pixelBuffer == NULL) - return NULL; + if (pixelBuffer == NULL) { + return nil; + } - const CMVideoDimensions srcDimensions = { (int32_t)CVPixelBufferGetWidth(pixelBuffer), (int32_t)CVPixelBufferGetHeight(pixelBuffer) }; + const CMVideoDimensions srcDimensions = { (int32_t)CVPixelBufferGetWidth(pixelBuffer.buffer), (int32_t)CVPixelBufferGetHeight(pixelBuffer.buffer) }; const CMVideoDimensions dstDimensions = CMVideoFormatDescriptionGetDimensions(_outputFormatDescription); EAGLContext *oldContext = [EAGLContext currentContext]; @@ -237,35 +281,72 @@ - (CVPixelBufferRef)copyRenderedPixelBuffer:(CVPixelBufferRef)pixelBuffer CVOpenGLESTextureRef srcTexture = NULL; CVOpenGLESTextureRef prevTexture = NULL; CVOpenGLESTextureRef dstTexture = NULL; - CVPixelBufferRef dstPixelBuffer = NULL; + CVPixelBufferRef dstPixelBufferValue = NULL; - err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, srcDimensions.width, srcDimensions.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &srcTexture); + err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer.buffer, NULL, GL_TEXTURE_2D, GL_RGBA, srcDimensions.width, srcDimensions.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &srcTexture); - if (!srcTexture || err) - goto bail; + if (!srcTexture || err) { + if (oldContext != _context) { + [EAGLContext setCurrentContext:oldContext]; + } + + if (srcTexture) { + CFRelease(srcTexture); + } + + if (prevTexture) { + CFRelease(prevTexture); + } + + if (dstTexture) { + CFRelease(dstTexture); + } + + return nil; + } bool hasPreviousTexture = false; if (_previousPixelBuffer != NULL) { - err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _prevTextureCache, _previousPixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, srcDimensions.width, srcDimensions.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &prevTexture); + err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _prevTextureCache, _previousPixelBuffer.buffer, NULL, GL_TEXTURE_2D, GL_RGBA, srcDimensions.width, srcDimensions.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &prevTexture); - if (!prevTexture || err) - goto bail; + if (!prevTexture || err) { + if (oldContext != _context) { + [EAGLContext setCurrentContext:oldContext]; + } + + if (srcTexture) { + CFRelease(srcTexture); + } + + if (prevTexture) { + CFRelease(prevTexture); + } + + if (dstTexture) { + CFRelease(dstTexture); + } + + return nil; + } hasPreviousTexture = true; } - err = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, _bufferPool, _bufferPoolAuxAttributes, &dstPixelBuffer); - if (err == kCVReturnWouldExceedAllocationThreshold) - { + err = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, _bufferPool.pool, _bufferPoolAuxAttributes, &dstPixelBufferValue); + if (err == kCVReturnWouldExceedAllocationThreshold) { CVOpenGLESTextureCacheFlush(_renderTextureCache, 0); - err = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, _bufferPool, _bufferPoolAuxAttributes, &dstPixelBuffer); + err = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, _bufferPool.pool, _bufferPoolAuxAttributes, &dstPixelBufferValue); } + TGVideoCameraRendererBuffer *dstPixelBuffer = nil; + if (dstPixelBufferValue) { + dstPixelBuffer = [[TGVideoCameraRendererBuffer alloc] initWithRetainedBuffer:dstPixelBufferValue]; + } if (err) goto bail; - err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _renderTextureCache, dstPixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, dstDimensions.width, dstDimensions.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &dstTexture); + err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _renderTextureCache, dstPixelBuffer.buffer, NULL, GL_TEXTURE_2D, GL_RGBA, dstDimensions.width, dstDimensions.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &dstTexture); if (!dstTexture || err) goto bail; @@ -388,7 +469,10 @@ - (bool)initializeBuffersWithOutputSize:(CGSize)outputSize retainedBufferCountHi _noMirrorUniform = [_shader uniformForKey:@"noMirror"]; size_t maxRetainedBufferCount = clientRetainedBufferCountHint + 1; - _bufferPool = [TGVideoCameraGLRenderer createPixelBufferPoolWithWidth:(int32_t)outputSize.width height:(int32_t)outputSize.height pixelFormat:kCVPixelFormatType_32BGRA maxBufferCount:(int32_t)maxRetainedBufferCount]; + CVPixelBufferPoolRef bufferPoolValue = [TGVideoCameraGLRenderer createPixelBufferPoolWithWidth:(int32_t)outputSize.width height:(int32_t)outputSize.height pixelFormat:kCVPixelFormatType_32BGRA maxBufferCount:(int32_t)maxRetainedBufferCount]; + if (bufferPoolValue) { + _bufferPool = [[TGVideoCameraGLRendererBufferPool alloc] initWithRetainedPool:bufferPoolValue]; + } if (!_bufferPool) { @@ -397,11 +481,11 @@ - (bool)initializeBuffersWithOutputSize:(CGSize)outputSize retainedBufferCountHi } _bufferPoolAuxAttributes = [TGVideoCameraGLRenderer createPixelBufferPoolAuxAttribute:(int32_t)maxRetainedBufferCount]; - [TGVideoCameraGLRenderer preallocatePixelBuffersInPool:_bufferPool auxAttributes:_bufferPoolAuxAttributes]; + [TGVideoCameraGLRenderer preallocatePixelBuffersInPool:_bufferPool.pool auxAttributes:_bufferPoolAuxAttributes]; CMFormatDescriptionRef outputFormatDescription = NULL; CVPixelBufferRef testPixelBuffer = NULL; - CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, _bufferPool, _bufferPoolAuxAttributes, &testPixelBuffer); + CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, _bufferPool.pool, _bufferPoolAuxAttributes, &testPixelBuffer); if (!testPixelBuffer) { success = false; @@ -460,11 +544,7 @@ - (void)deleteBuffers _renderTextureCache = 0; } - if (_bufferPool) - { - CFRelease(_bufferPool); - _bufferPool = NULL; - } + _bufferPool = nil; if (_bufferPoolAuxAttributes) { @@ -525,3 +605,5 @@ + (void)preallocatePixelBuffersInPool:(CVPixelBufferPoolRef)pool auxAttributes:( } @end + +#pragma clang diagnostic pop diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraGLView.m b/submodules/LegacyComponents/Sources/TGVideoCameraGLView.m index 7c44c8651c9..4d12955387a 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraGLView.m +++ b/submodules/LegacyComponents/Sources/TGVideoCameraGLView.m @@ -6,6 +6,7 @@ #import #import "LegacyComponentsInternal.h" +#import "TGVideoCameraPipeline.h" @interface TGVideoCameraGLView () { @@ -134,7 +135,7 @@ - (void)dealloc [self reset]; } -- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer +- (void)displayPixelBuffer:(TGVideoCameraRendererBuffer *)pixelBuffer { static const GLfloat squareVertices[] = { @@ -161,10 +162,10 @@ - (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer return; } - size_t frameWidth = CVPixelBufferGetWidth(pixelBuffer); - size_t frameHeight = CVPixelBufferGetHeight(pixelBuffer); + size_t frameWidth = CVPixelBufferGetWidth(pixelBuffer.buffer); + size_t frameHeight = CVPixelBufferGetHeight(pixelBuffer.buffer); CVOpenGLESTextureRef texture = NULL; - CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, (GLsizei)frameWidth, (GLsizei)frameHeight, GL_BGRA, GL_UNSIGNED_BYTE, 0, &texture); + CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer.buffer, NULL, GL_TEXTURE_2D, GL_RGBA, (GLsizei)frameWidth, (GLsizei)frameHeight, GL_BGRA, GL_UNSIGNED_BYTE, 0, &texture); if (!texture || err) return; diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraMovieRecorder.m b/submodules/LegacyComponents/Sources/TGVideoCameraMovieRecorder.m index 1f513604e6a..281c325ada7 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraMovieRecorder.m +++ b/submodules/LegacyComponents/Sources/TGVideoCameraMovieRecorder.m @@ -1,6 +1,8 @@ #import "TGVideoCameraMovieRecorder.h" #import +#import "TGVideoCameraPipeline.h" + typedef enum { TGMovieRecorderStatusIdle = 0, TGMovieRecorderStatusPreparingToRecord, @@ -146,7 +148,7 @@ - (void)prepareToRecord } ); } -- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime +- (void)appendVideoPixelBuffer:(TGVideoCameraRendererBuffer *)pixelBuffer withPresentationTime:(CMTime)presentationTime { CMSampleBufferRef sampleBuffer = NULL; @@ -155,7 +157,7 @@ - (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTim timingInfo.decodeTimeStamp = kCMTimeInvalid; timingInfo.presentationTimeStamp = presentationTime; - CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, true, NULL, NULL, _videoTrackSourceFormatDescription, &timingInfo, &sampleBuffer); + CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer.buffer, true, NULL, NULL, _videoTrackSourceFormatDescription, &timingInfo, &sampleBuffer); if (sampleBuffer) { diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h index 63e37576a6a..76e843a1ab3 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h +++ b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h @@ -4,6 +4,13 @@ @protocol TGVideoCameraPipelineDelegate; +@interface TGVideoCameraRendererBuffer : NSObject + +@property (nonatomic, assign) CVPixelBufferRef buffer; + +- (instancetype)initWithRetainedBuffer:(CVPixelBufferRef)buffer; + +@end @interface TGVideoCameraPipeline : NSObject @@ -40,7 +47,7 @@ - (void)capturePipeline:(TGVideoCameraPipeline *)capturePipeline didStopRunningWithError:(NSError *)error; -- (void)capturePipeline:(TGVideoCameraPipeline *)capturePipeline previewPixelBufferReadyForDisplay:(CVPixelBufferRef)previewPixelBuffer; +- (void)capturePipeline:(TGVideoCameraPipeline *)capturePipeline previewPixelBufferReadyForDisplay:(TGVideoCameraRendererBuffer *)previewPixelBuffer; - (void)capturePipelineDidRunOutOfPreviewBuffers:(TGVideoCameraPipeline *)capturePipeline; - (void)capturePipelineRecordingDidStart:(TGVideoCameraPipeline *)capturePipeline; diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m index 34d5231ed67..58609f8d2fc 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m +++ b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m @@ -57,7 +57,7 @@ @interface TGVideoCameraPipeline () 0) @@ -506,7 +502,11 @@ - (void)renderVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer [_renderer setPreviousPixelBuffer:NULL]; } - CVPixelBufferRef sourcePixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferRef sourcePixelBufferValue = CMSampleBufferGetImageBuffer(sampleBuffer); + TGVideoCameraRendererBuffer *sourcePixelBuffer = nil; + if (sourcePixelBufferValue) { + sourcePixelBuffer = [[TGVideoCameraRendererBuffer alloc] initWithRetainedBuffer:CVPixelBufferRetain(sourcePixelBufferValue)]; + } renderedPixelBuffer = [_renderer copyRenderedPixelBuffer:sourcePixelBuffer]; @synchronized (self) @@ -542,14 +542,11 @@ - (void)renderVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer if (!repeatingFrames) { - if (_previousPixelBuffer != NULL) - { - CFRelease(_previousPixelBuffer); + if (_previousPixelBuffer != NULL) { _previousPixelBuffer = NULL; } _previousPixelBuffer = sourcePixelBuffer; - CFRetain(sourcePixelBuffer); } } } @@ -568,8 +565,6 @@ - (void)renderVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer if (_recordingStatus == TGVideoCameraRecordingStatusRecording) [_recorder appendVideoPixelBuffer:renderedPixelBuffer withPresentationTime:timestamp]; } - - CFRelease(renderedPixelBuffer); } else { @@ -577,33 +572,27 @@ - (void)renderVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer } } -- (void)outputPreviewPixelBuffer:(CVPixelBufferRef)previewPixelBuffer +- (void)outputPreviewPixelBuffer:(TGVideoCameraRendererBuffer *)previewPixelBuffer { if (_currentPreviewPixelBuffer != NULL) { - CFRelease(_currentPreviewPixelBuffer); _currentPreviewPixelBuffer = NULL; } if (_previousPixelBuffer != NULL) { _currentPreviewPixelBuffer = previewPixelBuffer; - CFRetain(_currentPreviewPixelBuffer); } [self invokeDelegateCallbackAsync:^ { - CVPixelBufferRef currentPreviewPixelBuffer = NULL; + TGVideoCameraRendererBuffer *currentPreviewPixelBuffer = nil; @synchronized (self) { currentPreviewPixelBuffer = _currentPreviewPixelBuffer; - if (currentPreviewPixelBuffer != NULL) - { - CFRetain(currentPreviewPixelBuffer); - if (_currentPreviewPixelBuffer != NULL) - { - CFRelease(_currentPreviewPixelBuffer); - _currentPreviewPixelBuffer = NULL; + if (currentPreviewPixelBuffer != NULL) { + if (_currentPreviewPixelBuffer != NULL) { + _currentPreviewPixelBuffer = nil; } } } @@ -611,7 +600,6 @@ - (void)outputPreviewPixelBuffer:(CVPixelBufferRef)previewPixelBuffer if (currentPreviewPixelBuffer != NULL) { [_delegate capturePipeline:self previewPixelBufferReadyForDisplay:currentPreviewPixelBuffer]; - CFRelease(currentPreviewPixelBuffer); } }]; } diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m index b734ba1d5f2..8c40c0d823a 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m @@ -1280,7 +1280,7 @@ - (void)capturePipeline:(TGVideoCameraPipeline *)__unused capturePipeline didSto { } -- (void)capturePipeline:(TGVideoCameraPipeline *)__unused capturePipeline previewPixelBufferReadyForDisplay:(CVPixelBufferRef)previewPixelBuffer +- (void)capturePipeline:(TGVideoCameraPipeline *)__unused capturePipeline previewPixelBufferReadyForDisplay:(TGVideoCameraRendererBuffer *)previewPixelBuffer { if (!_gpuAvailable) return; diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index 9e743545d80..b919be5465a 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -27,7 +27,6 @@ swift_library( "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/StickerResources:StickerResources", "//submodules/TextFormat:TextFormat", - "//submodules/AttachmentUI:AttachmentUI", "//submodules/DrawingUI:DrawingUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/TelegramUI/Components/MediaEditor", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 20c32435fcc..14b8218342c 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -14,7 +14,6 @@ import MimeTypes import LocalMediaResources import LegacyUI import TextFormat -import AttachmentUI public func guessMimeTypeByFileExtension(_ ext: String) -> String { return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary" @@ -60,64 +59,6 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co } } -public class LegacyAssetPickerContext: AttachmentMediaPickerContext { - private weak var controller: TGMediaAssetsController? - - public var selectionCount: Signal { - return Signal { [weak self] subscriber in - let disposable = self?.controller?.selectionContext.selectionChangedSignal().start(next: { [weak self] value in - subscriber.putNext(Int(self?.controller?.selectionContext.count() ?? 0)) - }, error: { _ in }, completed: { }) - return ActionDisposable { - disposable?.dispose() - } - } - } - - public var caption: Signal { - return Signal { [weak self] subscriber in - let disposable = self?.controller?.editingContext.forcedCaption().start(next: { caption in - if let caption = caption as? NSAttributedString { - subscriber.putNext(caption) - } else { - subscriber.putNext(nil) - } - }, error: { _ in }, completed: { }) - return ActionDisposable { - disposable?.dispose() - } - } - } - - public var loadingProgress: Signal { - return .single(nil) - } - - public var mainButtonState: Signal { - return .single(nil) - } - - public init(controller: TGMediaAssetsController) { - self.controller = controller - } - - public func setCaption(_ caption: NSAttributedString) { - self.controller?.editingContext.setForcedCaption(caption, skipUpdate: true) - } - - public func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) { - self.controller?.send(mode == .silently, whenOnline: mode == .whenOnline) - } - - public func schedule() { - self.controller?.schedule(false) - } - - public func mainButtonAction() { - - } -} - public func legacyAssetPicker(context: AccountContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, threadTitle: String?, saveEditedPhotos: Bool, allowGrouping: Bool, selectionLimit: Int) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> { let isSecretChat = (peer?.id.namespace._internalGetInt32Value() ?? 0) == Namespaces.Peer.SecretChat._internalGetInt32Value() @@ -353,7 +294,7 @@ public func legacyEnqueueGifMessage(account: Account, data: Data, correlationId: fileAttributes.append(.Animated) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) - subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) + subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) subscriber.putCompletion() } else { subscriber.putError(Void()) @@ -395,7 +336,7 @@ public func legacyEnqueueVideoMessage(account: Account, data: Data, correlationI fileAttributes.append(.Animated) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) - subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) + subscriber.putNext(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) subscriber.putCompletion() } else { subscriber.putError(Void()) @@ -437,7 +378,11 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpeg" let scaledSize = image.size.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0)) if let scaledImage = TGScaleImageToPixelSize(image, scaledSize) { - if let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.6) { + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + if let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.6, tempFilePath: tempFile.path) { let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) @@ -491,7 +436,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + 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: false)) } } case let .asset(asset): @@ -565,7 +510,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + 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: false)) } } } @@ -615,7 +560,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: false)) + 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: false)) } case .tempFile: break @@ -667,7 +612,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true)) + 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: true)) case let .asset(asset): var randomId: Int64 = 0 arc4random_buf(&randomId, 8) @@ -702,7 +647,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: true)) + 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: true)) default: break } @@ -749,9 +694,13 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } + let defaultPreset = TGMediaVideoConversionPreset(rawValue: UInt32(UserDefaults.standard.integer(forKey: "TG_preferredVideoPreset_v0"))) + var preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetCompressedMedium if let selectedPreset = adjustments?.preset { preset = selectedPreset + } else if preset == TGMediaVideoConversionPresetCompressedDefault && defaultPreset != TGMediaVideoConversionPresetCompressedDefault { + preset = defaultPreset } if asAnimation { preset = TGMediaVideoConversionPresetAnimation @@ -863,7 +812,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile)) + 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)) } } } diff --git a/submodules/LegacyUI/BUILD b/submodules/LegacyUI/BUILD index 990dc5d3336..d398f76aed3 100644 --- a/submodules/LegacyUI/BUILD +++ b/submodules/LegacyUI/BUILD @@ -20,7 +20,6 @@ swift_library( "//submodules/DeviceAccess:DeviceAccess", "//submodules/LegacyComponents:LegacyComponents", "//submodules/StickerResources:StickerResources", - "//submodules/AttachmentUI:AttachmentUI", ], visibility = [ "//visibility:public", diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index b9b528a5279..11257c2b603 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -4,7 +4,6 @@ import Display import SwiftSignalKit import LegacyComponents import TelegramPresentationData -import AttachmentUI public enum LegacyControllerPresentation { case custom @@ -387,7 +386,7 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { let navigationBar = presentationTheme.rootController.navigationBar let tabBar = presentationTheme.rootController.tabBar - return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor) + return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), scheduleIconImage: PresentationResourcesChat.chatInputPanelScheduleButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor) } public func checkButtonPallete() -> TGCheckButtonPallete! { @@ -403,7 +402,7 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { } } -open class LegacyController: ViewController, PresentableController, AttachmentContainable { +open class LegacyController: ViewController, PresentableController { public private(set) var legacyController: UIViewController! private let presentation: LegacyControllerPresentation @@ -439,18 +438,7 @@ open class LegacyController: ViewController, PresentableController, AttachmentCo } public var disposables = DisposableSet() - - open var requestAttachmentMenuExpansion: () -> Void = {} - open var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } - open var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } - open var cancelPanGesture: () -> Void = { } - open var isContainerPanning: () -> Bool = { return false } - open var isContainerExpanded: () -> Bool = { return false } - - public var mediaPickerContext: AttachmentMediaPickerContext? { - return nil - } - + public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { self.sizeClass.set(SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber)) self.presentation = presentation diff --git a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift index 60791e2cfed..4f0a7df81a6 100644 --- a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift +++ b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift @@ -290,7 +290,7 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone let navigationBar = presentationTheme.rootController.navigationBar let tabBar = presentationTheme.rootController.tabBar - return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor) + return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), scheduleIconImage: PresentationResourcesChat.chatInputPanelScheduleButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor) } func checkButtonPallete() -> TGCheckButtonPallete! { diff --git a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift index 88c2bcca61f..1abae79c80a 100644 --- a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift +++ b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift @@ -134,7 +134,11 @@ public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, he switch format { case .none, .jpeg: - if let scaledImage = scaledImage, let data = compressImageToJPEG(scaledImage, quality: 0.6) { + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + if let scaledImage = scaledImage, let data = compressImageToJPEG(scaledImage, quality: 0.6, tempFilePath: tempFile.path) { #if DEBUG print("compression completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") #endif diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift index 9a838c35d8a..aa5ead74492 100644 --- a/submodules/LocationUI/Sources/LocationActionListItem.swift +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -9,6 +9,7 @@ import ItemListUI import LocationResources import AppBundle import LiveLocationTimerNode +import TelegramStringFormatting public enum LocationActionListItemIcon: Equatable { case location @@ -280,14 +281,6 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.iconNode.isHidden = true strongSelf.venueIconNode.isHidden = false - func flagEmoji(countryCode: String) -> String { - let base : UInt32 = 127397 - var flagString = "" - for v in countryCode.uppercased().unicodeScalars { - flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!) - } - return flagString - } let type = venue.venue?.type var flag: String? if let venue = venue.venue, venue.provider == "city", let countryCode = venue.id { diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift index 933ea8ec177..3abf13348be 100644 --- a/submodules/LocationUI/Sources/LocationUtils.swift +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -49,7 +49,12 @@ public func nearbyVenues(context: AccountContext, story: Bool = false, latitude: return botUsername |> mapToSignal { botUsername in return context.engine.peers.resolvePeerByName(name: botUsername) - |> take(1) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in guard let peer = peer else { return .single(nil) diff --git a/submodules/ManagedFile/Package.swift b/submodules/ManagedFile/Package.swift index 67e49adcbd2..48bde4e5b6f 100644 --- a/submodules/ManagedFile/Package.swift +++ b/submodules/ManagedFile/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "ManagedFile", - platforms: [.macOS(.v10_12)], + 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/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index f0464bd51aa..a2495febd07 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -135,7 +135,7 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, } } - let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: recipientName)! + let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: recipientName, isScheduledMessages: isScheduledMessages)! model.stickersContext = paintStickersContext controller.model = model model.controller = controller diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 023e4552da2..2a36c5036aa 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -855,7 +855,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) var peers = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil) let previewText = groupLayouts.count > 1 ? presentationData.strings.Attachment_MessagesPreview : presentationData.strings.Attachment_MessagePreview diff --git a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift index 260e5e54f32..9dd0b0c1219 100644 --- a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift @@ -65,7 +65,7 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { while true { let result = self.codecContext.receive(into: self.audioFrame) if case .success = result { - if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) { + if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts) { self.delayedFrames.append(convertedFrame) } } else { @@ -121,7 +121,7 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { } } - private func convertAudioFrame(_ frame: FFMpegAVFrame, pts: CMTime, duration: CMTime) -> MediaTrackFrame? { + private func convertAudioFrame(_ frame: FFMpegAVFrame, pts: CMTime) -> MediaTrackFrame? { guard let data = self.swrContext.resample(frame) else { return nil } @@ -135,18 +135,12 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { return nil } - //var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: pts, decodeTimeStamp: pts) var sampleBuffer: CMSampleBuffer? - //var sampleSize = data.count guard CMAudioSampleBufferCreateReadyWithPacketDescriptions(allocator: nil, dataBuffer: blockBuffer!, formatDescription: self.formatDescription, sampleCount: Int(data.count / 2), presentationTimeStamp: pts, packetDescriptions: nil, sampleBufferOut: &sampleBuffer) == noErr else { return nil } - /*guard CMSampleBufferCreate(allocator: nil, dataBuffer: blockBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: self.formatDescription, sampleCount: Int(frame.duration), sampleTimingEntryCount: 1, sampleTimingArray: &timingInfo, sampleSizeEntryCount: 1, sampleSizeArray: &sampleSize, sampleBufferOut: &sampleBuffer) == noErr else { - return nil - }*/ - let resetDecoder = self.resetDecoderOnNextFrame self.resetDecoderOnNextFrame = false diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift index b52a8e05220..2cbbed178af 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift @@ -85,7 +85,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa } if totalCount > maximumFetchSize { context.readingError = true - return 0 + return FFMPEG_CONSTANT_AVERROR_EOF } } @@ -118,7 +118,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa disposable.dispose() if !completedRequest { context.readingError = true - return 0 + return FFMPEG_CONSTANT_AVERROR_EOF } } } @@ -176,7 +176,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa disposable.dispose() if !completedRequest { context.readingError = true - return 0 + return FFMPEG_CONSTANT_AVERROR_EOF } } } @@ -192,7 +192,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa if context.closed { context.readingError = true - return 0 + return FFMPEG_CONSTANT_AVERROR_EOF } return fetchedCount } @@ -725,7 +725,7 @@ private func videoFrameFromPacket(_ packet: FFMpegPacket, videoStream: StreamCon if frameDuration != 0 { duration = CMTimeMake(value: frameDuration * videoStream.timebase.value, timescale: videoStream.timebase.timescale) } else { - duration = videoStream.fps + duration = CMTimeMake(value: Int64(videoStream.fps.timescale), timescale: Int32(videoStream.fps.value)) } return MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration) diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index a9dad5d9f13..577cb8f2fef 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -11,9 +11,13 @@ import FFMpegBinding private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() if let fd = context.fd { - return Int32(read(fd, buffer, Int(bufferSize))) + let result = read(fd, buffer, Int(bufferSize)) + if result == 0 { + return FFMPEG_CONSTANT_AVERROR_EOF + } + return Int32(result) } - return 0 + return FFMPEG_CONSTANT_AVERROR_EOF } private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index 0605e035681..8b581bc6b67 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -71,7 +71,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa context.readingOffset += Int64(fetchedCount) return fetchedCount } else { - return 0 + return FFMPEG_CONSTANT_AVERROR_EOF } } diff --git a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h index 27ec64c926d..ba76697e7e6 100644 --- a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h +++ b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h @@ -4,7 +4,7 @@ extern "C" { #endif -NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage); +NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage, NSString * _Nonnull tempFilePath); NSArray * _Nonnull extractJPEGDataScans(NSData * _Nonnull data); NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size); UIImage * _Nullable decompressImage(NSData * _Nonnull sourceData); diff --git a/submodules/MozjpegBinding/Sources/MozjpegBinding.mm b/submodules/MozjpegBinding/Sources/MozjpegBinding.mm index 167f661f718..509929aeb78 100644 --- a/submodules/MozjpegBinding/Sources/MozjpegBinding.mm +++ b/submodules/MozjpegBinding/Sources/MozjpegBinding.mm @@ -412,7 +412,12 @@ static inline float JXLGetDistance(int32_t quality) { return result; } #else -NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage) { +NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage, NSString * _Nonnull tempFilePath) { + FILE *outfile = fopen([tempFilePath UTF8String], "w"); + if (!outfile) { + return nil; + } + int width = (int)(sourceImage.size.width * sourceImage.scale); int height = (int)(sourceImage.size.height * sourceImage.scale); @@ -458,9 +463,7 @@ static inline float JXLGetDistance(int32_t quality) { cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); - uint8_t *outBuffer = NULL; - unsigned long outSize = 0; - jpeg_mem_dest(&cinfo, &outBuffer, &outSize); + jpeg_stdio_dest(&cinfo, outfile); cinfo.image_width = (uint32_t)width; cinfo.image_height = (uint32_t)height; @@ -482,13 +485,16 @@ static inline float JXLGetDistance(int32_t quality) { } jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); - NSData *result = [[NSData alloc] initWithBytes:outBuffer length:outSize]; + fclose(outfile); - jpeg_destroy_compress(&cinfo); + NSData *result = [[NSData alloc] initWithContentsOfFile:tempFilePath]; free(buffer); + [[NSFileManager defaultManager] removeItemAtPath:tempFilePath error:nil]; + return result; } #endif diff --git a/submodules/MtProtoKit/Package.swift b/submodules/MtProtoKit/Package.swift index d4644333355..7adf5f2beff 100644 --- a/submodules/MtProtoKit/Package.swift +++ b/submodules/MtProtoKit/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "MtProtoKit", - platforms: [.macOS(.v10_12)], + 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/MtProtoKit/PublicHeaders/MtProtoKit/MTSignal.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTSignal.h index c7e852057d9..d00926cfd46 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTSignal.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTSignal.h @@ -13,8 +13,13 @@ - (instancetype)initWithGenerator:(id (^)(MTSubscriber *))generator; - (id)startWithNext:(void (^)(id next))next error:(void (^)(id error))error completed:(void (^)())completed; +- (id)startWithNextStrict:(void (^)(id next))next error:(void (^)(id error))error completed:(void (^)())completed file:(const char *)file line:(int)line; + - (id)startWithNext:(void (^)(id next))next; +- (id)startWithNextStrict:(void (^)(id next))next file:(const char *)file line:(int)line; + - (id)startWithNext:(void (^)(id next))next completed:(void (^)())completed; +- (id)startWithNextStrict:(void (^)(id next))next completed:(void (^)())completed file:(const char *)file line:(int)line; + (MTSignal *)single:(id)next; + (MTSignal *)fail:(id)error; diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index e39218c3270..e9aeecf4a72 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -595,6 +595,14 @@ - (NSString *)platformString return @"iPhone 14 Pro"; if ([platform isEqualToString:@"iPhone15,3"]) return @"iPhone 14 Pro Max"; + if ([platform isEqualToString:@"iPhone15,4"]) + return @"iPhone 15"; + if ([platform isEqualToString:@"iPhone15,5"]) + return @"iPhone 15 Plus"; + if ([platform isEqualToString:@"iPhone16,1"]) + return @"iPhone 15 Pro"; + if ([platform isEqualToString:@"iPhone16,2"]) + return @"iPhone 15 Pro Max"; if ([platform hasPrefix:@"iPod1"]) return @"iPod touch 1G"; diff --git a/submodules/MtProtoKit/Sources/MTContext.m b/submodules/MtProtoKit/Sources/MTContext.m index 7b425dc085b..dba8f3f3946 100644 --- a/submodules/MtProtoKit/Sources/MTContext.m +++ b/submodules/MtProtoKit/Sources/MTContext.m @@ -1030,15 +1030,17 @@ - (void)publicKeysForDatacenterWithIdRequired:(NSInteger)datacenterId { __weak MTContext *weakSelf = self; MTMetaDisposable *disposable = [[MTMetaDisposable alloc] init]; _fetchPublicKeysActions[@(datacenterId)] = disposable; - [disposable setDisposable:[signal startWithNext:^(NSArray *next) { + [disposable setDisposable:[signal startWithNextStrict:^(NSArray *next) { [[MTContext contextQueue] dispatchOnQueue:^{ __strong MTContext *strongSelf = weakSelf; if (strongSelf != nil) { + id disposable = strongSelf->_fetchPublicKeysActions[@(datacenterId)]; [strongSelf->_fetchPublicKeysActions removeObjectForKey:@(datacenterId)]; + [disposable dispose]; [strongSelf updatePublicKeysForDatacenterWithId:datacenterId publicKeys:next]; } } synchronous:false]; - }]]; + } file:__FILE_NAME__ line:__LINE__]]; break; } } @@ -1157,10 +1159,12 @@ - (void)transportSchemeForDatacenterWithIdRequired:(NSInteger)datacenterId moreO { [[MTContext contextQueue] dispatchOnQueue:^ { + id disposable = strongSelf->_transportSchemeDisposableByDatacenterId[@(datacenterId)]; [strongSelf->_transportSchemeDisposableByDatacenterId removeObjectForKey:@(datacenterId)]; + [disposable dispose]; }]; } - }] startWithNext:^(MTTransportScheme *next) + }] startWithNextStrict:^(MTTransportScheme *next) { if (MTLogEnabled()) { MTLog(@"scheme: %@", next); @@ -1176,7 +1180,7 @@ - (void)transportSchemeForDatacenterWithIdRequired:(NSInteger)datacenterId moreO } completed:^ { - }]; + } file:__FILE_NAME__ line:__LINE__]; } }]; } @@ -1293,7 +1297,7 @@ - (void)_beginBackupAddressDiscoveryWithDelay:(double)delay { [strongSelf->_backupAddressListDisposable dispose]; strongSelf->_backupAddressListDisposable = nil; } - }] startWithNext:nil]; + }] startWithNextStrict:nil file:__FILE_NAME__ line:__LINE__]; } } @@ -1491,7 +1495,7 @@ - (void)checkIfLoggedOut:(NSInteger)datacenterId { _datacenterCheckKeyRemovedActionTimestamps[@(datacenterId)] = currentTimestamp; [_datacenterCheckKeyRemovedActions[@(datacenterId)] dispose]; __weak MTContext *weakSelf = self; - _datacenterCheckKeyRemovedActions[@(datacenterId)] = [[MTDiscoverConnectionSignals checkIfAuthKeyRemovedWithContext:self datacenterId:datacenterId authKey:[[MTDatacenterAuthKey alloc] initWithAuthKey:authInfo.authKey authKeyId:authInfo.authKeyId validUntilTimestamp:authInfo.validUntilTimestamp notBound:false]] startWithNext:^(NSNumber* isRemoved) { + _datacenterCheckKeyRemovedActions[@(datacenterId)] = [[MTDiscoverConnectionSignals checkIfAuthKeyRemovedWithContext:self datacenterId:datacenterId authKey:[[MTDatacenterAuthKey alloc] initWithAuthKey:authInfo.authKey authKeyId:authInfo.authKeyId validUntilTimestamp:authInfo.validUntilTimestamp notBound:false]] startWithNextStrict:^(NSNumber* isRemoved) { [[MTContext contextQueue] dispatchOnQueue:^{ __strong MTContext *strongSelf = weakSelf; if (strongSelf == nil) { @@ -1506,7 +1510,7 @@ - (void)checkIfLoggedOut:(NSInteger)datacenterId { } } }]; - }]; + } file:__FILE_NAME__ line:__LINE__]; } }]; } diff --git a/submodules/MtProtoKit/Sources/MTDNS.m b/submodules/MtProtoKit/Sources/MTDNS.m index 0b99d81516a..3448e7f239f 100644 --- a/submodules/MtProtoKit/Sources/MTDNS.m +++ b/submodules/MtProtoKit/Sources/MTDNS.m @@ -125,7 +125,7 @@ - (instancetype)init { if (disposable != nil) { __weak MTDNSContext *weakSelf = self; - [disposable setDisposable:[[[self performLookup:host port:port] deliverOn:[MTDNSContext sharedQueue]] startWithNext:^(NSString *result) { + [disposable setDisposable:[[[self performLookup:host port:port] deliverOn:[MTDNSContext sharedQueue]] startWithNextStrict:^(NSString *result) { __strong MTDNSContext *strongSelf = weakSelf; if (strongSelf == nil) { return; @@ -134,7 +134,7 @@ - (instancetype)init { [strongSelf->_contexts[key] complete:result]; [strongSelf->_contexts removeObjectForKey:key]; } - }]]; + } file:__FILE_NAME__ line:__LINE__]]; } __weak MTDNSContext *weakSelf = self; diff --git a/submodules/MtProtoKit/Sources/MTDisposable.m b/submodules/MtProtoKit/Sources/MTDisposable.m index cb673e0b685..949f1d4e88d 100644 --- a/submodules/MtProtoKit/Sources/MTDisposable.m +++ b/submodules/MtProtoKit/Sources/MTDisposable.m @@ -1,13 +1,11 @@ #import -#import -#import -#import +#import #import -@interface MTBlockDisposable () -{ - void *_block; +@interface MTBlockDisposable () { + void (^_action)(); + pthread_mutex_t _lock; } @end @@ -19,47 +17,35 @@ - (instancetype)initWithBlock:(void (^)())block self = [super init]; if (self != nil) { - _block = (__bridge_retained void *)[block copy]; + _action = [block copy]; + pthread_mutex_init(&_lock, nil); } return self; } -- (void)dealloc -{ - void *block = _block; - if (block != NULL) - { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (OSAtomicCompareAndSwapPtr(block, 0, &_block)) - { - if (block != nil) - { - __unused __strong id strongBlock = (__bridge_transfer id)block; - strongBlock = nil; - } - } -#pragma clang diagnostic pop +- (void)dealloc { + void (^freeAction)() = nil; + pthread_mutex_lock(&_lock); + freeAction = _action; + _action = nil; + pthread_mutex_unlock(&_lock); + + if (freeAction) { } + + pthread_mutex_destroy(&_lock); } -- (void)dispose -{ - void *block = _block; - if (block != NULL) - { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (OSAtomicCompareAndSwapPtr(block, 0, &_block)) - { - if (block != nil) - { - __strong id strongBlock = (__bridge_transfer id)block; - ((dispatch_block_t)strongBlock)(); - strongBlock = nil; - } - } -#pragma clang diagnostic pop +- (void)dispose { + void (^disposeAction)() = nil; + + pthread_mutex_lock(&_lock); + disposeAction = _action; + _action = nil; + pthread_mutex_unlock(&_lock); + + if (disposeAction) { + disposeAction(); } } @@ -67,7 +53,7 @@ - (void)dispose @interface MTMetaDisposable () { - os_unfair_lock _lock; + pthread_mutex_t _lock; bool _disposed; id _disposable; } @@ -76,128 +62,139 @@ @interface MTMetaDisposable () @implementation MTMetaDisposable -- (void)setDisposable:(id)disposable -{ +- (instancetype)init { + self = [super init]; + if (self != nil) { + pthread_mutex_init(&_lock, nil); + } + return self; +} + +- (void)dealloc { + id freeDisposable = nil; + pthread_mutex_lock(&_lock); + if (_disposable) { + freeDisposable = _disposable; + _disposable = nil; + } + pthread_mutex_unlock(&_lock); + + if (freeDisposable) { + } + + pthread_mutex_destroy(&_lock); +} + +- (void)setDisposable:(id)disposable { id previousDisposable = nil; - bool dispose = false; + bool disposeImmediately = false; - os_unfair_lock_lock(&_lock); - dispose = _disposed; - if (!dispose) - { + pthread_mutex_lock(&_lock); + disposeImmediately = _disposed; + if (!disposeImmediately) { previousDisposable = _disposable; _disposable = disposable; } - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); - if (previousDisposable != nil) + if (previousDisposable) { [previousDisposable dispose]; + } - if (dispose) + if (disposeImmediately) { [disposable dispose]; + } } -- (void)dispose -{ +- (void)dispose { id disposable = nil; - os_unfair_lock_lock(&_lock); - if (!_disposed) - { - disposable = _disposable; + pthread_mutex_lock(&_lock); + if (!_disposed) { _disposed = true; + disposable = _disposable; + _disposable = nil; } - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); - if (disposable != nil) + if (disposable) { [disposable dispose]; + } } @end @interface MTDisposableSet () { - os_unfair_lock _lock; + pthread_mutex_t _lock; bool _disposed; - id _singleDisposable; - NSArray *_multipleDisposables; + NSMutableArray> *_disposables; } @end @implementation MTDisposableSet -- (void)add:(id)disposable -{ - if (disposable == nil) - return; +- (instancetype)init { + self = [super init]; + if (self != nil) { + pthread_mutex_init(&_lock, nil); + _disposables = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dealloc { + NSArray> *disposables = nil; + pthread_mutex_lock(&_lock); + disposables = _disposables; + _disposables = nil; + pthread_mutex_unlock(&_lock); - bool dispose = false; + if (disposables) { + } + pthread_mutex_destroy(&_lock); +} + +- (void)add:(id)disposable { + bool disposeImmediately = false; - os_unfair_lock_lock(&_lock); - dispose = _disposed; - if (!dispose) - { - if (_multipleDisposables != nil) - { - NSMutableArray *multipleDisposables = [[NSMutableArray alloc] initWithArray:_multipleDisposables]; - [multipleDisposables addObject:disposable]; - _multipleDisposables = multipleDisposables; - } - else if (_singleDisposable != nil) - { - NSMutableArray *multipleDisposables = [[NSMutableArray alloc] initWithObjects:_singleDisposable, disposable, nil]; - _multipleDisposables = multipleDisposables; - _singleDisposable = nil; - } - else - { - _singleDisposable = disposable; - } + pthread_mutex_lock(&_lock); + if (_disposed) { + disposeImmediately = true; + } else { + [_disposables addObject:disposable]; } - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); - if (dispose) + if (disposeImmediately) { [disposable dispose]; + } } - (void)remove:(id)disposable { - os_unfair_lock_lock(&_lock); - if (_multipleDisposables != nil) - { - NSMutableArray *multipleDisposables = [[NSMutableArray alloc] initWithArray:_multipleDisposables]; - [multipleDisposables removeObject:disposable]; - _multipleDisposables = multipleDisposables; - } - else if (_singleDisposable == disposable) - { - _singleDisposable = nil; + pthread_mutex_lock(&_lock); + for (NSInteger i = 0; i < _disposables.count; i++) { + if (_disposables[i] == disposable) { + [_disposables removeObjectAtIndex:i]; + break; + } } - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); } -- (void)dispose -{ - id singleDisposable = nil; - NSArray *multipleDisposables = nil; - - os_unfair_lock_lock(&_lock); - if (!_disposed) - { +- (void)dispose { + NSArray> *disposables = nil; + pthread_mutex_lock(&_lock); + if (!_disposed) { _disposed = true; - singleDisposable = _singleDisposable; - multipleDisposables = _multipleDisposables; - _singleDisposable = nil; - _multipleDisposables = nil; + disposables = _disposables; + _disposables = nil; } - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); - if (singleDisposable != nil) - [singleDisposable dispose]; - if (multipleDisposables != nil) - { - for (id disposable in multipleDisposables) - { + if (disposables) { + for (id disposable in disposables) { [disposable dispose]; } } diff --git a/submodules/MtProtoKit/Sources/MTNetworkAvailability.m b/submodules/MtProtoKit/Sources/MTNetworkAvailability.m index 500cc281cba..47f45844de3 100644 --- a/submodules/MtProtoKit/Sources/MTNetworkAvailability.m +++ b/submodules/MtProtoKit/Sources/MTNetworkAvailability.m @@ -98,13 +98,13 @@ - (void)dealloc MTTimer *timer = _timer; _timer = nil; - [[MTNetworkAvailability networkAvailabilityQueue] dispatchOnQueue:^ - { - [timer invalidate]; - - SCNetworkReachabilitySetCallback(reachability, NULL, NULL); - SCNetworkReachabilitySetDispatchQueue(reachability, NULL); - }]; + [[MTNetworkAvailability networkAvailabilityQueue] dispatchOnQueue:^{ + [timer invalidate]; + + SCNetworkReachabilitySetCallback(reachability, NULL, NULL); + SCNetworkReachabilitySetDispatchQueue(reachability, NULL); + CFRelease(reachability); + }]; } + (MTQueue *)networkAvailabilityQueue diff --git a/submodules/MtProtoKit/Sources/MTProto.m b/submodules/MtProtoKit/Sources/MTProto.m index 725a95e6963..965ee029d09 100644 --- a/submodules/MtProtoKit/Sources/MTProto.m +++ b/submodules/MtProtoKit/Sources/MTProto.m @@ -838,7 +838,10 @@ - (void)transportConnectionProblemsStatusChanged:(MTTransport *)transport scheme __weak MTProto *weakSelf = self; MTSignal *checkSignal = [[MTConnectionProbing probeProxyWithContext:_context datacenterId:_datacenterId settings:transport.proxySettings] delay:5.0 onQueue:[MTQueue concurrentDefaultQueue]]; checkSignal = [[checkSignal then:[[MTSignal complete] delay:20.0 onQueue:[MTQueue concurrentDefaultQueue]]] restart]; - [_probingDisposable setDisposable:[checkSignal startWithNext:^(NSNumber *next) { + if (_probingDisposable == nil) { + _probingDisposable = [[MTMetaDisposable alloc] init]; + } + [_probingDisposable setDisposable:[checkSignal startWithNextStrict:^(NSNumber *next) { [[MTProto managerQueue] dispatchOnQueue:^{ __strong MTProto *strongSelf = weakSelf; if (strongSelf == nil) { @@ -849,7 +852,7 @@ - (void)transportConnectionProblemsStatusChanged:(MTTransport *)transport scheme [strongSelf _updateConnectionIssuesStatus:[strongSelf->_probingStatus boolValue]]; } }]; - }]]; + } file:__FILE_NAME__ line:__LINE__]]; } } }]; diff --git a/submodules/MtProtoKit/Sources/MTSignal.m b/submodules/MtProtoKit/Sources/MTSignal.m index f1c06c919be..f1065bce715 100644 --- a/submodules/MtProtoKit/Sources/MTSignal.m +++ b/submodules/MtProtoKit/Sources/MTSignal.m @@ -1,6 +1,6 @@ #import -#import +#import #import #import #import @@ -8,28 +8,97 @@ @interface MTSubscriberDisposable : NSObject { - MTSubscriber *_subscriber; + __weak MTSubscriber *_subscriber; id _disposable; + pthread_mutex_t _lock; } @end @implementation MTSubscriberDisposable -- (instancetype)initWithSubscriber:(MTSubscriber *)subscriber disposable:(id)disposable -{ +- (instancetype)initWithSubscriber:(MTSubscriber *)subscriber disposable:(id)disposable { self = [super init]; - if (self != nil) - { + if (self != nil) { _subscriber = subscriber; _disposable = disposable; + pthread_mutex_init(&_lock, nil); } return self; } -- (void)dispose -{ - [_subscriber _markTerminatedWithoutDisposal]; +- (void)dealloc { + pthread_mutex_destroy(&_lock); +} + +- (void)dispose { + MTSubscriber *subscriber = nil; + id disposeItem = nil; + pthread_mutex_lock(&_lock); + disposeItem = _disposable; + _disposable = nil; + subscriber = _subscriber; + _subscriber = nil; + pthread_mutex_unlock(&_lock); + + [disposeItem dispose]; + [subscriber _markTerminatedWithoutDisposal]; +} + +@end + +@interface MTStrictDisposable : NSObject { + id _disposable; + const char *_file; + int _line; + +#if DEBUG + pthread_mutex_t _lock; + bool _isDisposed; +#endif +} + +- (instancetype)initWithDisposable:(id)disposable file:(const char *)file line:(int)line; +- (void)dispose; + +@end + +@implementation MTStrictDisposable + +- (instancetype)initWithDisposable:(id)disposable file:(const char *)file line:(int)line { + self = [super init]; + if (self != nil) { + _disposable = disposable; + _file = file; + _line = line; + +#if DEBUG + pthread_mutex_init(&_lock, nil); +#endif + } + return self; +} + +- (void)dealloc { +#if DEBUG + pthread_mutex_lock(&_lock); + if (!_isDisposed) { + NSLog(@"Leaked disposable from %s:%d", _file, _line); + assert(false); + } + pthread_mutex_unlock(&_lock); + + pthread_mutex_destroy(&_lock); +#endif +} + +- (void)dispose { +#if DEBUG + pthread_mutex_lock(&_lock); + _isDisposed = true; + pthread_mutex_unlock(&_lock); +#endif + [_disposable dispose]; } @@ -55,7 +124,7 @@ - (instancetype)initWithValue:(id)value { @interface MTSignalQueueState : NSObject { - os_unfair_lock _lock; + pthread_mutex_t _lock; bool _executingSignal; bool _terminated; @@ -76,6 +145,8 @@ - (instancetype)initWithSubscriber:(MTSubscriber *)subscriber queueMode:(bool)qu self = [super init]; if (self != nil) { + pthread_mutex_init(&_lock, nil); + _subscriber = subscriber; _currentDisposable = [[MTMetaDisposable alloc] init]; _queuedSignals = queueMode ? [[NSMutableArray alloc] init] : nil; @@ -84,6 +155,10 @@ - (instancetype)initWithSubscriber:(MTSubscriber *)subscriber queueMode:(bool)qu return self; } +- (void)dealloc { + pthread_mutex_destroy(&_lock); +} + - (void)beginWithDisposable:(id)disposable { _disposable = disposable; @@ -92,7 +167,7 @@ - (void)beginWithDisposable:(id)disposable - (void)enqueueSignal:(MTSignal *)signal { bool startSignal = false; - os_unfair_lock_lock(&_lock); + pthread_mutex_lock(&_lock); if (_queueMode && _executingSignal) { [_queuedSignals addObject:signal]; @@ -102,7 +177,7 @@ - (void)enqueueSignal:(MTSignal *)signal _executingSignal = true; startSignal = true; } - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); if (startSignal) { @@ -130,7 +205,7 @@ - (void)headCompleted MTSignal *nextSignal = nil; bool terminated = false; - os_unfair_lock_lock(&_lock); + pthread_mutex_lock(&_lock); _executingSignal = false; if (_queueMode) @@ -146,7 +221,7 @@ - (void)headCompleted } else terminated = _terminated; - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); if (terminated) [_subscriber putCompletion]; @@ -174,10 +249,10 @@ - (void)headCompleted - (void)beginCompletion { bool executingSignal = false; - os_unfair_lock_lock(&_lock); + pthread_mutex_lock(&_lock); executingSignal = _executingSignal; _terminated = true; - os_unfair_lock_unlock(&_lock); + pthread_mutex_unlock(&_lock); if (!executingSignal) [_subscriber putCompletion]; @@ -235,6 +310,14 @@ - (instancetype)initWithGenerator:(id (^)(MTSubscriber *))generato return [[MTSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable]; } +- (id)startWithNextStrict:(void (^)(id next))next error:(void (^)(id error))error completed:(void (^)())completed file:(const char *)file line:(int)line +{ + MTSubscriber *subscriber = [[MTSubscriber alloc] initWithNext:next error:error completed:completed]; + id disposable = _generator(subscriber); + [subscriber _assignDisposable:disposable]; + return [[MTStrictDisposable alloc] initWithDisposable:[[MTSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable] file:file line:line]; +} + - (id)startWithNext:(void (^)(id next))next { MTSubscriber *subscriber = [[MTSubscriber alloc] initWithNext:next error:nil completed:nil]; @@ -243,6 +326,14 @@ - (instancetype)initWithGenerator:(id (^)(MTSubscriber *))generato return [[MTSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable]; } +- (id)startWithNextStrict:(void (^)(id next))next file:(const char *)file line:(int)line +{ + MTSubscriber *subscriber = [[MTSubscriber alloc] initWithNext:next error:nil completed:nil]; + id disposable = _generator(subscriber); + [subscriber _assignDisposable:disposable]; + return [[MTStrictDisposable alloc] initWithDisposable:[[MTSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable] file:file line:line]; +} + - (id)startWithNext:(void (^)(id next))next completed:(void (^)())completed { MTSubscriber *subscriber = [[MTSubscriber alloc] initWithNext:next error:nil completed:completed]; @@ -251,6 +342,14 @@ - (instancetype)initWithGenerator:(id (^)(MTSubscriber *))generato return [[MTSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable]; } +- (id)startWithNextStrict:(void (^)(id next))next completed:(void (^)())completed file:(const char *)file line:(int)line +{ + MTSubscriber *subscriber = [[MTSubscriber alloc] initWithNext:next error:nil completed:completed]; + id disposable = _generator(subscriber); + [subscriber _assignDisposable:disposable]; + return [[MTStrictDisposable alloc] initWithDisposable:[[MTSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable] file:file line:line]; +} + + (MTSignal *)single:(id)next { return [[MTSignal alloc] initWithGenerator:^id (MTSubscriber *subscriber) @@ -324,11 +423,11 @@ - (MTSignal *)delay:(NSTimeInterval)seconds onQueue:(MTQueue *)queue { return [[MTSignal alloc] initWithGenerator:^id (MTSubscriber *subscriber) { - MTMetaDisposable *disposable = [[MTMetaDisposable alloc] init]; + MTMetaDisposable *startDisposable = [[MTMetaDisposable alloc] init]; + MTMetaDisposable *timerDisposable = [[MTMetaDisposable alloc] init]; - MTTimer *timer = [[MTTimer alloc] initWithTimeout:seconds repeat:false completion:^ - { - [disposable setDisposable:[self startWithNext:^(id next) + MTTimer *timer = [[MTTimer alloc] initWithTimeout:seconds repeat:false completion:^{ + [startDisposable setDisposable:[self startWithNext:^(id next) { [subscriber putNext:next]; } error:^(id error) @@ -342,12 +441,15 @@ - (MTSignal *)delay:(NSTimeInterval)seconds onQueue:(MTQueue *)queue [timer start]; - [disposable setDisposable:[[MTBlockDisposable alloc] initWithBlock:^ + [timerDisposable setDisposable:[[MTBlockDisposable alloc] initWithBlock:^ { [timer invalidate]; }]]; - return disposable; + return [[MTBlockDisposable alloc] initWithBlock:^{ + [startDisposable dispose]; + [timerDisposable dispose]; + }]; }]; } @@ -355,11 +457,11 @@ - (MTSignal *)timeout:(NSTimeInterval)seconds onQueue:(MTQueue *)queue orSignal: { return [[MTSignal alloc] initWithGenerator:^id (MTSubscriber *subscriber) { - MTMetaDisposable *disposable = [[MTMetaDisposable alloc] init]; - - MTTimer *timer = [[MTTimer alloc] initWithTimeout:seconds repeat:false completion:^ - { - [disposable setDisposable:[signal startWithNext:^(id next) + MTMetaDisposable *startDisposable = [[MTMetaDisposable alloc] init]; + MTMetaDisposable *timerDisposable = [[MTMetaDisposable alloc] init]; + + MTTimer *timer = [[MTTimer alloc] initWithTimeout:seconds repeat:false completion:^{ + [startDisposable setDisposable:[signal startWithNext:^(id next) { [subscriber putNext:next]; } error:^(id error) @@ -372,7 +474,7 @@ - (MTSignal *)timeout:(NSTimeInterval)seconds onQueue:(MTQueue *)queue orSignal: } queue:queue.nativeQueue]; [timer start]; - [disposable setDisposable:[self startWithNext:^(id next) + [timerDisposable setDisposable:[self startWithNext:^(id next) { [timer invalidate]; [subscriber putNext:next]; @@ -386,7 +488,10 @@ - (MTSignal *)timeout:(NSTimeInterval)seconds onQueue:(MTQueue *)queue orSignal: [subscriber putCompletion]; }]]; - return disposable; + return [[MTBlockDisposable alloc] initWithBlock:^{ + [startDisposable dispose]; + [timerDisposable dispose]; + }]; }]; } diff --git a/submodules/MtProtoKit/Sources/MTSubscriber.m b/submodules/MtProtoKit/Sources/MTSubscriber.m index da600346c9f..db324ed7d07 100644 --- a/submodules/MtProtoKit/Sources/MTSubscriber.m +++ b/submodules/MtProtoKit/Sources/MTSubscriber.m @@ -68,6 +68,7 @@ - (void)_markTerminatedWithoutDisposal { os_unfair_lock_lock(&_lock); MTSubscriberBlocks *blocks = nil; + id disposable = _disposable; if (!_terminated) { blocks = _blocks; @@ -80,6 +81,8 @@ - (void)_markTerminatedWithoutDisposal if (blocks) { blocks = nil; } + if (disposable) { + } } - (void)putNext:(id)next @@ -100,6 +103,7 @@ - (void)putNext:(id)next - (void)putError:(id)error { bool shouldDispose = false; + id disposable = nil; MTSubscriberBlocks *blocks = nil; os_unfair_lock_lock(&_lock); @@ -111,19 +115,23 @@ - (void)putError:(id)error shouldDispose = true; _terminated = true; } + disposable = _disposable; + _disposable = nil; os_unfair_lock_unlock(&_lock); if (blocks && blocks->_error) { blocks->_error(error); } - if (shouldDispose) - [self->_disposable dispose]; + if (shouldDispose) { + [disposable dispose]; + } } - (void)putCompletion { bool shouldDispose = false; + id disposable = nil; MTSubscriberBlocks *blocks = nil; os_unfair_lock_lock(&_lock); @@ -135,18 +143,30 @@ - (void)putCompletion shouldDispose = true; _terminated = true; } + disposable = _disposable; + _disposable = nil; os_unfair_lock_unlock(&_lock); if (blocks && blocks->_completed) blocks->_completed(); - if (shouldDispose) - [self->_disposable dispose]; + if (shouldDispose) { + [disposable dispose]; + } } - (void)dispose { - [self->_disposable dispose]; + id disposable = nil; + + os_unfair_lock_lock(&_lock); + disposable = _disposable; + _disposable = nil; + os_unfair_lock_unlock(&_lock); + + if (disposable) { + [disposable dispose]; + } } @end diff --git a/submodules/MtProtoKit/Sources/MTTcpConnection.m b/submodules/MtProtoKit/Sources/MTTcpConnection.m index dd2755e68d1..e8cf2398818 100644 --- a/submodules/MtProtoKit/Sources/MTTcpConnection.m +++ b/submodules/MtProtoKit/Sources/MTTcpConnection.m @@ -947,7 +947,7 @@ - (void)start } __weak MTTcpConnection *weakSelf = self; - [_resolveDisposable setDisposable:[resolveSignal startWithNext:^(MTTcpConnectionData *connectionData) { + [_resolveDisposable setDisposable:[resolveSignal startWithNextStrict:^(MTTcpConnectionData *connectionData) { [[MTTcpConnection tcpQueue] dispatchOnQueue:^{ __strong MTTcpConnection *strongSelf = weakSelf; if (strongSelf == nil || connectionData == nil) { @@ -1111,7 +1111,7 @@ - (void)start [strongSelf->_socket readDataToLength:sizeof(struct socks5_ident_resp) withTimeout:-1 tag:MTTcpSocksLogin]; } }]; - }]]; + } file:__FILE_NAME__ line:__LINE__]]; } }]; } diff --git a/submodules/MurMurHash32/Package.swift b/submodules/MurMurHash32/Package.swift index a6841471df5..3ae1b30397a 100644 --- a/submodules/MurMurHash32/Package.swift +++ b/submodules/MurMurHash32/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "MurMurHash32", - platforms: [.macOS(.v10_12)], + 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/NetworkLogging/Package.swift b/submodules/NetworkLogging/Package.swift index a3e22204885..793c8dc7cb9 100644 --- a/submodules/NetworkLogging/Package.swift +++ b/submodules/NetworkLogging/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "NetworkLogging", - platforms: [.macOS(.v10_12)], + 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/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index 1953ba14d49..5d0aab44338 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -540,7 +540,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co let data = try Data(contentsOf: url) if data.count > settings.maxSize { - presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil)) + presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil, customUndoText: nil)) souceUrl.stopAccessingSecurityScopedResource() EngineTempBox.shared.dispose(tempFile) @@ -593,7 +593,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co if duration > Double(settings.maxDuration) { souceUrl.stopAccessingSecurityScopedResource() - presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil)) + presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil, customUndoText: nil)) } else { Logger.shared.log("NotificationSoundSelection", "Uploading sound") diff --git a/submodules/OpenSSLEncryptionProvider/Package.swift b/submodules/OpenSSLEncryptionProvider/Package.swift index df2556d5d5b..1fd16683193 100644 --- a/submodules/OpenSSLEncryptionProvider/Package.swift +++ b/submodules/OpenSSLEncryptionProvider/Package.swift @@ -7,7 +7,7 @@ import PackageDescription let package = Package( name: "OpenSSLEncryption", platforms: [ - .macOS(.v10_12) + .macOS(.v10_13) ], products: [ .library( diff --git a/submodules/OpusBinding/Package.swift b/submodules/OpusBinding/Package.swift index 7ab91a7e990..12028015a3f 100644 --- a/submodules/OpusBinding/Package.swift +++ b/submodules/OpusBinding/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "OpusBinding", - platforms: [.macOS(.v10_12)], + 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/OpusBinding/Sources/opusenc/opusenc.m b/submodules/OpusBinding/Sources/opusenc/opusenc.m index b8d7ef69fc6..c6bcfc79972 100644 --- a/submodules/OpusBinding/Sources/opusenc/opusenc.m +++ b/submodules/OpusBinding/Sources/opusenc/opusenc.m @@ -132,6 +132,10 @@ - (instancetype)init return self; } +- (void)dealloc { + [self cleanup]; +} + - (void)cleanup { if (_encoder != NULL) diff --git a/submodules/PassportUI/Sources/LegacySecureIdAttachmentMenu.swift b/submodules/PassportUI/Sources/LegacySecureIdAttachmentMenu.swift index 3873b935590..a6ec3524dbb 100644 --- a/submodules/PassportUI/Sources/LegacySecureIdAttachmentMenu.swift +++ b/submodules/PassportUI/Sources/LegacySecureIdAttachmentMenu.swift @@ -160,7 +160,11 @@ private func processedLegacySecureIdAttachmentItems(postbox: Postbox, signal: SS let randomId = Int64.random(in: Int64.min ... Int64.max) let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpeg" let scaledSize = image.size.aspectFitted(CGSize(width: 2048.0, height: 2048.0)) - if let scaledImage = TGScaleImageToPixelSize(image, scaledSize), let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.84) { + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + if let scaledImage = TGScaleImageToPixelSize(image, scaledSize), let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.84, tempFilePath: tempFile.path) { let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) return [resource] diff --git a/submodules/Pasteboard/Sources/Pasteboard.swift b/submodules/Pasteboard/Sources/Pasteboard.swift index b4343090bf5..93c62c6ac77 100644 --- a/submodules/Pasteboard/Sources/Pasteboard.swift +++ b/submodules/Pasteboard/Sources/Pasteboard.swift @@ -17,6 +17,11 @@ private func rtfStringWithAppliedEntities(_ text: String, entities: [MessageText } }) test.removeAttribute(ChatTextInputAttributes.customEmoji, range: NSRange(location: 0, length: test.length)) + + test.enumerateAttribute(ChatTextInputAttributes.quote, in: NSRange(location: 0, length: sourceString.length), using: { value, range, _ in + if value != nil { + } + }) if let data = try? test.data(from: NSRange(location: 0, length: test.length), documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.rtf]) { if var rtf = String(data: data, encoding: .windowsCP1252) { @@ -33,6 +38,18 @@ private func rtfStringWithAppliedEntities(_ text: String, entities: [MessageText } } +struct AppSpecificPasteboardString: Codable { + var text: String + var entities: [MessageTextEntity] +} + +private func appSpecificStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> Data { + guard let data = try? JSONEncoder().encode(AppSpecificPasteboardString(text: text, entities: entities)) else { + return Data() + } + return data +} + private func chatInputStateString(attributedString: NSAttributedString) -> NSAttributedString? { let string = NSMutableAttributedString(string: attributedString.string) attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in @@ -64,6 +81,9 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { string.addAttribute(ChatTextInputAttributes.customEmoji, value: value, range: range) } + if let value = attributes[ChatTextInputAttributes.quote] as? ChatTextInputTextQuoteAttribute { + string.addAttribute(ChatTextInputAttributes.quote, value: value, range: range) + } }) return string } @@ -92,12 +112,20 @@ public func chatInputStateStringFromRTF(_ data: Data, type: NSAttributedString.D return nil } +public func chatInputStateStringFromAppSpecificString(data: Data) -> NSAttributedString? { + guard let string = try? JSONDecoder().decode(AppSpecificPasteboardString.self, from: data) else { + return nil + } + return chatInputStateStringWithAppliedEntities(string.text, entities: string.entities) +} + public func storeMessageTextInPasteboard(_ text: String, entities: [MessageTextEntity]?) { var items: [String: Any] = [:] items[kUTTypeUTF8PlainText as String] = text if let entities = entities { items[kUTTypeRTF as String] = rtfStringWithAppliedEntities(text, entities: entities) + items["private.telegramtext"] = appSpecificStringWithAppliedEntities(text, entities: entities) } UIPasteboard.general.items = [items] } diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index afc2bb66c68..2a230594ff6 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -581,7 +581,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation guard let peer = peer, let user = user else { return } - presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), nil) }) } diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 3845e8f792a..0c4164e3c57 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -990,7 +990,14 @@ public func channelPermissionsController(context: AccountContext, updatedPresent } pushControllerImpl?(controller) }, openChannelExample: { - resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "durov") |> deliverOnMainQueue).start(next: { peer in + resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "durov") + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { peer in if let peer = peer { navigateToChatControllerImpl?(peer.id) } diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index aad02227f0f..c2f4b1f6401 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -663,7 +663,7 @@ private func deviceContactInfoEntries(account: Account, engine: TelegramEngine, firstName = presentationData.strings.Message_Contact } - entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? EnginePeer.user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: firstName, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil)), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare, hiddenAvatar: hiddenAvatar)) + entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? EnginePeer.user(TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: firstName, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil)), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare, hiddenAvatar: hiddenAvatar)) if !selecting { if let _ = peer { @@ -1334,7 +1334,7 @@ public func deviceContactInfoController(context: AccountContext, updatedPresenta return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { + let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = value let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) diff --git a/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift b/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift index a4761eacc60..1c3e5ae2c21 100644 --- a/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift @@ -392,7 +392,14 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres }, updateSearchText: { text in searchText.set(text) }, openStickersBot: { - resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") |> deliverOnMainQueue).start(next: { peer in + resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { peer in if let peer = peer { dismissImpl?() navigateToChatControllerImpl?(peer.id) diff --git a/submodules/PhoneInputNode/Sources/PhoneInputNode.swift b/submodules/PhoneInputNode/Sources/PhoneInputNode.swift index 76b745cd238..b55d0fee024 100644 --- a/submodules/PhoneInputNode/Sources/PhoneInputNode.swift +++ b/submodules/PhoneInputNode/Sources/PhoneInputNode.swift @@ -93,6 +93,15 @@ public final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { } } + public var codeNumberAndFullNumber: (String, String, String) { + let full = self.number + return ( + cleanPhoneNumber(self.countryCodeField.textField.text ?? ""), + cleanPhoneNumber(self.numberField.textField.text ?? ""), + full + ) + } + public var countryCodeText: String { get { return self.countryCodeField.textField.text ?? "" diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 913b6cf1e57..6a8c165c6fb 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -804,7 +804,7 @@ public func chatMessagePhotoInternal(photoData: Signal Signal, NoError> { +private func chatMessagePhotoThumbnailDatas(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, onlyFullSize: Bool = false, forceThumbnail: Bool = false) -> Signal, NoError> { let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))) { @@ -813,17 +813,23 @@ private func chatMessagePhotoThumbnailDatas(account: Account, userLocation: Medi let signal = maybeFullSize |> take(1) - |> mapToSignal { maybeData -> Signal, NoError> in + |> mapToSignal { maybeData -> Signal, NoError> in if maybeData.complete, !forceThumbnail { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) - let thumbnail = Signal { subscriber in + let thumbnail = Signal<(Data, Bool)?, NoError> { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in - subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + if next.size == 0 { + subscriber.putNext(nil) + } else if let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) { + subscriber.putNext((data, false)) + } else { + subscriber.putNext(nil) + } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { @@ -858,7 +864,8 @@ public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResou let signal = chatMessagePhotoThumbnailDatas(account: account, userLocation: userLocation, photoReference: photoReference, onlyFullSize: onlyFullSize, forceThumbnail: blurred) return signal |> map { value in - let thumbnailData = value._0 + let thumbnailData: Data? = value._0?.0 + let thumbnailIsBlurred: Bool = value._0?.1 ?? false let fullSizeData = value._1 let fullSizeComplete = value._2 return { arguments in @@ -905,23 +912,27 @@ public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResou thumbnailImage = image } - var blurredThumbnailImage: UIImage? + var blurredThumbnailImage: CGImage? if let thumbnailImage = thumbnailImage { - let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(blurred ? CGSize(width: 50.0, height: 50.0) : CGSize(width: 150.0, height: 150.0)) - if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { - thumbnailContext.withFlippedContext { c in - c.interpolationQuality = .none - c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - - if blurred { + if thumbnailIsBlurred { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(blurred ? CGSize(width: 50.0, height: 50.0) : CGSize(width: 150.0, height: 150.0)) + if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) + + if blurred { + imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) + } + + blurredThumbnailImage = thumbnailContext.generateImage()?.cgImage } - - blurredThumbnailImage = thumbnailContext.generateImage() + } else { + blurredThumbnailImage = thumbnailImage } } @@ -933,9 +944,9 @@ public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResou } c.setBlendMode(.copy) - if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { + if let blurredThumbnailImage { c.interpolationQuality = .low - drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) + drawImage(context: c, image: blurredThumbnailImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } @@ -1915,7 +1926,7 @@ public func chatWebpageSnippetPhotoData(account: Account, userLocation: MediaRes } } -public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true, placeholderColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource, automaticFetch: automaticFetch) return signal |> map { fullSizeData in @@ -2016,7 +2027,7 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource } } -public func chatWebpageSnippetPhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatWebpageSnippetPhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, placeholderColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatWebpageSnippetPhotoData(account: account, userLocation: userLocation, photoReference: photoReference) return signal |> map { fullSizeData in @@ -2053,6 +2064,22 @@ public func chatWebpageSnippetPhoto(account: Account, userLocation: MediaResourc addCorners(context, arguments: arguments) + return context + } else if let placeholderColor { + guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) else { + return nil + } + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.corners.topLeft.radius != 0.0 { + c.addPath(UIBezierPath(roundedRect: arguments.drawingRect, cornerRadius: arguments.corners.topLeft.radius).cgPath) + c.clip() + } + c.setFillColor(placeholderColor.cgColor) + c.fill(arguments.drawingRect) + } + return context } else { return nil diff --git a/submodules/Postbox/Package.swift b/submodules/Postbox/Package.swift index dd499b6a676..36fef051009 100644 --- a/submodules/Postbox/Package.swift +++ b/submodules/Postbox/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "Postbox", - platforms: [.macOS(.v10_12)], + 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/Postbox/Sources/MediaBoxFileContextV2Impl.swift b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift index edf765de7f6..6bc400325ab 100644 --- a/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift +++ b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift @@ -158,6 +158,10 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { } } + deinit { + self.pendingFetch?.disposable.dispose() + } + func request( range: Range, isFullRange: Bool, diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 9a3c87e0733..4611261d2ea 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -683,6 +683,11 @@ final class MutableMessageHistoryView { hasChanges = true } } + if !transaction.currentUpdatedPeers.isEmpty { + if loadedState.updatePeers(postbox: postbox, updatedPeers: transaction.currentUpdatedPeers) { + hasChanges = true + } + } } if hasChanges { diff --git a/submodules/Postbox/Sources/MessageHistoryViewState.swift b/submodules/Postbox/Sources/MessageHistoryViewState.swift index cec31265a6e..b2e937db9ec 100644 --- a/submodules/Postbox/Sources/MessageHistoryViewState.swift +++ b/submodules/Postbox/Sources/MessageHistoryViewState.swift @@ -1514,6 +1514,43 @@ final class HistoryViewLoadedState { return updated } + func updatePeers(postbox: PostboxImpl, updatedPeers: [PeerId: Peer]) -> Bool { + var updated = false + for space in self.orderedEntriesBySpace.keys { + let spaceUpdated = self.orderedEntriesBySpace[space]!.mutableScan({ entry in + switch entry { + case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers): + let message = value.message + var rebuild = false + var peers = message.peers + var author = message.author + for (peerId, _) in message.peers { + if let updatedPeer = updatedPeers[peerId] { + peers[peerId] = updatedPeer + rebuild = true + } + } + if let authorValue = author, let updatedAuthor = updatedPeers[authorValue.id] { + author = updatedAuthor + rebuild = true + } + + if rebuild { + let updatedMessage = message.withUpdatedPeers(peers).withUpdatedAuthor(author) + return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) + } + case .IntermediateMessageEntry: + break + } + return nil + }) + if spaceUpdated { + updated = true + } + } + return updated + } + func add(entry: MutableMessageHistoryEntry) -> Bool { if let ignoreMessagesInTimestampRange = self.ignoreMessagesInTimestampRange { if ignoreMessagesInTimestampRange.contains(entry.index.timestamp) { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 7d10a74adc5..628ed0337c3 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3266,7 +3266,20 @@ final class PostboxImpl { let disposable = signal.start(next: { next in subscriber.putNext((next.0, next.1, nil)) }) - return ActionDisposable { [weak self] in + + final class MarkedActionDisposable: Disposable { + let disposable: ActionDisposable + + init(_ f: @escaping () -> Void) { + self.disposable = ActionDisposable(action: f) + } + + func dispose() { + self.disposable.dispose() + } + } + + return MarkedActionDisposable { [weak self] in disposable.dispose() if let strongSelf = self { strongSelf.queue.justDispatch { @@ -4293,7 +4306,7 @@ public class Postbox { ).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) } - return disposable + return disposable.strict() } } @@ -4331,7 +4344,7 @@ public class Postbox { ).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) } - return disposable + return disposable.strict() } } @@ -4371,7 +4384,7 @@ public class Postbox { ).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) } - return disposable + return disposable.strict() } } diff --git a/submodules/Postbox/Sources/PostboxLogging.swift b/submodules/Postbox/Sources/PostboxLogging.swift index 6daff29cfa7..8faf111fc05 100644 --- a/submodules/Postbox/Sources/PostboxLogging.swift +++ b/submodules/Postbox/Sources/PostboxLogging.swift @@ -1,11 +1,17 @@ import Foundation private var postboxLogger: (String) -> Void = { _ in } +private var postboxLoggerSync: () -> Void = {} -public func setPostboxLogger(_ f: @escaping (String) -> Void) { +public func setPostboxLogger(_ f: @escaping (String) -> Void, sync: @escaping () -> Void) { postboxLogger = f + postboxLoggerSync = sync } public func postboxLog(_ what: @autoclosure () -> String) { postboxLogger(what()) } + +public func postboxLogSync() { + postboxLoggerSync() +} diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 7b63cd186d4..317cdca45de 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -60,6 +60,7 @@ struct SqlitePreparedStatement { if let path = pathToRemoveOnError { postboxLog("Corrupted DB at step, dropping") try? FileManager.default.removeItem(atPath: path) + postboxLogSync() preconditionFailure() } } @@ -84,6 +85,7 @@ struct SqlitePreparedStatement { if let path = pathToRemoveOnError { postboxLog("Corrupted DB at step, dropping") try? FileManager.default.removeItem(atPath: path) + postboxLogSync() preconditionFailure() } } @@ -300,12 +302,14 @@ public final class SqliteValueBox: ValueBox { } catch { let _ = try? FileManager.default.removeItem(atPath: tempPath) postboxLog("Don't have write access to database folder") + postboxLogSync() preconditionFailure("Don't have write access to database folder") } if self.removeDatabaseOnError { let _ = try? FileManager.default.removeItem(atPath: path) } + postboxLogSync() preconditionFailure("Couldn't open database") } @@ -577,6 +581,7 @@ public final class SqliteValueBox: ValueBox { try? FileManager.default.removeItem(atPath: databasePath) } + postboxLogSync() preconditionFailure() } }) @@ -1197,6 +1202,7 @@ public final class SqliteValueBox: ValueBox { let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) if status != SQLITE_OK { let errorText = self.database.currentError() ?? "Unknown error" + postboxLogSync() preconditionFailure(errorText) } let preparedStatement = SqlitePreparedStatement(statement: statement) @@ -1211,6 +1217,7 @@ public final class SqliteValueBox: ValueBox { let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) if status != SQLITE_OK { let errorText = self.database.currentError() ?? "Unknown error" + postboxLogSync() preconditionFailure(errorText) } let preparedStatement = SqlitePreparedStatement(statement: statement) @@ -1250,6 +1257,7 @@ public final class SqliteValueBox: ValueBox { let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO NOTHING", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) if status != SQLITE_OK { let errorText = self.database.currentError() ?? "Unknown error" + postboxLogSync() preconditionFailure(errorText) } let preparedStatement = SqlitePreparedStatement(statement: statement) @@ -1264,6 +1272,7 @@ public final class SqliteValueBox: ValueBox { let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) if status != SQLITE_OK { let errorText = self.database.currentError() ?? "Unknown error" + postboxLogSync() preconditionFailure(errorText) } let preparedStatement = SqlitePreparedStatement(statement: statement) @@ -2297,6 +2306,7 @@ public final class SqliteValueBox: ValueBox { self.clearStatements() if self.isReadOnly { + postboxLogSync() preconditionFailure() } @@ -2346,6 +2356,7 @@ public final class SqliteValueBox: ValueBox { private func reencryptInPlace(database: Database, encryptionParameters: ValueBoxEncryptionParameters) -> Database { if self.isReadOnly { + postboxLogSync() preconditionFailure() } diff --git a/submodules/Postbox/Sources/ViewTracker.swift b/submodules/Postbox/Sources/ViewTracker.swift index 739f228028e..d724c7bea9a 100644 --- a/submodules/Postbox/Sources/ViewTracker.swift +++ b/submodules/Postbox/Sources/ViewTracker.swift @@ -84,6 +84,9 @@ final class ViewTracker { } func removeMessageHistoryView(index: Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>.Index) { + #if DEBUG + assert(self.messageHistoryViews.get(index) != nil) + #endif self.messageHistoryViews.remove(index) self.updateTrackedHoles() diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 34094f74618..77b604e786d 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -105,8 +105,17 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", - "//submodules/AttachmentUI:AttachmentUI", - "//submodules/Components/BalancedTextComponent" + "//submodules/Components/BalancedTextComponent", + "//submodules/ItemListPeerItem:ItemListPeerItem", + "//submodules/ItemListPeerActionItem:ItemListPeerActionItem", + "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", + "//submodules/TelegramUI/Components/ShareWithPeersScreen", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", + "//submodules/CountrySelectionUI", + "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", + "//submodules/InvisibleInkDustNode", + "//submodules/AlertUI", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift new file mode 100644 index 00000000000..817742e9416 --- /dev/null +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -0,0 +1,1142 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import AlertUI +import PresentationDataUtils +import AppBundle +import TelegramStringFormatting +import ItemListPeerItem +import ItemListDatePickerItem +import ItemListPeerActionItem +import ShareWithPeersScreen +import InAppPurchaseManager +import UndoUI +import CountrySelectionUI + +private final class CreateGiveawayControllerArguments { + let context: AccountContext + let updateState: ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void + let dismissInput: () -> Void + let openPeersSelection: () -> Void + let openChannelsSelection: () -> Void + let openCountriesSelection: () -> Void + let openPremiumIntro: () -> Void + let scrollToDate: () -> 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) { + self.context = context + self.updateState = updateState + self.dismissInput = dismissInput + self.openPeersSelection = openPeersSelection + self.openChannelsSelection = openChannelsSelection + self.openCountriesSelection = openCountriesSelection + self.openPremiumIntro = openPremiumIntro + self.scrollToDate = scrollToDate + self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions + self.removeChannel = removeChannel + } +} + +private enum CreateGiveawaySection: Int32 { + case header + case mode + case subscriptions + case channels + case users + case time + case duration +} + +private enum CreateGiveawayEntryTag: ItemListItemTag { + case date + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? CreateGiveawayEntryTag, self == other { + return true + } else { + return false + } + } +} + +private enum CreateGiveawayEntry: ItemListNodeEntry { + case header(PresentationTheme, String, String) + + case createGiveaway(PresentationTheme, String, String, Bool) + case awardUsers(PresentationTheme, String, String, Bool) + + case prepaidHeader(PresentationTheme, String) + case prepaid(PresentationTheme, String, String, PrepaidGiveaway) + + case subscriptionsHeader(PresentationTheme, String, String) + case subscriptions(PresentationTheme, Int32) + case subscriptionsInfo(PresentationTheme, String) + + case channelsHeader(PresentationTheme, String) + case channel(Int32, PresentationTheme, EnginePeer, Int32?, Bool) + case channelAdd(PresentationTheme, String) + case channelsInfo(PresentationTheme, String) + + case usersHeader(PresentationTheme, String) + case usersAll(PresentationTheme, String, String, Bool) + case usersNew(PresentationTheme, String, String, Bool) + case usersInfo(PresentationTheme, String) + + case timeHeader(PresentationTheme, String) + case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool) + case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Int32?, Int32?) + case timeInfo(PresentationTheme, String) + + case durationHeader(PresentationTheme, String) + case duration(Int32, PresentationTheme, Int32, String, String, String, String?, Bool) + case durationInfo(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .header: + return CreateGiveawaySection.header.rawValue + case .createGiveaway, .awardUsers, .prepaidHeader, .prepaid: + return CreateGiveawaySection.mode.rawValue + case .subscriptionsHeader, .subscriptions, .subscriptionsInfo: + return CreateGiveawaySection.subscriptions.rawValue + case .channelsHeader, .channel, .channelAdd, .channelsInfo: + 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 + } + } + + 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 + } + } + + static func ==(lhs: CreateGiveawayEntry, rhs: CreateGiveawayEntry) -> Bool { + switch lhs { + case let .header(lhsTheme, lhsTitle, lhsText): + if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText { + return true + } else { + return false + } + case let .createGiveaway(lhsTheme, lhsText, lhsSubtext, lhsSelected): + if case let .createGiveaway(rhsTheme, rhsText, rhsSubtext, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .awardUsers(lhsTheme, lhsText, lhsSubtext, lhsSelected): + if case let .awardUsers(rhsTheme, rhsText, rhsSubtext, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .prepaidHeader(lhsTheme, lhsText): + if case let .prepaidHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .prepaid(lhsTheme, lhsText, lhsSubtext, lhsPrepaidGiveaway): + if case let .prepaid(rhsTheme, rhsText, rhsSubtext, rhsPrepaidGiveaway) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsPrepaidGiveaway == rhsPrepaidGiveaway { + return true + } else { + return false + } + case let .subscriptionsHeader(lhsTheme, lhsText, lhsAdditionalText): + if case let .subscriptionsHeader(rhsTheme, rhsText, rhsAdditionalText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAdditionalText == rhsAdditionalText { + return true + } else { + return false + } + case let .subscriptions(lhsTheme, lhsValue): + if case let .subscriptions(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + return true + } else { + return false + } + case let .subscriptionsInfo(lhsTheme, lhsText): + if case let .subscriptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .channelsHeader(lhsTheme, lhsText): + if case let .channelsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .channel(lhsIndex, lhsTheme, lhsPeer, lhsBoosts, lhsIsRevealed): + if case let .channel(rhsIndex, rhsTheme, rhsPeer, rhsBoosts, rhsIsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsBoosts == rhsBoosts, lhsIsRevealed == rhsIsRevealed { + return true + } else { + return false + } + case let .channelAdd(lhsTheme, lhsText): + if case let .channelAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .channelsInfo(lhsTheme, lhsText): + if case let .channelsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .usersHeader(lhsTheme, lhsText): + if case let .usersHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .usersAll(lhsTheme, lhsText, lhsSubtitle, lhsSelected): + if case let .usersAll(rhsTheme, rhsText, rhsSubtitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtitle == rhsSubtitle, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .usersNew(lhsTheme, lhsText, lhsSubtitle, lhsSelected): + if case let .usersNew(rhsTheme, rhsText, rhsSubtitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtitle == rhsSubtitle, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .usersInfo(lhsTheme, lhsText): + if case let .usersInfo(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 + } else { + return false + } + case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive): + if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive { + return true + } else { + return false + } + case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsMinDate, lhsMaxDate): + if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsMinDate, rhsMaxDate) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsMinDate == rhsMinDate, lhsMaxDate == rhsMaxDate { + return true + } else { + return false + } + case let .timeInfo(lhsTheme, lhsText): + if case let .timeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } 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 + } + } + } + + static func <(lhs: CreateGiveawayEntry, rhs: CreateGiveawayEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! CreateGiveawayControllerArguments + switch self { + case let .header(_, title, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(title + text), sectionId: self.section) + case let .createGiveaway(_, title, subtitle, isSelected): + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: .blue, name: "Premium/Giveaway"), title: title, subtitle: subtitle, isSelected: isSelected, sectionId: self.section, action: { + arguments.updateState { state in + var updatedState = state + updatedState.mode = .giveaway + return updatedState + } + }) + case let .awardUsers(_, title, subtitle, isSelected): + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: .violet, name: "Media Editor/Privacy/SelectedUsers"), title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: { + var openSelection = false + arguments.updateState { state in + var updatedState = state + if state.mode == .gift || state.peers.isEmpty { + openSelection = true + } + updatedState.mode = .gift + return updatedState + } + if openSelection { + arguments.openPeersSelection() + } + }) + case let .prepaidHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .prepaid(_, title, subtitle, prepaidGiveaway): + let color: GiftOptionItem.Icon.Color + switch prepaidGiveaway.months { + case 3: + color = .green + case 6: + color = .blue + case 12: + color = .red + default: + color = .blue + } + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity * 4)", subtitle: subtitle, sectionId: self.section, action: nil) + case let .subscriptionsHeader(_, text, additionalText): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section) + case let .subscriptions(_, value): + return SubscriptionsCountItem(theme: presentationData.theme, strings: presentationData.strings, value: value, sectionId: self.section, updated: { value in + arguments.updateState { state in + var updatedState = state + updatedState.subscriptions = value + return updatedState + } + }) + case let .subscriptionsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .channelsHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .channel(_, _, peer, boosts, isRevealed): + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { + }, setPeerIdWithRevealedOptions: { lhs, rhs in + arguments.setItemIdWithRevealedOptions(lhs, rhs) + }, removePeer: { id in + arguments.removeChannel(id) + }) + case let .channelAdd(theme, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: { + arguments.openChannelsSelection() + }) + case let .channelsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .usersHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .usersAll(_, title, subtitle, isSelected): + return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: { + var openSelection = false + arguments.updateState { state in + var updatedState = state + if !updatedState.onlyNewEligible { + openSelection = true + } + updatedState.onlyNewEligible = false + return updatedState + } + if openSelection { + arguments.openCountriesSelection() + } + }) + case let .usersNew(_, title, subtitle, isSelected): + return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: { + var openSelection = false + arguments.updateState { state in + var updatedState = state + if updatedState.onlyNewEligible { + openSelection = true + } + updatedState.onlyNewEligible = true + return updatedState + } + if openSelection { + arguments.openCountriesSelection() + } + }) + case let .usersInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .timeHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .timeExpiryDate(theme, dateTimeFormat, value, active): + let text: String + if let value = value { + text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) + } else { + text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever + } + return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + arguments.dismissInput() + var focus = false + arguments.updateState { state in + var updatedState = state + updatedState.pickingTimeLimit = !state.pickingTimeLimit + if updatedState.pickingTimeLimit { + focus = true + } + return updatedState + } + if focus { + Queue.mainQueue().after(0.1) { + arguments.scrollToDate() + } + } + }) + case let .timeCustomPicker(_, dateTimeFormat, date, minDate, maxDate): + return ItemListDatePickerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, date: date, minDate: minDate, maxDate: maxDate, sectionId: self.section, style: .blocks, updated: { date in + arguments.updateState({ state in + var updatedState = state + updatedState.time = date + return updatedState + }) + }, 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: { + 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() + }) + } + } +} + +private struct PremiumGiftProduct: Equatable { + let giftOption: PremiumGiftCodeOption + let storeProduct: InAppPurchaseManager.Product + + var id: String { + return self.storeProduct.id + } + + var months: Int32 { + return self.giftOption.months + } + + var price: String { + return self.storeProduct.price + } + + var pricePerMonth: String { + return self.storeProduct.pricePerMonth(Int(self.months)) + } +} + +private func createGiveawayControllerEntries( + peerId: EnginePeer.Id, + subject: CreateGiveawaySubject, + state: CreateGiveawayControllerState, + presentationData: PresentationData, + locale: Locale, + peers: [EnginePeer.Id: EnginePeer], + products: [PremiumGiftProduct], + defaultPrice: (Int64, NSDecimalNumber), + minDate: Int32, + maxDate: Int32 +) -> [CreateGiveawayEntry] { + var entries: [CreateGiveawayEntry] = [] + + switch subject { + case .generic: + entries.append(.createGiveaway(presentationData.theme, presentationData.strings.BoostGift_CreateGiveaway, presentationData.strings.BoostGift_CreateGiveawayInfo, state.mode == .giveaway)) + + let recipientsText: String + if !state.peers.isEmpty { + var peerNamesArray: [String] = [] + let peersCount = state.peers.count + for peerId in state.peers.prefix(2) { + if let peer = peers[peerId] { + peerNamesArray.append(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)) + } + } + let peerNames = String(peerNamesArray.map { $0 }.joined(separator: ", ")) + if !peerNames.isEmpty { + recipientsText = peerNames + } else { + recipientsText = presentationData.strings.PremiumGift_LabelRecipients(Int32(peersCount)) + } + } else { + recipientsText = presentationData.strings.BoostGift_SelectRecipients + } + entries.append(.awardUsers(presentationData.theme, presentationData.strings.BoostGift_AwardSpecificUsers, recipientsText, state.mode == .gift)) + case let .prepaid(prepaidGiveaway): + entries.append(.prepaidHeader(presentationData.theme, presentationData.strings.BoostGift_PrepaidGiveawayTitle)) + entries.append(.prepaid(presentationData.theme, presentationData.strings.BoostGift_PrepaidGiveawayCount(prepaidGiveaway.quantity), presentationData.strings.BoostGift_PrepaidGiveawayMonths("\(prepaidGiveaway.months)").string, prepaidGiveaway)) + } + + if case .giveaway = state.mode { + 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)) + entries.append(.subscriptionsInfo(presentationData.theme, presentationData.strings.BoostGift_QuantityInfo)) + } + + entries.append(.channelsHeader(presentationData.theme, presentationData.strings.BoostGift_ChannelsTitle.uppercased())) + var index: Int32 = 0 + let channels = [peerId] + state.channels + for channelId in channels { + if let channel = peers[channelId] { + entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions * 4 : nil, false)) + } + index += 1 + } + entries.append(.channelAdd(presentationData.theme, presentationData.strings.BoostGift_AddChannel)) + entries.append(.channelsInfo(presentationData.theme, presentationData.strings.BoostGift_ChannelsInfo)) + + entries.append(.usersHeader(presentationData.theme, presentationData.strings.BoostGift_UsersTitle.uppercased())) + + let countriesText: String + if state.countries.count > 2 { + countriesText = presentationData.strings.BoostGift_FromCountries(Int32(state.countries.count)) + } else if !state.countries.isEmpty { + if state.countries.count == 2 { + let firstCountryCode = state.countries.first ?? "" + let secondCountryCode = state.countries.last ?? "" + let firstCountryName = locale.localizedString(forRegionCode: firstCountryCode) ?? firstCountryCode + let secondCountryName = locale.localizedString(forRegionCode: secondCountryCode) ?? secondCountryCode + countriesText = presentationData.strings.BoostGift_FromTwoCountries(firstCountryName, secondCountryName).string + } else { + let countryCode = state.countries.first ?? "" + let countryName = locale.localizedString(forRegionCode: countryCode) ?? countryCode + countriesText = presentationData.strings.BoostGift_FromOneCountry(countryName).string + } + } else { + countriesText = presentationData.strings.BoostGift_FromAllCountries + } + + entries.append(.usersAll(presentationData.theme, presentationData.strings.BoostGift_AllSubscribers, countriesText, !state.onlyNewEligible)) + 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(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, state.time, state.pickingTimeLimit)) + if state.pickingTimeLimit { + entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, state.time, minDate, maxDate)) + } + 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 + } + + 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)) + } + + return entries +} + +private struct CreateGiveawayControllerState: Equatable { + enum Mode { + case giveaway + case gift + } + + var mode: Mode + var subscriptions: Int32 + var channels: [EnginePeer.Id] + var peers: [EnginePeer.Id] + var selectedMonths: Int32? + var countries: [String] + var onlyNewEligible: Bool + var time: Int32 + var pickingTimeLimit = false + var revealedItemId: EnginePeer.Id? = nil + var updating = false +} + +public enum CreateGiveawaySubject { + case generic + case prepaid(PrepaidGiveaway) +} + +public func createGiveawayController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, subject: CreateGiveawaySubject, completion: (() -> Void)? = nil) -> ViewController { + let actionsDisposable = DisposableSet() + + let initialSubscriptions: Int32 + if case let .prepaid(prepaidGiveaway) = subject { + initialSubscriptions = prepaidGiveaway.quantity + } else { + initialSubscriptions = 5 + } + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let expiryTime = currentTime + 86400 * 3 + let minDate = currentTime + 60 * 10 + let maxDate = currentTime + context.userLimits.maxGiveawayPeriodSeconds + + let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, channels: [], peers: [], countries: [], onlyNewEligible: false, time: expiryTime) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let productsValue = Atomic<[PremiumGiftProduct]?>(value: nil) + + var buyActionImpl: (() -> Void)? + var openPeersSelectionImpl: (() -> Void)? + var openChannelsSelectionImpl: (() -> Void)? + var openCountriesSelectionImpl: (() -> Void)? + var openPremiumIntroImpl: (() -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + var scrollToDateImpl: (() -> Void)? + var dismissImpl: (() -> Void)? + var dismissInputImpl: (() -> Void)? + + let arguments = CreateGiveawayControllerArguments(context: context, updateState: { f in + updateState(f) + }, dismissInput: { + dismissInputImpl?() + }, openPeersSelection: { + openPeersSelectionImpl?() + }, openChannelsSelection: { + openChannelsSelectionImpl?() + }, openCountriesSelection: { + openCountriesSelectionImpl?() + }, openPremiumIntro: { + openPremiumIntroImpl?() + }, scrollToDate: { + scrollToDateImpl?() + }, setItemIdWithRevealedOptions: { itemId, fromItemId in + updateState { state in + var updatedState = state + if (itemId == nil && fromItemId == state.revealedItemId) || (itemId != nil && fromItemId == nil) { + updatedState.revealedItemId = itemId + } + return updatedState + } + }, + removeChannel: { id in + updateState { state in + var updatedState = state + updatedState.channels = updatedState.channels.filter { $0 != id } + return updatedState + } + }) + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + + let locale = localeWithStrings(context.sharedContext.currentPresentationData.with { $0 }.strings) + + let productsAndDefaultPrice: Signal<([PremiumGiftProduct], (Int64, NSDecimalNumber)), NoError> = combineLatest( + .single([]) |> then(context.engine.payments.premiumGiftCodeOptions(peerId: peerId)), + context.inAppPurchaseManager?.availableProducts ?? .single([]) + ) + |> map { options, products in + var gifts: [PremiumGiftProduct] = [] + for option in options { + if let product = products.first(where: { $0.id == option.storeProductId }), !product.isSubscription { + gifts.append(PremiumGiftProduct(giftOption: option, storeProduct: product)) + } + } + let defaultPrice: (Int64, NSDecimalNumber) + if let defaultProduct = products.first(where: { $0.id == "org.telegram.telegramPremium.monthly" }) { + defaultPrice = (defaultProduct.priceCurrencyAndAmount.amount, defaultProduct.priceValue) + } else { + defaultPrice = (1, NSDecimalNumber(value: 1)) + } + return (gifts, defaultPrice) + } + + let previousState = Atomic(value: nil) + let signal = combineLatest( + presentationData, + statePromise.get() + |> mapToSignal { state in + return context.engine.data.get(EngineDataMap( + Set([peerId] + state.channels + state.peers).map { + TelegramEngine.EngineData.Item.Peer.Peer(id: $0) + } + )) + |> map { peers in + return (state, peers) + } + }, + productsAndDefaultPrice + ) + |> deliverOnMainQueue + |> map { presentationData, stateAndPeersMap, productsAndDefaultPrice -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + + let (products, defaultPrice) = productsAndDefaultPrice + + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + + let (state, peersMap) = stateAndPeersMap + + let headerItem = CreateGiveawayHeaderItem(theme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.BoostGift_Title, text: presentationData.strings.BoostGift_Description, cancel: { + dismissImpl?() + }) + + let badgeCount: Int32 + switch state.mode { + case .giveaway: + badgeCount = state.subscriptions + case .gift: + badgeCount = Int32(state.peers.count) + } + let footerItem = CreateGiveawayFooterItem(theme: presentationData.theme, title: state.mode == .gift ? presentationData.strings.BoostGift_GiftPremium : presentationData.strings.BoostGift_StartGiveaway, badgeCount: badgeCount, isLoading: state.updating, action: { + buyActionImpl?() + }) + let leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}) + + let _ = productsValue.swap(products) + + let previousState = previousState.swap(state) + var animateChanges = false + if let previousState = previousState { + if previousState.pickingTimeLimit != state.pickingTimeLimit { + animateChanges = true + } + if previousState.mode != state.mode { + animateChanges = true + } + if previousState.channels.count > state.channels.count { + animateChanges = true + } + } + + var peers: [EnginePeer.Id: EnginePeer] = [:] + for (peerId, peer) in peersMap { + if let peer { + peers[peerId] = peer + } + } + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(""), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGiveawayControllerEntries(peerId: peerId, subject: subject, state: state, presentationData: presentationData, locale: locale, peers: peers, products: products, defaultPrice: defaultPrice, minDate: minDate, maxDate: maxDate), style: .blocks, emptyStateItem: nil, headerItem: headerItem, footerItem: footerItem, crossfadeState: false, animateChanges: animateChanges) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal + controller.beganInteractiveDragging = { + dismissInputImpl?() + } + presentControllerImpl = { [weak controller] c in + controller?.present(c, in: .window(.root)) + } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } + dismissInputImpl = { [weak controller] in + controller?.view.endEditing(true) + } + dismissImpl = { [weak controller] in + controller?.dismiss() + } + + buyActionImpl = { [weak controller] in + let state = stateValue.with { $0 } + guard let products = productsValue.with({ $0 }), !products.isEmpty else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var selectedProduct: PremiumGiftProduct? + let selectedMonths = state.selectedMonths ?? 12 + if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) { + selectedProduct = product + } + + guard let selectedProduct else { + let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: { + updateState { state in + var updatedState = state + updatedState.subscriptions = 25 + return updatedState + } + })], parseMarkdown: true) + presentControllerImpl?(alertController) + return + } + + let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount + + let purpose: AppStoreTransactionPurpose + 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) + quantity = selectedProduct.giftOption.storeQuantity + case .gift: + purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) + quantity = Int32(state.peers.count) + } + + updateState { state in + var updatedState = state + updatedState.updating = true + return updatedState + } + + switch subject { + case .generic: + let _ = (context.engine.payments.canPurchasePremium(purpose: purpose) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] available in + if available, let inAppPurchaseManager = context.inAppPurchaseManager { + let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: quantity, purpose: purpose) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] status in + if case .purchased = status { + if let controller, let navigationController = controller.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + var count = 0 + for c in controllers.reversed() { + if c is PeerInfoScreen { + if case .giveaway = state.mode { + count += 1 + } + break + } else { + count += 1 + } + } + controllers.removeLast(count) + navigationController.setViewControllers(controllers, animated: true) + + let title: String + let text: String + switch state.mode { + case .giveaway: + title = presentationData.strings.BoostGift_GiveawayCreated_Title + text = presentationData.strings.BoostGift_GiveawayCreated_Text + case .gift: + title = presentationData.strings.BoostGift_PremiumGifted_Title + text = presentationData.strings.BoostGift_PremiumGifted_Text + } + + let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] statsDatacenterId in + guard let statsDatacenterId else { + return + } + let statsController = context.sharedContext.makeChannelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, boosts: true, boostStatus: nil, statsDatacenterId: statsDatacenterId) + navigationController?.pushViewController(statsController) + }) + }), elevatedLayout: false, action: { _ in + return true + }) + (controllers.last as? ViewController)?.present(tooltipController, in: .current) + } + } + }, error: { error in + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText = errorText { + let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + presentControllerImpl?(alertController) + } + + updateState { state in + var updatedState = state + updatedState.updating = false + return updatedState + } + }) + } else { + updateState { state in + var updatedState = state + updatedState.updating = false + return updatedState + } + } + }) + case let .prepaid(prepaidGiveaway): + 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) + |> deliverOnMainQueue).startStandalone(completed: { + if let controller, let navigationController = controller.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + var count = 0 + for c in controllers.reversed() { + if c is PeerInfoScreen { + if case .giveaway = state.mode { + count += 1 + } + break + } else { + count += 1 + } + } + controllers.removeLast(count) + navigationController.setViewControllers(controllers, animated: true) + + let title = presentationData.strings.BoostGift_GiveawayCreated_Title + let text = presentationData.strings.BoostGift_GiveawayCreated_Text + + let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] statsDatacenterId in + guard let statsDatacenterId else { + return + } + let statsController = context.sharedContext.makeChannelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, boosts: true, boostStatus: nil, statsDatacenterId: statsDatacenterId) + navigationController?.pushViewController(statsController) + }) + }), elevatedLayout: false, action: { _ in + return true + }) + (controllers.last as? ViewController)?.present(tooltipController, in: .current) + } + }) + break + } + } + + openPeersSelectionImpl = { + let state = stateValue.with { $0 } + + let stateContext = ShareWithPeersScreen.StateContext( + context: context, + subject: .members(peerId: peerId, searchQuery: nil), + initialPeerIds: Set(state.peers) + ) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in + let controller = ShareWithPeersScreen( + context: context, + initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: state.peers), + stateContext: stateContext, + completion: { _, privacy ,_, _, _, _ in + updateState { state in + var updatedState = state + updatedState.peers = privacy.additionallyIncludePeers + if updatedState.peers.isEmpty { + updatedState.mode = .giveaway + } + return updatedState + } + } + ) + controller.dismissed = { + updateState { state in + var updatedState = state + if updatedState.peers.isEmpty { + updatedState.mode = .giveaway + } + return updatedState + } + } + pushControllerImpl?(controller) + }) + } + + openChannelsSelectionImpl = { + let state = stateValue.with { $0 } + + let stateContext = ShareWithPeersScreen.StateContext( + context: context, + subject: .channels(exclude: Set([peerId]), searchQuery: nil), + initialPeerIds: Set(state.channels.filter { $0 != peerId }) + ) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in + let controller = ShareWithPeersScreen( + context: context, + initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: state.peers), + stateContext: stateContext, + completion: { _, privacy ,_, _, _, _ in + updateState { state in + var updatedState = state + updatedState.channels = privacy.additionallyIncludePeers + return updatedState + } + } + ) + pushControllerImpl?(controller) + }) + } + + openCountriesSelectionImpl = { + let state = stateValue.with { $0 } + + let stateContext = CountriesMultiselectionScreen.StateContext( + context: context, + subject: .countries, + initialSelectedCountries: state.countries + ) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in + let controller = CountriesMultiselectionScreen( + context: context, + stateContext: stateContext, + completion: { countries in + updateState { state in + var updatedState = state + updatedState.countries = countries + return updatedState + } + } + ) + pushControllerImpl?(controller) + }) + } + + openPremiumIntroImpl = { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) + pushControllerImpl?(controller) + } + + scrollToDateImpl = { [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 == .date { + resultItemNode = listItemNode + return true + } + } + return false + }) + if let resultItemNode = resultItemNode { + controller.ensureItemNodeVisible(resultItemNode, overflow: 120.0, atTop: true) + } + }) + } + + let countriesConfiguration = context.currentCountriesConfiguration.with { $0 } + AuthorizationSequenceCountrySelectionController.setupCountryCodes(countries: countriesConfiguration.countries, codesByPrefix: countriesConfiguration.countriesByPrefix) + + return controller +} diff --git a/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift new file mode 100644 index 00000000000..19f18297f72 --- /dev/null +++ b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift @@ -0,0 +1,168 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import ButtonComponent + +final class CreateGiveawayFooterItem: ItemListControllerFooterItem { + let theme: PresentationTheme + let title: String + let badgeCount: Int32 + let isLoading: Bool + let action: () -> Void + + init(theme: PresentationTheme, title: String, badgeCount: Int32, isLoading: Bool, action: @escaping () -> Void) { + self.theme = theme + self.title = title + self.badgeCount = badgeCount + self.isLoading = isLoading + self.action = action + } + + func isEqual(to: ItemListControllerFooterItem) -> Bool { + if let item = to as? CreateGiveawayFooterItem { + return self.theme === item.theme && self.title == item.title && self.badgeCount == item.badgeCount && self.isLoading == item.isLoading + } else { + return false + } + } + + func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + if let current = current as? CreateGiveawayFooterItemNode { + current.item = self + return current + } else { + return CreateGiveawayFooterItemNode(item: self) + } + } +} + +final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode { + private let backgroundNode: NavigationBackgroundNode + private let separatorNode: ASDisplayNode + private let button = ComponentView() + + private var validLayout: ContainerViewLayout? + + private var currentIsLoading = false + var item: CreateGiveawayFooterItem { + didSet { + self.updateItem() + if let layout = self.validLayout { + let _ = self.updateLayout(layout: layout, transition: .immediate) + } + } + } + + init(item: CreateGiveawayFooterItem) { + self.item = item + + self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) + self.separatorNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + + self.updateItem() + } + + private func updateItem() { + self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate) + self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor + } + + override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.backgroundNode, alpha: alpha) + transition.updateAlpha(node: self.separatorNode, alpha: alpha) + } + + override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + let hadLayout = self.validLayout != nil + self.validLayout = layout + + let buttonInset: CGFloat = 16.0 + let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0 + let inset: CGFloat = 9.0 + + let insets = layout.insets(options: [.input]) + + var panelHeight: CGFloat = 50.0 + inset * 2.0 + let totalPanelHeight: CGFloat + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + totalPanelHeight = panelHeight + insets.bottom + } else { + 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)) + + var buttonTransition: Transition = .easeInOut(duration: 0.2) + if !hadLayout { + buttonTransition = .immediate + } + let buttonSize = self.button.update( + transition: buttonTransition, + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + color: self.item.theme.list.itemCheckColors.fillColor, + foreground: self.item.theme.list.itemCheckColors.foregroundColor, + pressedColor: self.item.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: self.item.title, + badge: Int(self.item.badgeCount), + textColor: self.item.theme.list.itemCheckColors.foregroundColor, + badgeBackground: self.item.theme.list.itemCheckColors.foregroundColor, + badgeForeground: self.item.theme.list.itemCheckColors.fillColor, + badgeStyle: .roundedRectangle, + badgeIconName: "Premium/BoostButtonIcon", + combinedAlignment: true + )) + ), + isEnabled: true, + displaysProgress: self.item.isLoading, + action: { [weak self] in + guard let self else { + return + } + self.item.action() + } + ) + ), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: 50.0) + ) + if let view = self.button.view { + if view.superview == nil { + self.view.addSubview(view) + } + transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: buttonSize)) + } + + + transition.updateFrame(node: self.backgroundNode, frame: panelFrame) + self.backgroundNode.update(size: panelFrame.size, transition: transition) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) + + return panelHeight + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.backgroundNode.frame.contains(point) { + return true + } else { + return false + } + } +} diff --git a/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift b/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift new file mode 100644 index 00000000000..daeb42a2e00 --- /dev/null +++ b/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift @@ -0,0 +1,229 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import Markdown +import ComponentFlow + +final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem { + let theme: PresentationTheme + let strings: PresentationStrings + let title: String + let text: String + let cancel: () -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, title: String, text: String, cancel: @escaping () -> Void) { + self.theme = theme + self.strings = strings + self.title = title + self.text = text + self.cancel = cancel + } + + func isEqual(to: ItemListControllerHeaderItem) -> Bool { + if let item = to as? CreateGiveawayHeaderItem { + return self.theme === item.theme && self.title == item.title && self.text == item.text + } else { + return false + } + } + + func node(current: ItemListControllerHeaderItemNode?) -> ItemListControllerHeaderItemNode { + if let current = current as? CreateGiveawayHeaderItemNode { + current.item = self + return current + } else { + return CreateGiveawayHeaderItemNode(item: self) + } + } +} + +private let titleFont = Font.semibold(20.0) +private let textFont = Font.regular(15.0) + +class CreateGiveawayHeaderItemNode: ItemListControllerHeaderItemNode { + private let backgroundNode: NavigationBackgroundNode + private let separatorNode: ASDisplayNode + private let titleNode: ImmediateTextNode + private let textNode: ImmediateTextNode + + private let cancelNode: HighlightableButtonNode + + private var hostView: ComponentHostView? + + private var component: AnyComponent? + private var validLayout: ContainerViewLayout? + + fileprivate var item: CreateGiveawayHeaderItem { + didSet { + self.updateItem() + if let layout = self.validLayout { + let _ = self.updateLayout(layout: layout, transition: .immediate) + } + } + } + + init(item: CreateGiveawayHeaderItem) { + self.item = item + + self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.navigationBar.blurredBackgroundColor) + + self.separatorNode = ASDisplayNode() + + self.titleNode = ImmediateTextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.textNode = ImmediateTextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.contentMode = .left + self.textNode.contentsScale = UIScreen.main.scale + self.textNode.maximumNumberOfLines = 0 + + self.cancelNode = HighlightableButtonNode() + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.cancelNode) + + self.cancelNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + + self.updateItem() + } + + @objc private func cancelPressed() { + self.item.cancel() + } + + override func didLoad() { + super.didLoad() + + let hostView = ComponentHostView() + self.hostView = hostView + self.view.insertSubview(hostView, at: 0) + + if let layout = self.validLayout, let component = self.component { + let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0) + + let size = hostView.update( + transition: .immediate, + component: component, + environment: {}, + containerSize: containerSize + ) + hostView.bounds = CGRect(origin: .zero, size: size) + } + } + + func updateItem() { + self.backgroundNode.updateColor(color: self.item.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.separatorNode.backgroundColor = self.item.theme.rootController.navigationBar.separatorColor + + let attributedTitle = NSAttributedString(string: self.item.title, font: titleFont, textColor: self.item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) + let attributedText = NSAttributedString(string: self.item.text, font: textFont, textColor: self.item.theme.list.freeTextColor, paragraphAlignment: .center) + + self.titleNode.attributedText = attributedTitle + self.textNode.attributedText = attributedText + + self.cancelNode.setAttributedTitle(NSAttributedString(string: self.item.strings.Common_Cancel, font: Font.regular(17.0), textColor: self.item.theme.rootController.navigationBar.accentTextColor), for: .normal) + } + + override func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { + guard let layout = self.validLayout else { + return + } + let navigationHeight: CGFloat = 56.0 + let statusBarHeight = layout.statusBarHeight ?? 0.0 + + let topInset : CGFloat = 0.0 + let titleOffsetDelta = (topInset + 160.0) - (statusBarHeight + (navigationHeight - statusBarHeight) / 2.0) + let topContentOffset = contentOffset + max(0.0, min(1.0, contentOffset / titleOffsetDelta)) * 10.0 + + let titleOffset = topContentOffset + let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta)) + let titleScale = 1.0 - fraction * 0.18 + + let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0 + transition.updateAlpha(node: self.backgroundNode, alpha: topPanelAlpha) + transition.updateAlpha(node: self.separatorNode, alpha: topPanelAlpha) + + let starPosition = CGPoint( + x: layout.size.width / 2.0, + y: -contentOffset + 80.0 + ) + if let view = self.hostView { + transition.updatePosition(layer: view.layer, position: starPosition) + } + + let titlePosition = CGPoint( + x: layout.size.width / 2.0, + y: max(topInset + 170.0 - titleOffset, statusBarHeight + (navigationHeight - statusBarHeight) / 2.0) + ) + + transition.updatePosition(node: self.titleNode, position: titlePosition) + transition.updateTransformScale(node: self.titleNode, scale: titleScale) + + let textPosition = CGPoint( + x: layout.size.width / 2.0, + y: -contentOffset + 212.0 + ) + transition.updatePosition(node: self.textNode, position: textPosition) + } + + override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + let leftInset: CGFloat = 24.0 + + let navigationBarHeight: CGFloat = 56.0 + let constrainedSize = CGSize(width: layout.size.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude) + let titleSize = self.titleNode.updateLayout(constrainedSize) + let textSize = self.textNode.updateLayout(constrainedSize) + + let cancelSize = self.cancelNode.measure(constrainedSize) + transition.updateFrame(node: self.cancelNode, frame: CGRect(origin: CGPoint(x: 16.0 + layout.safeInsets.left, y: floorToScreenPixels((navigationBarHeight - cancelSize.height) / 2.0)), size: cancelSize)) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + self.backgroundNode.update(size: CGSize(width: layout.size.width, height: navigationBarHeight), transition: transition) + + let component = AnyComponent(PremiumStarComponent(isIntro: true, isVisible: true, hasIdleAnimations: true)) + let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0) + + if let hostView = self.hostView { + let size = hostView.update( + transition: .immediate, + component: component, + environment: {}, + containerSize: containerSize + ) + hostView.bounds = CGRect(origin: .zero, size: size) + } + + self.titleNode.bounds = CGRect(origin: .zero, size: titleSize) + self.textNode.bounds = CGRect(origin: .zero, size: textSize) + + let contentHeight = titleSize.height + textSize.height + 128.0 + + self.component = component + self.validLayout = layout + + return contentHeight + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if let hostView = self.hostView, hostView.frame.contains(point) { + return true + } else { + return super.point(inside: point, with: event) + } + } +} diff --git a/submodules/PremiumUI/Sources/GiftOptionItem.swift b/submodules/PremiumUI/Sources/GiftOptionItem.swift new file mode 100644 index 00000000000..5fefc71ad66 --- /dev/null +++ b/submodules/PremiumUI/Sources/GiftOptionItem.swift @@ -0,0 +1,662 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import AccountContext +import AvatarNode + +public final class GiftOptionItem: ListViewItem, ItemListItem { + public enum Icon: Equatable { + public enum Color { + case blue + case green + case red + case violet + } + + case peer(EnginePeer) + case image(color: Color, name: String) + } + + public enum Font { + case regular + case bold + } + + public enum SubtitleFont { + case regular + case small + } + + public enum Label { + case generic(String) + case semitransparent(String) + case boosts(Int32) + + var string: String { + switch self { + case let .generic(value), let .semitransparent(value): + return value + case let .boosts(value): + return "\(value)" + } + } + } + + let presentationData: ItemListPresentationData + let context: AccountContext + let icon: Icon? + let title: String + let titleFont: Font + let titleBadge: String? + let subtitle: String? + let subtitleFont: SubtitleFont + let subtitleActive: Bool + let label: Label? + let badge: String? + let isSelected: Bool? + public let sectionId: ItemListSectionId + let action: (() -> Void)? + + public init(presentationData: ItemListPresentationData, context: AccountContext, icon: Icon? = nil, title: String, titleFont: Font = .regular, titleBadge: String? = nil, subtitle: String?, subtitleFont: SubtitleFont = .regular, subtitleActive: Bool = false, label: Label? = nil, badge: String? = nil, isSelected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?) { + self.presentationData = presentationData + self.icon = icon + self.context = context + self.title = title + self.titleFont = titleFont + self.titleBadge = titleBadge + self.subtitle = subtitle + self.subtitleFont = subtitleFont + self.subtitleActive = subtitleActive + self.label = label + self.badge = badge + self.isSelected = isSelected + self.sectionId = sectionId + self.action = action + } + + 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 = GiftOptionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(false) }) + }) + } + } + } + + 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? GiftOptionItemNode { + let makeLayout = nodeValue.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animated) + }) + } + } + } + } + } + + public var selectable: Bool { + return self.action != nil + } + + public func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } +} + +class GiftOptionItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let containerNode: ASDisplayNode + override var controlsContainer: ASDisplayNode { + return self.containerNode + } + + fileprivate var iconNode: ASImageNode? + fileprivate var avatarNode: AvatarNode? + private let titleNode: TextNode + private let titleBadge = ComponentView() + private let statusNode: TextNode + private var statusArrowNode: ASImageNode? + + private var labelBackgroundNode: ASImageNode? + private let labelNode: TextNode + private var labelIconNode: ASImageNode? + private let badgeTextNode: TextNode + private var badgeBackgroundNode: ASImageNode? + + private var layoutParams: (GiftOptionItem, ListViewItemLayoutParams, ItemListNeighbors)? + + private var selectableControlNode: ItemListSelectableControlNode? + + private let activateArea: AccessibilityAreaNode + + private let fetchDisposable = MetaDisposable() + + override var canBeSelected: Bool { + return true + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.containerNode = ASDisplayNode() + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = TextNode() + self.statusNode.isUserInteractionEnabled = false + self.statusNode.contentMode = .left + self.statusNode.contentsScale = UIScreen.main.scale + + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + self.labelNode.contentMode = .left + self.labelNode.contentsScale = UIScreen.main.scale + + self.badgeTextNode = TextNode() + self.badgeTextNode.isUserInteractionEnabled = false + self.badgeTextNode.contentMode = .left + self.badgeTextNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + + self.containerNode.addSubnode(self.titleNode) + self.containerNode.addSubnode(self.statusNode) + self.containerNode.addSubnode(self.labelNode) + self.addSubnode(self.activateArea) + } + + override func tapped() { + guard let item = self.layoutParams?.0 else { + return + } + item.action?() + } + + func asyncLayout() -> (_ item: GiftOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + let makeBadgeLayout = TextNode.asyncLayout(self.badgeTextNode) + let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode) + + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors in + let titleFont: UIFont + switch item.titleFont { + case .regular: + titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + case .bold: + titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + } + + let statusFont: UIFont + switch item.subtitleFont { + case .regular: + statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + case .small: + statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) + } + + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let rightInset: CGFloat = params.rightInset + + let titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let statusAttributedString = NSAttributedString(string: item.subtitle ?? "", font: statusFont, textColor: item.subtitleActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) + let badgeAttributedString = NSAttributedString(string: item.badge ?? "", font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + + let labelColor: UIColor + let labelFont: UIFont + if let label = item.label, case .boosts = label { + labelColor = item.presentationData.theme.list.itemAccentColor + labelFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + } else if let label = item.label, case .semitransparent = label { + labelColor = item.presentationData.theme.list.itemAccentColor + labelFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + } else { + labelColor = item.presentationData.theme.list.itemSecondaryTextColor + labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + } + + let labelAttributedString = NSAttributedString(string: item.label?.string ?? "", font: labelFont, textColor: labelColor) + + let leftInset: CGFloat = 14.0 + params.leftInset + + var avatarInset: CGFloat = 0.0 + if let _ = item.icon { + avatarInset += 48.0 + } + + let verticalInset: CGFloat = 10.0 + var titleSpacing: CGFloat = 2.0 + if case .bold = item.titleFont { + titleSpacing = 0.0 + } + + let insets = itemListNeighborsGroupedInsets(neighbors, params) + let separatorHeight = UIScreenPixel + + var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? + var editingOffset: CGFloat = 0.0 + + if let isSelected = item.isSelected { + let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, false) + selectableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0 + } + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: .greatestFiniteMagnitude))) + + let textConstrainedWidth = params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - avatarInset + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: .greatestFiniteMagnitude))) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (badgeLayout, badgeApply) = makeBadgeLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height) + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] animated in + if let strongSelf = self { + strongSelf.layoutParams = (item, params, neighbors) + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = titleAttributedString.string + strongSelf.activateArea.accessibilityValue = statusAttributedString.string + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + let iconUpdated = currentItem?.icon != item.icon + + let iconSize = CGSize(width: 40.0, height: 40.0) + if let icon = item.icon { + let iconFrame = CGRect(origin: CGPoint(x: leftInset - 3.0 + editingOffset, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize) + + switch icon { + case let .peer(peer): + if let iconNode = strongSelf.iconNode { + strongSelf.iconNode = nil + iconNode.removeFromSupernode() + } + + let avatarNode: AvatarNode + if let current = strongSelf.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))) + strongSelf.addSubnode(avatarNode) + + strongSelf.avatarNode = avatarNode + } + avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer) + avatarNode.frame = iconFrame + case let .image(color, name): + if let avatarNode = strongSelf.avatarNode { + strongSelf.avatarNode = nil + avatarNode.removeFromSupernode() + } + + let iconNode: ASImageNode + if let current = strongSelf.iconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + strongSelf.addSubnode(iconNode) + + strongSelf.iconNode = iconNode + } + + let colors: [UIColor] + switch color { + case .blue: + colors = [UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x71d4fc)] + case .green: + colors = [UIColor(rgb: 0x54cb68), UIColor(rgb: 0xa0de7e)] + case .red: + colors = [UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)] + case .violet: + colors = [UIColor(rgb: 0xd569ec), UIColor(rgb: 0xe0a2f3)] + } + if iconNode.image == nil || iconUpdated { + iconNode.image = generateAvatarImage(size: iconSize, icon: generateTintedImage(image: UIImage(bundleImageName: name), color: .white), iconScale: 1.0, cornerRadius: 20.0, color: .blue, customColors: colors) + } + iconNode.frame = iconFrame + } + } else { + if let avatarNode = strongSelf.avatarNode { + strongSelf.avatarNode = nil + avatarNode.removeFromSupernode() + } + if let iconNode = strongSelf.iconNode { + strongSelf.iconNode = nil + iconNode.removeFromSupernode() + } + } + + if let selectableControlSizeAndApply = selectableControlSizeAndApply { + let selectableControlSize = CGSize(width: selectableControlSizeAndApply.0, height: layout.contentSize.height) + let selectableControlFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: selectableControlSize) + if strongSelf.selectableControlNode == nil { + let selectableControlNode = selectableControlSizeAndApply.1(selectableControlSize, false) + strongSelf.selectableControlNode = selectableControlNode + strongSelf.addSubnode(selectableControlNode) + selectableControlNode.frame = selectableControlFrame + transition.animatePosition(node: selectableControlNode, from: CGPoint(x: -selectableControlFrame.size.width / 2.0, y: selectableControlFrame.midY)) + selectableControlNode.alpha = 0.0 + transition.updateAlpha(node: selectableControlNode, alpha: 1.0) + } else if let selectableControlNode = strongSelf.selectableControlNode { + transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame) + let _ = selectableControlSizeAndApply.1(selectableControlSize, true) + } + } else if let selectableControlNode = strongSelf.selectableControlNode { + var selectableControlFrame = selectableControlNode.frame + selectableControlFrame.origin.x = -selectableControlFrame.size.width + strongSelf.selectableControlNode = nil + transition.updateAlpha(node: selectableControlNode, alpha: 0.0) + transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame, completion: { [weak selectableControlNode] _ in + selectableControlNode?.removeFromSupernode() + }) + } + + let _ = titleApply() + let _ = statusApply() + let _ = labelApply() + let _ = badgeApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.addSubnode(strongSelf.maskNode) + } + + 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 = leftInset + editingOffset + avatarInset + 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.presentationData.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.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) + transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) + + let titleVerticalOriginY: CGFloat + if statusLayout.size.height > 0.0 { + titleVerticalOriginY = verticalInset + } else { + titleVerticalOriginY = floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + } + let titleFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: titleVerticalOriginY), size: titleLayout.size) + transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) + + var badgeOffset: CGFloat = 0.0 + if badgeLayout.size.width > 0.0 { + let badgeFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + 2.0, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: badgeLayout.size) + let badgeBackgroundFrame = badgeFrame.insetBy(dx: -3.0, dy: -2.0) + + let badgeBackgroundNode: ASImageNode + if let current = strongSelf.badgeBackgroundNode { + badgeBackgroundNode = current + } else { + badgeBackgroundNode = ASImageNode() + badgeBackgroundNode.displaysAsynchronously = false + badgeBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: item.presentationData.theme.list.itemCheckColors.fillColor) + strongSelf.badgeBackgroundNode = badgeBackgroundNode + + strongSelf.containerNode.addSubnode(badgeBackgroundNode) + strongSelf.containerNode.addSubnode(strongSelf.badgeTextNode) + } + + transition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame) + transition.updateFrame(node: strongSelf.badgeTextNode, frame: badgeFrame) + + badgeOffset = badgeLayout.size.width + 10.0 + } + + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + badgeOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) + + if let label = item.label, case .boosts = label { + let backgroundNode: ASImageNode + let iconNode: ASImageNode + if let currentBackground = strongSelf.labelBackgroundNode, let currentIcon = strongSelf.labelIconNode { + backgroundNode = currentBackground + iconNode = currentIcon + } else { + backgroundNode = ASImageNode() + backgroundNode.displaysAsynchronously = false + backgroundNode.image = generateStretchableFilledCircleImage(radius: 13.0, color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.1)) + strongSelf.containerNode.insertSubnode(backgroundNode, at: 1) + + iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostChannel"), color: item.presentationData.theme.list.itemAccentColor) + strongSelf.containerNode.addSubnode(iconNode) + + strongSelf.labelBackgroundNode = backgroundNode + strongSelf.labelIconNode = iconNode + } + + if let icon = iconNode.image { + let labelFrame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 21.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + let iconFrame = CGRect(origin: CGPoint(x: labelFrame.minX - icon.size.width - 2.0, y: labelFrame.minY - 1.0), size: icon.size) + let totalFrame = CGRect(x: iconFrame.minX - 7.0, y: labelFrame.minY - 4.0, width: iconFrame.width + labelFrame.width + 18.0, height: 26.0) + transition.updateFrame(node: backgroundNode, frame: totalFrame) + transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame) + transition.updateFrame(node: iconNode, frame: iconFrame) + } + } else if let label = item.label, case .semitransparent = label { + let backgroundNode: ASImageNode + if let currentBackground = strongSelf.labelBackgroundNode { + backgroundNode = currentBackground + } else { + backgroundNode = ASImageNode() + backgroundNode.displaysAsynchronously = false + backgroundNode.image = generateStretchableFilledCircleImage(radius: 13.0, color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.1)) + strongSelf.containerNode.insertSubnode(backgroundNode, at: 1) + + strongSelf.labelBackgroundNode = backgroundNode + } + + let labelFrame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 19.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + let totalFrame = CGRect(x: labelFrame.minX - 7.0, y: labelFrame.minY - 5.0, width: labelFrame.width + 14.0, height: 26.0) + transition.updateFrame(node: backgroundNode, frame: totalFrame) + transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame) + } else { + transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 18.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)) + } + + if item.subtitleActive { + let statusArrowNode: ASImageNode + if let current = strongSelf.statusArrowNode { + statusArrowNode = current + } else { + statusArrowNode = ASImageNode() + statusArrowNode.displaysAsynchronously = false + statusArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) + strongSelf.statusArrowNode = statusArrowNode + strongSelf.containerNode.addSubnode(statusArrowNode) + } + if let arrowSize = statusArrowNode.image?.size { + transition.updateFrame(node: statusArrowNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + statusLayout.size.width + 4.0, y: strongSelf.titleNode.frame.maxY + titleSpacing + 4.0), size: arrowSize)) + } + } else if let statusArrowNode = strongSelf.statusArrowNode { + strongSelf.statusArrowNode = nil + statusArrowNode.removeFromSupernode() + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + if let badge = item.titleBadge { + let badgeSize = strongSelf.titleBadge.update( + transition: .immediate, + component: AnyComponent( + BoostIconComponent(hasIcon: true, text: badge) + ), + environment: {}, + containerSize: CGSize(width: params.width, height: 100.0) + ) + if let view = strongSelf.titleBadge.view { + if view.superview == nil { + strongSelf.view.addSubview(view) + } + + let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - badgeSize.height / 2.0) - 1.0), size: badgeSize) + view.frame = badgeFrame + } + } else { + if let view = strongSelf.titleBadge.view { + view.removeFromSuperview() + } + } + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/PremiumUI/Sources/GiveawayInfoController.swift b/submodules/PremiumUI/Sources/GiveawayInfoController.swift new file mode 100644 index 00000000000..7254a63fe79 --- /dev/null +++ b/submodules/PremiumUI/Sources/GiveawayInfoController.swift @@ -0,0 +1,398 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramStringFormatting +import TelegramPresentationData +import Markdown +import AlertUI + +public func giveawayInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, message: EngineMessage, giveawayInfo: PremiumGiveawayInfo) -> ViewController? { + guard let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway else { + return nil + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var peerName = "" + if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] { + peerName = EnginePeer(peer).compactDisplayTitle + } + + let untilDate = stringForDate(timestamp: giveaway.untilDate, strings: presentationData.strings) + + let title: String + let text: String + var warning: String? + + var dismissImpl: (() -> Void)? + + var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + dismissImpl?() + })] + + switch giveawayInfo { + case let .ongoing(start, status): + let startDate = stringForDate(timestamp: start, strings: presentationData.strings) + + title = presentationData.strings.Chat_Giveaway_Info_Title + + 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 + } 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 + } + + 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 + } 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 + } else { + ending = presentationData.strings.Chat_Giveaway_Info_Ongoing(untilDate, randomSubscribers, peerName).string + } + } + + 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 + } else { + participation = presentationData.strings.Chat_Giveaway_Info_NotQualified(peerName, untilDate).string + } + case let .notAllowed(reason): + switch reason { + case let .joinedTooEarly(joinedOn): + let joinDate = stringForDate(timestamp: joinedOn, strings: presentationData.strings) + participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedJoinedEarly(joinDate).string + case let .channelAdmin(adminId): + let _ = adminId + participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedAdmin(peerName).string + case .disallowedCountry: + 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 + } else { + participation = presentationData.strings.Chat_Giveaway_Info_Participating(peerName).string + } + case .almostOver: + participation = presentationData.strings.Chat_Giveaway_Info_AlmostOver + } + + if !participation.isEmpty { + participation = "\n\n\(participation)" + } + + text = "\(intro)\n\n\(ending)\(participation)" + case let .finished(status, start, finish, _, activatedCount): + let startDate = stringForDate(timestamp: start, strings: presentationData.strings) + let finishDate = stringForDate(timestamp: finish, 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 + + var 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_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 { + ending = presentationData.strings.Chat_Giveaway_Info_EndedMany(finishDate, randomSubscribers, peerName).string + } else { + ending = presentationData.strings.Chat_Giveaway_Info_Ended(finishDate, randomSubscribers, peerName).string + } + } + + if activatedCount > 0 { + ending += " " + presentationData.strings.Chat_Giveaway_Info_ActivatedLinks(activatedCount) + } + + var result: String + switch status { + case .refunded: + result = "" + warning = presentationData.strings.Chat_Giveaway_Info_Refunded + actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Close, action: { + dismissImpl?() + })] + case .notWon: + result = "\n\n" + presentationData.strings.Chat_Giveaway_Info_DidntWin + case let .won(slug): + result = "\n\n" + presentationData.strings.Chat_Giveaway_Info_Won("🏆").string + let _ = slug + actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: { + dismissImpl?() + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?() + })] + } + + text = "\(intro)\n\n\(ending)\(result)" + } + + let alertController = giveawayInfoAlertController( + context: context, + updatedPresentationData: updatedPresentationData, + title: title, + text: text, + warning: warning, + actions: actions + ) + dismissImpl = { [weak alertController] in + alertController?.dismissAnimated() + } + return alertController +} + +private final class GiveawayInfoAlertContentNode: AlertContentNode { + private let title: String + private let text: String + private let warning: String? + + private let titleNode: ASTextNode + private let textNode: ASTextNode + fileprivate let warningBackgroundNode: ASImageNode + fileprivate let warningTextNode: ImmediateTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + public var theme: PresentationTheme + + public override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + public init(theme: AlertControllerTheme, ptheme: PresentationTheme, title: String, text: String, warning: String?, actions: [TextAlertAction]) { + self.theme = ptheme + self.title = title + self.text = text + self.warning = warning + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.warningBackgroundNode = ASImageNode() + self.warningBackgroundNode.displaysAsynchronously = false + + self.warningTextNode = ImmediateTextNode() + self.warningTextNode.maximumNumberOfLines = 0 + self.warningTextNode.lineSpacing = 0.1 + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.warningBackgroundNode) + self.addSubnode(self.warningTextNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + } + + public override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor) + let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor) + let attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) + + self.textNode.attributedText = attributedText + + self.warningTextNode.attributedText = NSAttributedString(string: self.warning ?? "", font: Font.semibold(13.0), textColor: theme.destructiveColor, paragraphAlignment: .center) + self.warningBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: theme.destructiveColor.withAlphaComponent(0.1)) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 6.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + if "".isEmpty { + effectiveActionLayout = .vertical + } + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = max(titleSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + var warningHeight: CGFloat = 0.0 + if let _ = self.warning { + let warningSize = self.warningTextNode.updateLayout(measureSize) + let warningFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - warningSize.width) / 2.0), y: origin.y + 20.0), size: warningSize) + transition.updateFrame(node: self.warningTextNode, frame: warningFrame) + + transition.updateFrame(node: self.warningBackgroundNode, frame: warningFrame.insetBy(dx: -8.0, dy: -8.0)) + + warningHeight += warningSize.height + 26.0 + } + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + 8.0 + actionsHeight + warningHeight + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + } + + return controller +} diff --git a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift new file mode 100644 index 00000000000..3547991b3ac --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift @@ -0,0 +1,240 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramPresentationData +import UndoUI +import PresentationDataUtils + +private struct BoostState { + let level: Int32 + let currentLevelBoosts: Int32 + let nextLevelBoosts: Int32? + let boosts: Int32 + + func displayData(peer: EnginePeer, isCurrent: Bool, canBoostAgain: Bool, myBoostCount: Int32, currentMyBoostCount: Int32, replacedBoosts: Int32? = nil) -> (subject: PremiumLimitScreen.Subject, count: Int32) { + var currentLevel = self.level + var nextLevelBoosts = self.nextLevelBoosts + var currentLevelBoosts = self.currentLevelBoosts + var boosts = self.boosts + if let replacedBoosts { + boosts = max(currentLevelBoosts, boosts - replacedBoosts) + } + + if currentMyBoostCount > 0 && self.boosts == currentLevelBoosts { + currentLevel = max(0, currentLevel - 1) + nextLevelBoosts = currentLevelBoosts + currentLevelBoosts = max(0, currentLevelBoosts - 1) + } + + return ( + .storiesChannelBoost(peer: peer, boostSubject: .stories, isCurrent: isCurrent, level: currentLevel, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: nil, myBoostCount: myBoostCount, canBoostAgain: canBoostAgain), + boosts + ) + } +} + +public func PremiumBoostScreen( + context: AccountContext, + contentContext: Any?, + peerId: EnginePeer.Id, + isCurrent: Bool, + status: ChannelBoostStatus?, + myBoostStatus: MyBoostStatus?, + replacedBoosts: (Int32, Int32)? = nil, + forceDark: Bool, + openPeer: @escaping (EnginePeer) -> Void, + presentController: @escaping (ViewController) -> Void, + pushController: @escaping (ViewController) -> Void, + dismissed: @escaping () -> Void +) { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { peer, accountPeer in + guard let peer, let accountPeer, let status else { + return + } + + let isPremium = accountPeer.isPremium + + var myBoostCount: Int32 = 0 + var currentMyBoostCount: Int32 = 0 + var availableBoosts: [MyBoostStatus.Boost] = [] + var occupiedBoosts: [MyBoostStatus.Boost] = [] + if let myBoostStatus { + for boost in myBoostStatus.boosts { + if let boostPeer = boost.peer { + if boostPeer.id == peer.id { + myBoostCount += 1 + } else { + occupiedBoosts.append(boost) + } + } else { + availableBoosts.append(boost) + } + } + } + + let initialState = BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts)) + let updatedState = Promise() + updatedState.set(.single(BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts + 1)))) + + var updateImpl: (() -> Void)? + var dismissImpl: (() -> Void)? + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 })) + let canBoostAgain = premiumConfiguration.boostsPerGiftCount > 0 + + let (initialSubject, initialCount) = initialState.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: 0, replacedBoosts: replacedBoosts?.0) + let controller = PremiumLimitScreen(context: context, subject: initialSubject, count: initialCount, forceDark: forceDark, action: { + let dismiss = false + updateImpl?() + return dismiss + }, + openPeer: { peer in + openPeer(peer) + }) + pushController(controller) + + if let (replacedBoosts, inChannels) = replacedBoosts { + currentMyBoostCount += 1 + let (subject, count) = initialState.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: 1, replacedBoosts: nil) + controller.updateSubject(subject, count: count) + + Queue.mainQueue().after(0.3) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let undoController = UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostReplaceIcon"), color: .white)!, title: nil, text: presentationData.strings.ReassignBoost_Success(presentationData.strings.ReassignBoost_Boosts(replacedBoosts), presentationData.strings.ReassignBoost_OtherChannels(inChannels)).string, round: false, undoText: nil), elevatedLayout: false, position: .bottom, action: { _ in return true }) + controller.present(undoController, in: .current) + } + } + + controller.disposed = { + dismissed() + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + updateImpl = { [weak controller] in + if let _ = status.nextLevelBoosts { + if let availableBoost = availableBoosts.first { + currentMyBoostCount += 1 + myBoostCount += 1 + + let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [availableBoost.slot]) + |> deliverOnMainQueue).startStandalone(completed: { + updatedState.set(context.engine.peers.getChannelBoostStatus(peerId: peerId) + |> map { status in + if let status { + return BoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts + 1)) + } else { + return nil + } + }) + }) + + let _ = (updatedState.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { state in + guard let state else { + return + } + let (subject, count) = state.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: currentMyBoostCount) + controller?.updateSubject(subject, count: count) + }) + + availableBoosts.removeFirst() + } else if !occupiedBoosts.isEmpty, let myBoostStatus { + if canBoostAgain { + var dismissReplaceImpl: (() -> Void)? + let replaceController = ReplaceBoostScreen(context: context, peerId: peerId, myBoostStatus: myBoostStatus, replaceBoosts: { slots in + var channelIds = Set() + for boost in myBoostStatus.boosts { + if slots.contains(boost.slot) { + if let peer = boost.peer { + channelIds.insert(peer.id) + } + } + } + + let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: slots).startStandalone(completed: { + let _ = combineLatest(queue: Queue.mainQueue(), + context.engine.peers.getChannelBoostStatus(peerId: peerId), + context.engine.peers.getMyBoostStatus() + ).startStandalone(next: { boostStatus, myBoostStatus in + dismissReplaceImpl?() + PremiumBoostScreen(context: context, contentContext: contentContext, peerId: peerId, isCurrent: isCurrent, status: boostStatus, myBoostStatus: myBoostStatus, replacedBoosts: (Int32(slots.count), Int32(channelIds.count)), forceDark: forceDark, openPeer: openPeer, presentController: presentController, pushController: pushController, dismissed: dismissed) + }) + }) + }) + dismissImpl?() + pushController(replaceController) + dismissReplaceImpl = { [weak replaceController] in + replaceController?.dismiss(animated: true) + } + } else if let boost = occupiedBoosts.first, let occupiedPeer = boost.peer { + let replaceController = replaceBoostConfirmationController(context: context, fromPeers: [occupiedPeer], toPeer: peer, commit: { + currentMyBoostCount += 1 + myBoostCount += 1 + let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [boost.slot]) + |> deliverOnMainQueue).startStandalone(completed: { [weak controller] in + let _ = (updatedState.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] state in + guard let state else { + return + } + let (subject, count) = state.displayData(peer: peer, isCurrent: isCurrent, canBoostAgain: canBoostAgain, myBoostCount: myBoostCount, currentMyBoostCount: currentMyBoostCount) + controller?.updateSubject(subject, count: count) + }) + }) + }) + presentController(replaceController) + } + } else { + if isPremium { + if !canBoostAgain { + dismissImpl?() + } else { + let controller = textAlertController( + sharedContext: context.sharedContext, + updatedPresentationData: nil, + 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: {}) + ], + parseMarkdown: true + ) + presentController(controller) + } + } else { + let controller = textAlertController( + sharedContext: context.sharedContext, + updatedPresentationData: nil, + title: presentationData.strings.ChannelBoost_Error_PremiumNeededTitle, + text: presentationData.strings.ChannelBoost_Error_PremiumNeededText, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + dismissImpl?() + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: forceDark, dismissed: nil) + pushController(controller) + }) + ], + parseMarkdown: true + ) + presentController(controller) + } + } + } else { + dismissImpl?() + } + } + dismissImpl = { [weak controller] in + controller?.dismissAnimated() + } + }) +} diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 6c45aa83347..691c500d6d9 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1262,6 +1262,12 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true } + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift new file mode 100644 index 00000000000..64d7ffed842 --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -0,0 +1,1181 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown +import BalancedTextComponent +import ConfettiEffect +import AvatarNode +import TextFormat +import TelegramStringFormatting +import UndoUI +import InvisibleInkDustNode + +private final class PremiumGiftCodeSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: PremiumGiftCodeScreen.Subject + let action: () -> Void + let cancel: (Bool) -> Void + let openPeer: (EnginePeer) -> Void + let openMessage: (EngineMessage.Id) -> Void + let copyLink: (String) -> Void + let shareLink: (String) -> Void + let displayHiddenTooltip: () -> Void + + init( + context: AccountContext, + subject: PremiumGiftCodeScreen.Subject, + action: @escaping () -> Void, + cancel: @escaping (Bool) -> Void, + openPeer: @escaping (EnginePeer) -> Void, + openMessage: @escaping (EngineMessage.Id) -> Void, + copyLink: @escaping (String) -> Void, + shareLink: @escaping (String) -> Void, + displayHiddenTooltip: @escaping () -> Void + ) { + self.context = context + self.subject = subject + self.action = action + self.cancel = cancel + self.openPeer = openPeer + self.openMessage = openMessage + self.copyLink = copyLink + self.shareLink = shareLink + self.displayHiddenTooltip = displayHiddenTooltip + } + + static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private var disposable: Disposable? + var initialized = false + + var peerMap: [EnginePeer.Id: EnginePeer] = [:] + + var cachedCloseImage: (UIImage, PresentationTheme)? + + var inProgress = false + + init(context: AccountContext, subject: PremiumGiftCodeScreen.Subject) { + self.context = context + + super.init() + + var peerIds: [EnginePeer.Id] = [] + switch subject { + case let .giftCode(giftCode): + peerIds.append(giftCode.fromPeerId) + if let toPeerId = giftCode.toPeerId { + peerIds.append(toPeerId) + } + case let .boost(channelId, boost): + peerIds.append(channelId) + if let peerId = boost.peer?.id { + peerIds.append(peerId) + } + } + + self.disposable = (context.engine.data.get( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + ) + ) |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + if let strongSelf = self { + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + peersMap[peerId] = peer + } + } + strongSelf.peerMap = peersMap + strongSelf.initialized = true + + strongSelf.updated(transition: .immediate) + } + }) + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, subject: self.subject) + } + + static var body: Body { + let closeButton = Child(Button.self) + let title = Child(MultilineTextComponent.self) + let star = Child(PremiumStarComponent.self) + let description = Child(BalancedTextComponent.self) + let linkButton = Child(Button.self) + let table = Child(TableComponent.self) + let additional = Child(BalancedTextComponent.self) + let button = Child(SolidRoundedButtonComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let dateTimeFormat = environment.dateTimeFormat + let accountContext = context.component.context + + let state = context.state + let subject = component.subject + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 + environment.safeInsets.left + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.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: { [weak component] in + component?.cancel(true) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + + let titleText: String + let descriptionText: String + let additionalText: String + let buttonText: String + + let link: String? + let date: Int32 + let fromPeer: EnginePeer? + var toPeerId: EnginePeer.Id? + let toPeer: EnginePeer? + let months: Int32 + + var gloss = false + switch subject { + case let .giftCode(giftCode): + gloss = !giftCode.isUsed + if let usedDate = giftCode.usedDate { + let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat) + titleText = strings.GiftLink_UsedTitle + descriptionText = strings.GiftLink_UsedDescription + additionalText = strings.GiftLink_UsedFooter(dateString).string + buttonText = strings.Common_OK + } else { + titleText = strings.GiftLink_Title + descriptionText = strings.GiftLink_Description + additionalText = strings.GiftLink_Footer + buttonText = strings.GiftLink_UseLink + } + link = "https://t.me/giftcode/\(giftCode.slug)" + date = giftCode.date + fromPeer = state.peerMap[giftCode.fromPeerId] + toPeerId = giftCode.toPeerId + if let toPeerId = giftCode.toPeerId { + toPeer = state.peerMap[toPeerId] + } else { + toPeer = nil + } + months = giftCode.months + case let .boost(channelId, boost): + titleText = strings.GiftLink_Title + if let peer = boost.peer, !boost.flags.contains(.isUnclaimed) { + toPeer = boost.peer + if boost.slug == nil { + descriptionText = strings.GiftLink_PersonalDescription(peer.compactDisplayTitle).string + } else { + descriptionText = strings.GiftLink_PersonalUsedDescription(peer.compactDisplayTitle).string + } + } else { + toPeer = nil + descriptionText = strings.GiftLink_UnclaimedDescription + } + if boost.flags.contains(.isUnclaimed) || boost.slug == nil { + additionalText = strings.GiftLink_NotUsedFooter + } else { + additionalText = "" + } + buttonText = strings.Common_OK + if boost.flags.contains(.isUnclaimed), let slug = boost.slug { + link = "https://t.me/giftcode/\(slug)" + } else { + link = nil + } + date = boost.date + if boost.flags.contains(.isUnclaimed) { + toPeerId = nil + } else { + toPeerId = boost.peer?.id + } + fromPeer = state.peerMap[channelId] + months = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0))) + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: titleText, + font: Font.semibold(17.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let star = star.update( + component: PremiumStarComponent(isIntro: false, isVisible: true, hasIdleAnimations: true), + availableSize: CGSize(width: context.availableSize.width, height: 200.0), + transition: .immediate + ) + + 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 description = description.update( + component: BalancedTextComponent( + text: .markdown(text: descriptionText, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let linkButton = linkButton.update( + component: Button( + content: AnyComponent( + LinkButtonContentComponent(theme: environment.theme, text: link) + ), + action: { + if let link { + component.copyLink(link) + } else { + component.displayHiddenTooltip() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: .immediate + ) + + let tableFont = Font.regular(15.0) + let tableTextColor = theme.list.itemPrimaryTextColor + let tableLinkColor = theme.list.itemAccentColor + var tableItems: [TableComponent.Item] = [] + + tableItems.append(.init( + id: "from", + title: strings.GiftLink_From, + component: AnyComponent( + Button( + content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)), + action: { + if let peer = fromPeer, peer.id != accountContext.account.peerId { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + } + ) + ) + )) + if let toPeer { + tableItems.append(.init( + id: "to", + title: strings.GiftLink_To, + component: AnyComponent( + Button( + content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), + action: { + if toPeer.id != accountContext.account.peerId { + component.openPeer(toPeer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + } + ) + ) + )) + } else if toPeerId == nil { + tableItems.append(.init( + id: "to", + title: strings.GiftLink_To, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.GiftLink_NoRecipient, font: tableFont, textColor: tableTextColor))) + ) + )) + } + let giftTitle = strings.GiftLink_TelegramPremium(months) + tableItems.append(.init( + id: "gift", + title: strings.GiftLink_Gift, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: tableFont, textColor: tableTextColor))) + ) + )) + + if case let .giftCode(giftCode) = component.subject { + let giftReason: String + if giftCode.toPeerId == nil { + giftReason = strings.GiftLink_Reason_Unclaimed + } else { + giftReason = giftCode.isGiveaway ? strings.GiftLink_Reason_Giveaway : strings.GiftLink_Reason_Gift + } + tableItems.append(.init( + id: "reason", + title: strings.GiftLink_Reason, + component: AnyComponent( + Button( + content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))), + isEnabled: true, + action: { + if let messageId = giftCode.messageId { + component.openMessage(messageId) + } + Queue.mainQueue().after(1.0) { + component.cancel(false) + } + } + ) + ) + )) + } else if case let .boost(_, boost) = component.subject { + if boost.flags.contains(.isUnclaimed) { + let giftReason = strings.GiftLink_Reason_Unclaimed + tableItems.append(.init( + id: "reason", + title: strings.GiftLink_Reason, + component: AnyComponent( + Button( + content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: boost.giveawayMessageId != nil ? tableLinkColor : tableTextColor)))), + isEnabled: true, + action: { + if let messageId = boost.giveawayMessageId { + component.openMessage(messageId) + } + Queue.mainQueue().after(1.0) { + component.cancel(false) + } + } + ) + ) + )) + } + } + tableItems.append(.init( + id: "date", + title: strings.GiftLink_Date, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + + let table = table.update( + component: TableComponent( + theme: environment.theme, + items: tableItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + + let additional = additional.update( + component: BalancedTextComponent( + text: .markdown(text: additionalText, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1, + highlightColor: linkColor.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 + if let link { + component.shareLink(link) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: gloss, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { [weak state] in + if gloss { + component.action() + if let state { + state.inProgress = true + state.updated() + } + } else { + component.cancel(true) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0)) + ) + + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0)) + ) + + var originY: CGFloat = 0.0 + originY += star.size.height - 32.0 + + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) + ) + originY += description.size.height + 21.0 + + context.add(linkButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + linkButton.size.height / 2.0)) + ) + originY += linkButton.size.height + 16.0 + + context.add(table + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) + ) + originY += table.size.height + 23.0 + + context.add(additional + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0)) + ) + originY += additional.size.height + 23.0 + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) + + return contentSize + } + } +} + +private final class PremiumGiftCodeSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: PremiumGiftCodeScreen.Subject + let action: () -> Void + let openPeer: (EnginePeer) -> Void + let openMessage: (EngineMessage.Id) -> Void + let copyLink: (String) -> Void + let shareLink: (String) -> Void + let displayHiddenTooltip: () -> Void + + init( + context: AccountContext, + subject: PremiumGiftCodeScreen.Subject, + action: @escaping () -> Void, + openPeer: @escaping (EnginePeer) -> Void, + openMessage: @escaping (EngineMessage.Id) -> Void, + copyLink: @escaping (String) -> Void, + shareLink: @escaping (String) -> Void, + displayHiddenTooltip: @escaping () -> Void + ) { + self.context = context + self.subject = subject + self.action = action + self.openPeer = openPeer + self.openMessage = openMessage + self.copyLink = copyLink + self.shareLink = shareLink + self.displayHiddenTooltip = displayHiddenTooltip + } + + static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(PremiumGiftCodeSheetContent( + context: context.component.context, + subject: context.component.subject, + action: context.component.action, + cancel: { animate in + if animate { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + }, + openPeer: context.component.openPeer, + openMessage: context.component.openMessage, + copyLink: context.component.copyLink, + shareLink: context.component.shareLink, + displayHiddenTooltip: context.component.displayHiddenTooltip + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public class PremiumGiftCodeScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case giftCode(PremiumGiftCodeInfo) + case boost(EnginePeer.Id, ChannelBoostersContext.State.Boost) + } + + private let context: AccountContext + public var disposed: () -> Void = {} + + private let hapticFeedback = HapticFeedback() + + public init( + context: AccountContext, + subject: PremiumGiftCodeScreen.Subject, + forceDark: Bool = false, + action: @escaping () -> Void, + openPeer: @escaping (EnginePeer) -> Void = { _ in }, + openMessage: @escaping (EngineMessage.Id) -> Void = { _ in }, + shareLink: @escaping (String) -> Void = { _ in } + ) { + self.context = context + + var copyLinkImpl: ((String) -> Void)? + var displayHiddenTooltipImpl: (() -> Void)? + super.init( + context: context, + component: PremiumGiftCodeSheetComponent( + context: context, + subject: subject, + action: action, + openPeer: openPeer, + openMessage: openMessage, + copyLink: { link in + copyLinkImpl?(link) + }, + shareLink: shareLink, + displayHiddenTooltip: { + displayHiddenTooltipImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + + 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: .window(.root)) + } + + displayHiddenTooltipImpl = { [weak self] in + guard let self else { + return + } + self.dismissAllTooltips() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.GiftLink_LinkHidden, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root)) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposed() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } + + 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 + }) + } +} + +private final class LinkButtonContentComponent: CombinedComponent { + let theme: PresentationTheme + let text: String? + + public init( + theme: PresentationTheme, + text: String? + ) { + self.theme = theme + self.text = text + } + + static func ==(lhs: LinkButtonContentComponent, rhs: LinkButtonContentComponent) -> 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 icon = Child(BundleIconComponent.self) + let dust = Child(DustComponent.self) + + return { context in + let component = context.component + + let sideInset: CGFloat = 38.0 + + let background = background.update( + component: RoundedRectangle(color: component.theme.list.itemInputField.backgroundColor, cornerRadius: 10.0), + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let _ = component.text { + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: (component.text ?? "").replacingOccurrences(of: "https://", with: ""), + font: Font.regular(17.0), + textColor: component.theme.list.itemPrimaryTextColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset - sideInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.theme.list.itemAccentColor), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0)) + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + } else { + let dust = dust.update( + component: DustComponent(color: component.theme.list.itemSecondaryTextColor), + availableSize: CGSize(width: context.availableSize.width * 0.8, height: context.availableSize.height * 0.54), + transition: context.transition + ) + context.add(dust + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + } + + return context.availableSize + } + } +} + +private final class TableComponent: CombinedComponent { + class Item: Equatable { + public let id: AnyHashable + public let title: String + public let component: AnyComponent + + public init(id: IdType, title: String, component: AnyComponent) { + self.id = AnyHashable(id) + self.title = title + self.component = component + } + + public static func == (lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.component != rhs.component { + return false + } + return true + } + } + + private let theme: PresentationTheme + private let items: [Item] + + public init(theme: PresentationTheme, items: [Item]) { + self.theme = theme + self.items = items + } + + public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + final class State: ComponentState { + var cachedBorderImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + public static var body: Body { + let leftColumnBackground = Child(Rectangle.self) + let verticalBorder = Child(Rectangle.self) + let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let outerBorder = Child(Image.self) + + return { context in + let verticalPadding: CGFloat = 11.0 + let horizontalPadding: CGFloat = 12.0 + let borderWidth: CGFloat = 1.0 + + let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor + let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + + var leftColumnWidth: CGFloat = 0.0 + + var updatedTitleChildren: [_UpdatedChildComponent] = [] + var updatedValueChildren: [_UpdatedChildComponent] = [] + var updatedBorderChildren: [_UpdatedChildComponent] = [] + + for item in context.component.items { + let titleChild = titleChildren[item.id].update( + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + )), + availableSize: context.availableSize, + transition: context.transition + ) + updatedTitleChildren.append(titleChild) + + if titleChild.size.width > leftColumnWidth { + leftColumnWidth = titleChild.size.width + } + } + + leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) + let rightColumnWidth = context.availableSize.width - leftColumnWidth + + var i = 0 + var rowHeights: [Int: CGFloat] = [:] + var totalHeight: CGFloat = 0.0 + + for item in context.component.items { + let titleChild = updatedTitleChildren[i] + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append(valueChild) + + let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0) + rowHeights[i] = rowHeight + totalHeight += rowHeight + + if i < context.component.items.count - 1 { + let borderChild = borderChildren[item.id].update( + component: AnyComponent(Rectangle(color: borderColor)), + availableSize: CGSize(width: context.availableSize.width, height: borderWidth), + transition: context.transition + ) + updatedBorderChildren.append(borderChild) + } + + i += 1 + } + + let leftColumnBackground = leftColumnBackground.update( + component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), + availableSize: CGSize(width: leftColumnWidth, height: totalHeight), + transition: context.transition + ) + context.add( + leftColumnBackground + .position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 2.0)) + ) + + let borderImage: UIImage + if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { + borderImage = currentImage + } else { + let borderRadius: CGFloat = 5.0 + borderImage = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.setFillColor(backgroundColor.cgColor) + context.fill(bounds) + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + + context.setBlendMode(.normal) + context.setStrokeColor(borderColor.cgColor) + context.setLineWidth(borderWidth) + context.addPath(path) + context.strokePath() + })!.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) + context.state.cachedBorderImage = (borderImage, context.component.theme) + } + + let outerBorder = outerBorder.update( + component: Image(image: borderImage), + availableSize: CGSize(width: context.availableSize.width, height: totalHeight), + transition: context.transition + ) + context.add(outerBorder + .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) + ) + + let verticalBorder = verticalBorder.update( + component: Rectangle(color: borderColor), + availableSize: CGSize(width: borderWidth, height: totalHeight), + transition: context.transition + ) + context.add( + verticalBorder + .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0)) + ) + + i = 0 + var originY: CGFloat = 0.0 + for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) { + let rowHeight = rowHeights[i] ?? 0.0 + + let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) + let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size) + + context.add(titleChild + .position(titleFrame.center) + ) + + context.add(valueChild + .position(valueFrame.center) + ) + + if i < updatedBorderChildren.count { + let borderChild = updatedBorderChildren[i] + context.add(borderChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) + ) + } + + originY += rowHeight + i += 1 + } + + return CGSize(width: context.availableSize.width, height: totalHeight) + } + } +} + +private final class PeerCellComponent: Component { + let context: AccountContext + let textColor: UIColor + let peer: EnginePeer? + + init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) { + self.context = context + self.textColor = textColor + self.peer = peer + } + + static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.textColor !== rhs.textColor { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let avatarNode: AvatarNode + private let text = ComponentView() + + private var component: PeerCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + self.avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true + ) + + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 6.0 + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) + self.avatarNode.frame = avatarFrame + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + transition.setFrame(view: view, frame: textFrame) + } + + 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) + } +} + +private final class DustComponent: Component { + let color: UIColor + + init(color: UIColor) { + self.color = color + } + + static func ==(lhs: DustComponent, rhs: DustComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView { + private let dustView = InvisibleInkDustView(textNode: nil, enableAnimations: true) + + private var component: DustComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.dustView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: DustComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let rects: [CGRect] = [CGRect(origin: .zero, size: availableSize).insetBy(dx: 5.0, dy: 5.0)] + self.dustView.update(size: availableSize, color: component.color, textColor: component.color, rects: rects, wordRects: rects) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index b83c4eb5a88..6f3d2682e14 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -19,7 +19,6 @@ import InAppPurchaseManager import ConfettiEffect import TextFormat import UniversalMediaPlayer -import AttachmentUI public enum PremiumGiftSource: Equatable { case profile @@ -670,11 +669,13 @@ private final class PremiumGiftScreenComponent: CombinedComponent { self.updateInProgress(true) self.updated(transition: .immediate) - let _ = (self.context.engine.payments.canPurchasePremium(purpose: .gift(peerId: self.peerId, currency: currency, amount: amount)) + let purpose: AppStoreTransactionPurpose = .gift(peerId: self.peerId, currency: currency, amount: amount) + 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, targetPeerId: strongSelf.peerId) + strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self, case .purchased = status { Queue.mainQueue().after(2.0) { @@ -692,7 +693,6 @@ private final class PremiumGiftScreenComponent: CombinedComponent { strongSelf.updateInProgress(false) strongSelf.updated(transition: .immediate) - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } var errorText: String? switch error { case .generic: @@ -991,7 +991,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { } } -public final class PremiumGiftScreen: ViewControllerComponentContainer, AttachmentContainable { +open class PremiumGiftScreen: ViewControllerComponentContainer { fileprivate let context: AccountContext private var didSetReady = false @@ -1004,7 +1004,9 @@ public final class PremiumGiftScreen: ViewControllerComponentContainer, Attachme public weak var containerView: UIView? public var animationColor: UIColor? - fileprivate let mainButtonStatePromise = Promise(nil) + public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + + 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) { @@ -1091,55 +1093,7 @@ public final class PremiumGiftScreen: ViewControllerComponentContainer, Attachme } } - @objc fileprivate func mainButtonPressed() { + @objc public func mainButtonPressed() { self.mainButtonActionSlot.invoke(Void()) } - - public var requestAttachmentMenuExpansion: () -> Void = {} - public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } - public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } - public var cancelPanGesture: () -> Void = { } - public var isContainerPanning: () -> Bool = { return false } - public var isContainerExpanded: () -> Bool = { return false } - - public var mediaPickerContext: AttachmentMediaPickerContext? { - return PremiumGiftContext(controller: self) - } -} - -private final class PremiumGiftContext: AttachmentMediaPickerContext { - private weak var controller: PremiumGiftScreen? - - var selectionCount: Signal { - return .single(0) - } - - var caption: Signal { - return .single(nil) - } - - public var loadingProgress: Signal { - return .single(nil) - } - - public var mainButtonState: Signal { - return self.controller?.mainButtonStatePromise.get() ?? .single(nil) - } - - init(controller: PremiumGiftScreen) { - self.controller = controller - } - - func setCaption(_ caption: NSAttributedString) { - } - - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) { - } - - func schedule() { - } - - func mainButtonAction() { - self.controller?.mainButtonPressed() - } } diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 8249cdb75ce..90db0253fa6 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -28,6 +28,7 @@ import UniversalMediaPlayer import CheckNode import AnimationCache import MultiAnimationRenderer +import TelegramNotices public enum PremiumSource: Equatable { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { @@ -1432,12 +1433,15 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var selectedProductId: String? var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = [] + var newPerks: [String] = [] + var isPremium: Bool? private var disposable: Disposable? private(set) var configuration = PremiumIntroConfiguration.defaultValue private var stickersDisposable: Disposable? + private var newPerksDisposable: Disposable? private var preloadDisposableSet = DisposableSet() var price: String? { @@ -1515,12 +1519,24 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } }) + + + self.newPerksDisposable = (ApplicationSpecificNotice.dismissedPremiumAppIconsBadge(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge in + guard let self else { + return + } + let newPerks: [String] = [] + self.newPerks = newPerks + self.updated() + }) } deinit { self.disposable?.dispose() self.preloadDisposableSet.dispose() self.stickersDisposable?.dispose() + self.newPerksDisposable?.dispose() } } @@ -1824,7 +1840,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { subtitleColor: subtitleColor, arrowColor: arrowColor, accentColor: accentColor, - badge: perk.identifier == "stories" ? strings.Premium_New : nil + badge: state.newPerks.contains(perk.identifier) ? strings.Premium_New : nil ) ) ), @@ -1854,6 +1870,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .animatedUserpics case .appIcons: demoSubject = .appIcons +// let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .animatedEmoji: demoSubject = .animatedEmoji case .emojiStatus: @@ -2087,6 +2104,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let context: AccountContext let source: PremiumSource let forceDark: Bool + let forceHasPremium: Bool let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void let push: (ViewController) -> Void @@ -2096,10 +2114,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let completion: () -> Void // MARK: Nicegram (dismiss) - init(context: AccountContext, source: PremiumSource, forceDark: 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, dismiss: @escaping () -> Void, completion: @escaping () -> Void) { self.context = context self.source = source self.forceDark = forceDark + self.forceHasPremium = forceHasPremium self.updateInProgress = updateInProgress self.present = present self.push = push @@ -2119,6 +2138,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if lhs.forceDark != rhs.forceDark { return false } + if lhs.forceHasPremium != rhs.forceHasPremium { + return false + } return true } @@ -2182,7 +2204,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } // MARK: Nicegram (dismiss) - init(context: AccountContext, source: PremiumSource, 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, dismiss: @escaping () -> Void, completion: @escaping () -> Void) { self.context = context self.updateInProgress = updateInProgress self.present = present @@ -2226,6 +2248,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent { otherPeerName = .single(nil) } + if forceHasPremium { + self.isPremium = true + } + self.disposable = combineLatest( queue: Queue.mainQueue(), availableProducts, @@ -2249,7 +2275,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } strongSelf.products = products - strongSelf.isPremium = isPremium + strongSelf.isPremium = forceHasPremium || isPremium strongSelf.otherPeerName = otherPeerName if !hadProducts { @@ -2330,11 +2356,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.updateInProgress(true) self.updated(transition: .immediate) - let _ = (self.context.engine.payments.canPurchasePremium(purpose: isUpgrade ? .upgrade : .subscription) + let purpose: AppStoreTransactionPurpose = isUpgrade ? .upgrade : .subscription + let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in if let strongSelf = self { if available { - strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, isUpgrade: isUpgrade) + strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self, case .purchased = status { strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId) @@ -2429,7 +2456,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { func makeState() -> State { // MARK: Nicegram (dismiss) - return State(context: self.context, source: self.source, 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, dismiss: self.dismiss, completion: self.completion) } static var body: Body { @@ -2879,7 +2906,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { public weak var containerView: UIView? public var animationColor: UIColor? - public init(context: AccountContext, modal: Bool = true, source: PremiumSource, forceDark: Bool = false) { + public init(context: AccountContext, modal: Bool = true, source: PremiumSource, forceDark: Bool = false, forceHasPremium: Bool = false) { self.context = context var updateInProgressImpl: ((Bool) -> Void)? @@ -2893,6 +2920,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { context: context, source: source, forceDark: forceDark, + forceHasPremium: forceHasPremium, updateInProgress: { inProgress in updateInProgressImpl?(inProgress) }, @@ -2945,8 +2973,8 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { // completionImpl = { [weak self] in - if let strongSelf = self { - strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds)) + if let self { + self.animateSuccess() } } } @@ -2960,6 +2988,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { self.wasDismissed?() } + 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/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index f77b5820810..653321026cd 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -17,6 +17,8 @@ import Markdown import BalancedTextComponent import ConfettiEffect import AvatarNode +import TextFormat +import RoundedRectWithTailPath func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in @@ -39,131 +41,6 @@ func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor }) } -private func generateBadgePath(rectSize: CGSize, tailPosition: CGFloat? = 0.5) -> UIBezierPath { - let cornerRadius: CGFloat = rectSize.height / 2.0 - let tailWidth: CGFloat = 20.0 - let tailHeight: CGFloat = 9.0 - let tailRadius: CGFloat = 4.0 - - let rect = CGRect(origin: CGPoint(x: 0.0, y: tailHeight), size: rectSize) - - guard let tailPosition else { - return UIBezierPath(cgPath: CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)) - } - - let path = UIBezierPath() - - path.move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius)) - - var leftArcEndAngle: CGFloat = .pi / 2.0 - var leftConnectionArcRadius = tailRadius - var tailLeftHalfWidth: CGFloat = tailWidth / 2.0 - var tailLeftArcStartAngle: CGFloat = -.pi / 4.0 - var tailLeftHalfRadius = tailRadius - - var rightArcStartAngle: CGFloat = -.pi / 2.0 - var rightConnectionArcRadius = tailRadius - var tailRightHalfWidth: CGFloat = tailWidth / 2.0 - var tailRightArcStartAngle: CGFloat = .pi / 4.0 - var tailRightHalfRadius = tailRadius - - if tailPosition < 0.5 { - let fraction = max(0.0, tailPosition - 0.15) / 0.35 - leftArcEndAngle *= fraction - - let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15 - leftConnectionArcRadius *= connectionFraction - - if tailPosition < 0.27 { - let fraction = tailPosition / 0.27 - tailLeftHalfWidth *= fraction - tailLeftArcStartAngle *= fraction - tailLeftHalfRadius *= fraction - } - } else if tailPosition > 0.5 { - let tailPosition = 1.0 - tailPosition - let fraction = max(0.0, tailPosition - 0.15) / 0.35 - rightArcStartAngle *= fraction - - let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15 - rightConnectionArcRadius *= connectionFraction - - if tailPosition < 0.27 { - let fraction = tailPosition / 0.27 - tailRightHalfWidth *= fraction - tailRightArcStartAngle *= fraction - tailRightHalfRadius *= fraction - } - } - path.addArc( - withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi, - endAngle: .pi + max(0.0001, leftArcEndAngle), - clockwise: true - ) - - let leftArrowStart = max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfWidth - leftConnectionArcRadius) - path.addArc( - withCenter: CGPoint(x: leftArrowStart, y: rect.minY - leftConnectionArcRadius), - radius: leftConnectionArcRadius, - startAngle: .pi / 2.0, - endAngle: .pi / 4.0, - clockwise: false - ) - - path.addLine(to: CGPoint(x: max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfRadius), y: rect.minY - tailHeight)) - - path.addArc( - withCenter: CGPoint(x: rect.minX + rectSize.width * tailPosition, y: rect.minY - tailHeight + tailRadius / 2.0), - radius: tailRadius, - startAngle: -.pi / 2.0 + tailLeftArcStartAngle, - endAngle: -.pi / 2.0 + tailRightArcStartAngle, - clockwise: true - ) - - path.addLine(to: CGPoint(x: min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfRadius), y: rect.minY - tailHeight)) - - let rightArrowStart = min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfWidth + rightConnectionArcRadius) - path.addArc( - withCenter: CGPoint(x: rightArrowStart, y: rect.minY - rightConnectionArcRadius), - radius: rightConnectionArcRadius, - startAngle: .pi - .pi / 4.0, - endAngle: .pi / 2.0, - clockwise: false - ) - - path.addArc( - withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + cornerRadius), - radius: cornerRadius, - startAngle: min(-0.0001, rightArcStartAngle), - endAngle: 0.0, - clockwise: true - ) - - path.addLine(to: CGPoint(x: rect.minX + rectSize.width, y: rect.minY + rectSize.height - cornerRadius)) - - path.addArc( - withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + rectSize.height - cornerRadius), - radius: cornerRadius, - startAngle: 0.0, - endAngle: .pi / 2.0, - clockwise: true - ) - - path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height)) - - path.addArc( - withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height - cornerRadius), - radius: cornerRadius, - startAngle: .pi / 2.0, - endAngle: .pi, - clockwise: true - ) - - return path -} - public class PremiumLimitDisplayComponent: Component { private let inactiveColor: UIColor private let activeColors: [UIColor] @@ -285,6 +162,9 @@ public class PremiumLimitDisplayComponent: Component { private let badgeLabel: BadgeLabelView private let badgeLabelMaskView = UIImageView() + private var badgeTailPosition: CGFloat = 0.0 + private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? + private let hapticFeedback = HapticFeedback() override init(frame: CGRect) { @@ -355,6 +235,10 @@ public class PremiumLimitDisplayComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.badgeShapeAnimator?.invalidate() + } + private var didPlayAppearanceAnimation = false func playAppearanceAnimation(component: PremiumLimitDisplayComponent, badgeFullSize: CGSize, from: CGFloat? = nil) { if from == nil { @@ -657,17 +541,30 @@ public class PremiumLimitDisplayComponent: Component { if badgePosition > 1.0 - 0.15 { progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0)) - progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 1.0).cgPath) + + if component.isPremiumDisabled || progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 1.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 1.0 if let _ = self.badgeView.layer.animation(forKey: "appearance1") { - } else { self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 82.0) } } else if badgePosition < 0.15 { progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0)) - progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.0).cgPath) - + + if component.isPremiumDisabled || progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.0 + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { } else { @@ -675,8 +572,15 @@ public class PremiumLimitDisplayComponent: Component { } } else { progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) - progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.5).cgPath) - + + if component.isPremiumDisabled || progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.5).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.5 + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { } else { @@ -740,6 +644,36 @@ public class PremiumLimitDisplayComponent: Component { return size } + private var badgeShapeAnimator: ConstantDisplayLinkAnimator? + private func animateBadgeTailPositionChange() { + if self.badgeShapeAnimator == nil { + self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.animateBadgeTailPositionChange() + }) + self.badgeShapeAnimator?.isPaused = true + } + + if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments { + self.badgeShapeAnimator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: value).cgPath + + if t >= 1.0 { + self.badgeShapeArguments = nil + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } else { + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } + private func setupGradientAnimations() { guard let _ = self.component else { return @@ -810,8 +744,9 @@ private final class LimitSheetContent: CombinedComponent { let dismiss: () -> Void let openPeer: (EnginePeer) -> Void let openStats: (() -> Void)? + let openGift: (() -> Void)? - init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, dismiss: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?) { + init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, dismiss: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?, openGift: (() -> Void)?) { self.context = context self.subject = subject self.count = count @@ -820,6 +755,7 @@ private final class LimitSheetContent: CombinedComponent { self.dismiss = dismiss self.openPeer = openPeer self.openStats = openStats + self.openGift = openGift } static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool { @@ -844,9 +780,10 @@ private final class LimitSheetContent: CombinedComponent { var premiumLimits: EngineConfiguration.UserLimits var isPremium = false - var boosted = false + var myBoostCount: Int32 = 0 var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? init(context: AccountContext, subject: PremiumLimitScreen.Subject) { self.context = context @@ -884,13 +821,18 @@ private final class LimitSheetContent: CombinedComponent { let closeButton = Child(Button.self) let title = Child(MultilineTextComponent.self) let text = Child(BalancedTextComponent.self) - let alternateText = Child(BalancedTextComponent.self) + let alternateText = Child(List.self) let limit = Child(PremiumLimitDisplayComponent.self) let linkButton = Child(SolidRoundedButtonComponent.self) let button = Child(SolidRoundedButtonComponent.self) let peerShortcut = Child(Button.self) let statsButton = Child(Button.self) + let orLeftLine = Child(Rectangle.self) + let orRightLine = Child(Rectangle.self) + let orText = Child(MultilineTextComponent.self) + let giftText = Child(BalancedTextComponent.self) + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component @@ -939,9 +881,13 @@ private final class LimitSheetContent: CombinedComponent { var peerShortcutChild: _UpdatedChildComponent? var useAlternateText = false + var alternateTitle = "" + var alternateBadge: String? + var titleText = strings.Premium_LimitReached var actionButtonText: String? var actionButtonHasGloss = true + var gradientedActionButton = true var buttonAnimationName: String? = "premium_x2" var buttonIconName: String? let iconName: String @@ -1159,7 +1105,7 @@ private final class LimitSheetContent: CombinedComponent { string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string } buttonAnimationName = nil - case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, boosted): + case let .storiesChannelBoost(peer, boostSubject, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, myBoostCount, canBoostAgain): if link == nil, !isCurrent, state.initialized { peerShortcutChild = peerShortcut.update( component: Button( @@ -1167,7 +1113,8 @@ private final class LimitSheetContent: CombinedComponent { PeerShortcutComponent( context: component.context, theme: environment.theme, - peer: peer + peer: peer, + badge: myBoostCount > 0 ? "\(myBoostCount)" : nil ) ), action: { @@ -1182,6 +1129,10 @@ private final class LimitSheetContent: CombinedComponent { ) } + if link != nil { + gradientedActionButton = false + } + if let _ = link, let openStats = component.openStats { let _ = openStats let statsButton = statsButton.update( @@ -1207,11 +1158,11 @@ private final class LimitSheetContent: CombinedComponent { ) } - if boosted && state.boosted != boosted { - state.boosted = boosted + if myBoostCount > 0 && state.myBoostCount != myBoostCount { + state.myBoostCount = myBoostCount boostUpdated = true } - useAlternateText = boosted + useAlternateText = myBoostCount > 0 iconName = "Premium/Boost" badgeText = "\(component.count)" @@ -1226,8 +1177,14 @@ private final class LimitSheetContent: CombinedComponent { let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1) let valueString = strings.ChannelBoost_MoreBoosts(remaining) if level == 0 { - titleText = strings.ChannelBoost_EnableStories - string = strings.ChannelBoost_EnableStoriesText(valueString).string + switch boostSubject { + case .stories: + titleText = strings.ChannelBoost_EnableStories + string = strings.ChannelBoost_EnableStoriesText(valueString).string + case .nameColors: + titleText = strings.ChannelBoost_EnableColors + string = strings.ChannelBoost_EnableColorsText(valueString).string + } } else { titleText = strings.ChannelBoost_IncreaseLimit string = strings.ChannelBoost_IncreaseLimitText(valueString, storiesString).string @@ -1252,12 +1209,14 @@ private final class LimitSheetContent: CombinedComponent { string = strings.ChannelBoost_HelpUpgradeChannelText(peer.compactDisplayTitle, boostsString, storiesString).string } actionButtonText = strings.ChannelBoost_BoostChannel + buttonIconName = "Premium/BoostChannel" } else { titleText = strings.ChannelBoost_MaxLevelReached string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level)", storiesString).string actionButtonText = strings.Common_OK + buttonIconName = nil + actionButtonHasGloss = false } - buttonIconName = "Premium/BoostChannel" } buttonAnimationName = nil defaultTitle = strings.ChannelBoost_Level("\(level)").string @@ -1266,12 +1225,20 @@ private final class LimitSheetContent: CombinedComponent { premiumTitle = "" - if boosted { + if myBoostCount > 0 { + alternateTitle = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannelText + if myBoostCount > 1 { + alternateBadge = "X\(myBoostCount)" + } let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1) - buttonIconName = nil - actionButtonText = environment.strings.Common_OK + if let _ = remaining, canBoostAgain { + actionButtonText = strings.ChannelBoost_BoostAgain + } else { + buttonIconName = nil + actionButtonText = environment.strings.Common_OK + actionButtonHasGloss = false + } if let remaining { - titleText = isCurrent ? strings.ChannelBoost_YouBoostedChannel(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannel let boostsString = strings.ChannelBoost_MoreBoosts(remaining) if level == 0 { if remaining == 0 { @@ -1288,7 +1255,6 @@ private final class LimitSheetContent: CombinedComponent { } } } else { - titleText = strings.ChannelBoost_MaxLevelReached string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level + 1)", storiesString).string } } @@ -1309,6 +1275,9 @@ private final class LimitSheetContent: CombinedComponent { if case .folders = subject, !state.isPremium { reachedMaximumLimit = false } + if case .storiesChannelBoost = component.subject { + reachedMaximumLimit = false + } let contentSize: CGSize if state.initialized { @@ -1323,31 +1292,58 @@ private final class LimitSheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 1 ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let textColor = theme.actionSheet.primaryTextColor - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in - return nil + 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) }) - var textChild: _UpdatedChildComponent? var alternateTextChild: _UpdatedChildComponent? if useAlternateText { alternateTextChild = alternateText.update( - component: BalancedTextComponent( - text: .markdown(text: string, attributes: markdownAttributes), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.1 + component: List( + [ + AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + BoostedTitleContent(text: NSAttributedString(string: alternateTitle, font: Font.semibold(15.0), textColor: textColor), badge: alternateBadge) + ) + ), + AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + BalancedTextComponent( + text: .markdown(text: string, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ) + ) + ) + ], + centerAlignment: true ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) + +// alternateTextChild = alternateText.update( +// component: BalancedTextComponent( +// text: .markdown(text: string, attributes: markdownAttributes), +// horizontalAlignment: .center, +// maximumNumberOfLines: 0, +// lineSpacing: 0.1 +// ), +// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), +// transition: .immediate +// ) } else { textChild = text.update( component: BalancedTextComponent( @@ -1370,6 +1366,7 @@ private final class LimitSheetContent: CombinedComponent { } let gradientColors: [UIColor] + var buttonGradientColors: [UIColor] if isPremiumDisabled { gradientColors = [ UIColor(rgb: 0x007afe), @@ -1383,6 +1380,14 @@ private final class LimitSheetContent: CombinedComponent { UIColor(rgb: 0xe46ace) ] } + if gradientedActionButton { + buttonGradientColors = gradientColors + } else { + buttonGradientColors = [ + UIColor(rgb: 0x007afe), + UIColor(rgb: 0x5494ff) + ] + } var limitTransition: Transition = .immediate if boostUpdated { @@ -1395,7 +1400,7 @@ private final class LimitSheetContent: CombinedComponent { title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK), theme: SolidRoundedButtonComponent.Theme( backgroundColor: .black, - backgroundColors: gradientColors, + backgroundColors: buttonGradientColors, foregroundColor: .white ), font: .bold, @@ -1421,13 +1426,13 @@ private final class LimitSheetContent: CombinedComponent { ) var buttonOffset: CGFloat = 0.0 - var textOffset: CGFloat = 228.0 + topOffset + var textOffset: CGFloat = 184.0 + topOffset - if case let .storiesChannelBoost(_, _, _, _, _, link, _) = component.subject { + if case let .storiesChannelBoost(_, _, _, _, _, _, link, _, _) = component.subject { if let link { let linkButton = linkButton.update( component: SolidRoundedButtonComponent( - title: link, + title: link.replacingOccurrences(of: "https://", with: ""), theme: SolidRoundedButtonComponent.Theme( backgroundColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), backgroundColors: [], @@ -1447,13 +1452,11 @@ private final class LimitSheetContent: CombinedComponent { transition: context.transition ) buttonOffset += 66.0 - - let linkFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil((textChild?.size ?? .zero).height / 2.0) + 24.0), size: linkButton.size) + + let linkFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + (textChild?.size ?? .zero).height + 24.0), size: linkButton.size) context.add(linkButton .position(CGPoint(x: linkFrame.midX, y: linkFrame.midY)) ) - } else { - textOffset -= 26.0 } } if isPremiumDisabled { @@ -1467,6 +1470,9 @@ private final class LimitSheetContent: CombinedComponent { var textSize: CGSize if let textChild { textSize = textChild.size + + textOffset += textSize.height / 2.0 + context.add(textChild .position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset)) .appear(Transition.Appear({ _, view, transition in @@ -1483,6 +1489,9 @@ private final class LimitSheetContent: CombinedComponent { ) } else if let alternateTextChild { textSize = alternateTextChild.size + + textOffset += textSize.height / 2.0 + context.add(alternateTextChild .position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset)) .appear(Transition.Appear({ _, view, transition in @@ -1528,17 +1537,84 @@ private final class LimitSheetContent: CombinedComponent { context.add(button .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) ) + + var additionalContentHeight: CGFloat = 0.0 + if case let .storiesChannelBoost(_, _, _, _, _, _, link, _, _) = component.subject, link != nil, let openGift = component.openGift { + let orText = orText.update( + component: MultilineTextComponent(text: .plain(NSAttributedString(string: "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: environment.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: environment.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 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, environment.theme) + } + + let giftString = environment.strings.Premium_BoostByGiftDescription + 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 + 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 + } - contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) + contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + additionalContentHeight + 5.0 + environment.safeInsets.bottom) } else { var height: CGFloat = 351.0 if isPremiumDisabled { height -= 78.0 } - if case let .storiesChannelBoost(_, isCurrent, _, _, _, link, _) = component.subject { + if case let .storiesChannelBoost(_, _, isCurrent, _, _, _, link, _, _) = component.subject { if link != nil { height += 66.0 + + if let _ = component.openGift { + height += 100.0 + } } else { if isCurrent { height -= 53.0 @@ -1566,8 +1642,9 @@ private final class LimitSheetComponent: CombinedComponent { let action: () -> Bool let openPeer: (EnginePeer) -> Void let openStats: (() -> Void)? + let openGift: (() -> Void)? - init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?) { + init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?, openGift: (() -> Void)?) { self.context = context self.subject = subject self.count = count @@ -1575,6 +1652,7 @@ private final class LimitSheetComponent: CombinedComponent { self.action = action self.openPeer = openPeer self.openStats = openStats + self.openGift = openGift } static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool { @@ -1595,6 +1673,8 @@ private final class LimitSheetComponent: CombinedComponent { let sheet = Child(SheetComponent.self) let animateOut = StoredActionSlot(Action.self) + let sheetExternalState = SheetComponent.ExternalState() + return { context in let environment = context.environment[EnvironmentType.self] @@ -1616,9 +1696,12 @@ private final class LimitSheetComponent: CombinedComponent { }) }, openPeer: context.component.openPeer, - openStats: context.component.openStats + openStats: context.component.openStats, + openGift: context.component.openGift )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + externalState: sheetExternalState, animateOut: animateOut ), environment: { @@ -1627,7 +1710,7 @@ private final class LimitSheetComponent: CombinedComponent { isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, - regularMetricsSize: nil, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { animateOut.invoke(Action { _ in @@ -1651,6 +1734,22 @@ private final class LimitSheetComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + return context.availableSize } } @@ -1670,7 +1769,12 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case storiesWeekly case storiesMonthly - case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, boosted: Bool) + + public enum BoostSubject { + case stories + case nameColors + } + case storiesChannelBoost(peer: EnginePeer, boostSubject: BoostSubject, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool) } private let context: AccountContext @@ -1680,16 +1784,19 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { private let hapticFeedback = HapticFeedback() - public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void = { _ in }, openStats: (() -> Void)? = nil) { + public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void = { _ in }, openStats: (() -> Void)? = nil, openGift: (() -> Void)? = nil) { self.context = context self.openPeer = openPeer var actionImpl: (() -> Bool)? super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: { return actionImpl?() ?? true - }, openPeer: openPeer, openStats: openStats), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default) + }, openPeer: openPeer, openStats: openStats, openGift: openGift), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default) self.navigationPresentation = .flatModal + if case .storiesChannelBoost = subject { + self.automaticallyControlPresentationContextLayout = false + } self.wasDismissed = cancel @@ -1718,14 +1825,32 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true } + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } + public func updateSubject(_ subject: Subject, count: Int32) { - let component = LimitSheetComponent(context: self.context, subject: subject, count: count, cancel: {}, action: { - return true - }, openPeer: self.openPeer, openStats: nil) + let component = LimitSheetComponent( + context: self.context, + subject: subject, + count: count, + cancel: {}, + action: { [weak self] in + return self?.action?() ?? true + }, + openPeer: self.openPeer, + openStats: nil, + openGift: nil + ) self.updateComponent(component: AnyComponent(component), transition: .easeInOut(duration: 0.2)) + self.animateSuccess() + } + + public func animateSuccess() { self.hapticFeedback.impact() - self.view.addSubview(ConfettiView(frame: self.view.bounds)) } } @@ -1734,11 +1859,13 @@ private final class PeerShortcutComponent: Component { let context: AccountContext let theme: PresentationTheme let peer: EnginePeer + let badge: String? - init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { + init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer, badge: String?) { self.context = context self.theme = theme self.peer = peer + self.badge = badge } static func ==(lhs: PeerShortcutComponent, rhs: PeerShortcutComponent) -> Bool { @@ -1751,10 +1878,14 @@ private final class PeerShortcutComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.badge != rhs.badge { + return false + } return true } final class View: UIView { + private let backgroundView = UIView() private let avatarNode: AvatarNode private let text = ComponentView() @@ -1766,9 +1897,10 @@ private final class PeerShortcutComponent: Component { super.init(frame: frame) - self.clipsToBounds = true - self.layer.cornerRadius = 16.0 + self.backgroundView.clipsToBounds = true + self.backgroundView.layer.cornerRadius = 16.0 + self.addSubview(self.backgroundView) self.addSubnode(self.avatarNode) } @@ -1780,7 +1912,7 @@ private final class PeerShortcutComponent: Component { self.component = component self.state = state - self.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3) + self.backgroundView.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3) self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0)) self.avatarNode.setPeer( @@ -1810,6 +1942,8 @@ private final class PeerShortcutComponent: Component { view.frame = textFrame } + self.backgroundView.frame = CGRect(origin: .zero, size: size) + return size } } @@ -1822,3 +1956,160 @@ private final class PeerShortcutComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class BoostIconComponent: Component { + let hasIcon: Bool + let text: String + + public init(hasIcon: Bool, text: String) { + self.hasIcon = hasIcon + self.text = text + } + + public static func ==(lhs: BoostIconComponent, rhs: BoostIconComponent) -> Bool { + if lhs.hasIcon != rhs.hasIcon { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + public final class View: UIView { + private let badgeBackground = UIView() + private let badgeIcon: UIImageView + private let badgeText = ComponentView() + + private var component: BoostIconComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.badgeIcon = UIImageView(image: UIImage(bundleImageName: "Premium/BoostButtonIcon")) + + super.init(frame: frame) + + self.badgeBackground.clipsToBounds = true + self.badgeBackground.backgroundColor = UIColor(rgb: 0x9671ff) + + self.addSubview(self.badgeBackground) + self.addSubview(self.badgeIcon) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BoostIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let textSize = self.badgeText.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.text, font: Font.with(size: 10.0, design: .round, weight: .bold), textColor: .white, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: availableSize + ) + + let spacing: CGFloat = 2.0 + var totalWidth = textSize.width + var iconSize = CGSize() + if let icon = self.badgeIcon.image, component.hasIcon { + iconSize = CGSize(width: icon.size.width * 0.9, height: icon.size.height * 0.9) + totalWidth += spacing + icon.size.width + } + + let size = CGSize(width: totalWidth + 8.0, height: 19.0) + + let iconFrame = CGRect(x: floorToScreenPixels((size.width - totalWidth) / 2.0 + 1.0), y: 4.0 + UIScreenPixel, width: iconSize.width, height: iconSize.height) + self.badgeIcon.frame = iconFrame + + let textFrame = CGRect(origin: CGPoint(x: component.hasIcon ? iconFrame.maxX + spacing : 5.0, y: 4.0), size: textSize) + + if let view = self.badgeText.view { + if view.superview == nil { + self.addSubview(view) + } + view.frame = textFrame + } + + self.badgeBackground.frame = CGRect(origin: .zero, size: size) + self.badgeBackground.layer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BoostedTitleContent: CombinedComponent { + let text: NSAttributedString + let badge: String? + + init( + text: NSAttributedString, + badge: String? + ) { + self.text = text + self.badge = badge + } + + static func ==(lhs: BoostedTitleContent, rhs: BoostedTitleContent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.badge != rhs.badge { + return false + } + return true + } + + static var body: Body { + let text = Child(MultilineTextComponent.self) + let badge = Child(BoostIconComponent.self) + + return { context in + let component = context.component + + let height: CGFloat = 24.0 + var totalWidth: CGFloat = 0.0 + let text = text.update( + component: MultilineTextComponent( + text: .plain(component.text), + horizontalAlignment: .center + ), + availableSize: CGSize(width: context.availableSize.width - 40.0, height: context.availableSize.height), + transition: .immediate + ) + totalWidth += text.size.width + context.add(text + .position(CGPoint(x: text.size.width / 2.0, y: height / 2.0)) + ) + + if let badgeText = component.badge { + let badge = badge.update( + component: BoostIconComponent(hasIcon: false, text: badgeText), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: .immediate + ) + totalWidth += badge.size.width + 4.0 + context.add(badge + .position(CGPoint(x: totalWidth - badge.size.width / 2.0, y: height / 2.0)) + ) + } + + return CGSize(width: totalWidth, height: height) + } + } +} diff --git a/submodules/TelegramUI/Sources/ReplaceBoostConfirmationController.swift b/submodules/PremiumUI/Sources/ReplaceBoostConfirmationController.swift similarity index 65% rename from submodules/TelegramUI/Sources/ReplaceBoostConfirmationController.swift rename to submodules/PremiumUI/Sources/ReplaceBoostConfirmationController.swift index a27ae1d227d..08d638c2288 100644 --- a/submodules/TelegramUI/Sources/ReplaceBoostConfirmationController.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostConfirmationController.swift @@ -1,3 +1,4 @@ + import Foundation import UIKit import SwiftSignalKit @@ -11,8 +12,82 @@ import AccountContext import AppBundle import AvatarNode import Markdown +import CheckNode + +private func generateBoostIcon(theme: PresentationTheme) -> UIImage? { + if let image = UIImage(bundleImageName: "Premium/AvatarBoost") { + let size = CGSize(width: image.size.width + 4.0, height: image.size.height + 4.0) + return generateImage(size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: image.size)) + } + + let lineWidth = 2.0 - UIScreenPixel + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel)) + }, opaque: false) + } + return nil +} -private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { +private final class PreviousBoostNode: ASDisplayNode { + let checkNode: InteractiveCheckNode + let avatarNode: AvatarNode + let labelNode: ImmediateTextNode + + var pressed: (PreviousBoostNode) -> Void = { _ in } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, peer: EnginePeer, badge: String?) { + self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) + self.checkNode.setSelected(false, animated: false) + + self.labelNode = ImmediateTextNode() + self.labelNode.maximumNumberOfLines = 4 + self.labelNode.isUserInteractionEnabled = true + self.labelNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.semibold(13.0), textColor: theme.primaryColor) + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) + + super.init() + + self.addSubnode(self.checkNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.labelNode) + + self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer) + + self.checkNode.valueChanged = { [weak self] value in + if let self { + if value { + self.pressed(self) + } + } + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let checkSize = CGSize(width: 22.0, height: 22.0) + let condensedSize = CGSize(width: size.width - 76.0, height: size.height) + let avatarSize = CGSize(width: 30.0, height: 30.0) + + let labelSize = self.labelNode.updateLayout(condensedSize) + transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: 12.0, y: -2.0), size: checkSize)) + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 46.0, y: -8.0), size: avatarSize)) + + transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: 84.0, y: 0.0), size: labelSize)) + + return CGSize(width: size.width, height: checkSize.height) + } + + func setChecked(_ checked: Bool) { + self.checkNode.setSelected(checked, animated: false) + } +} + +private final class ReplaceBoostConfirmationAlertContentNode: AlertContentNode { private let strings: PresentationStrings private let text: String @@ -26,13 +101,15 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { private let actionNodes: [TextAlertContentActionNode] private let actionVerticalSeparators: [ASDisplayNode] + private var boostNodes: [PreviousBoostNode] = [] + private var validLayout: CGSize? override var dismissOnOutsideTap: Bool { return self.isUserInteractionEnabled } - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeer: EnginePeer, toPeer: EnginePeer, text: String, actions: [TextAlertAction]) { + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeers: [EnginePeer], toPeer: EnginePeer, text: String, actions: [TextAlertAction]) { self.strings = strings self.text = text @@ -49,7 +126,7 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false - self.iconNode.image = UIImage(bundleImageName: "Premium/AvatarBoost") + self.iconNode.image = generateBoostIcon(theme: ptheme) self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true @@ -68,6 +145,18 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { } self.actionVerticalSeparators = actionVerticalSeparators + var boostNodes: [PreviousBoostNode] = [] + if fromPeers.count > 1 { + for peer in fromPeers { + let boostNode = PreviousBoostNode(context: context, theme: theme, ptheme: ptheme, peer: peer, badge: nil) + if boostNodes.isEmpty { + boostNode.setChecked(true) + } + boostNodes.append(boostNode) + } + } + self.boostNodes = boostNodes + super.init() self.addSubnode(self.textNode) @@ -86,9 +175,20 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { self.addSubnode(separatorNode) } + for boostNode in self.boostNodes { + boostNode.pressed = { [weak self] sender in + if let self { + for node in self.boostNodes { + node.setChecked(node === sender) + } + } + } + self.addSubnode(boostNode) + } + self.updateTheme(theme) - self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeer) + self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeers.first!) self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer) } @@ -145,8 +245,10 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { origin.y += avatarSize.height + 10.0 + var entriesHeight: CGFloat = 0.0 let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 10.0 let actionButtonHeight: CGFloat = 44.0 var minActionsWidth: CGFloat = 0.0 @@ -171,6 +273,17 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { let contentWidth = max(size.width, minActionsWidth) + if !self.boostNodes.isEmpty { + origin.y += 17.0 + for boostNode in self.boostNodes { + let boostSize = boostNode.updateLayout(size: size, transition: transition) + transition.updateFrame(node: boostNode, frame: CGRect(origin: CGPoint(x: 36.0, y: origin.y), size: boostSize)) + + entriesHeight += boostSize.height + 20.0 + origin.y += boostSize.height + 20.0 + } + } + var actionsHeight: CGFloat = 0.0 switch effectiveActionLayout { case .horizontal: @@ -179,8 +292,7 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) } - let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) - + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + entriesHeight + actionsHeight + 16.0 + insets.top + insets.bottom) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) var actionOffset: CGFloat = 0.0 @@ -230,21 +342,22 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { } } -func replaceBoostConfirmationController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, text: String, commit: @escaping () -> Void) -> AlertController { - let theme = defaultDarkColorPresentationTheme +func replaceBoostConfirmationController(context: AccountContext, fromPeers: [EnginePeer], toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings + let text = strings.ChannelBoost_ReplaceBoost(fromPeers.first!.compactDisplayTitle, toPeer.compactDisplayTitle).string + var dismissImpl: ((Bool) -> Void)? - var contentNode: PhotoUpdateConfirmationAlertContentNode? + var contentNode: ReplaceBoostConfirmationAlertContentNode? let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_Replace, action: { + }), TextAlertAction(type: .defaultAction, title: strings.ChannelBoost_Replace, action: { dismissImpl?(true) commit() })] - contentNode = PhotoUpdateConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, fromPeer: fromPeer, toPeer: toPeer, text: text, actions: actions) + contentNode = ReplaceBoostConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, fromPeers: fromPeers, toPeer: toPeer, text: text, actions: actions) let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) dismissImpl = { [weak controller] animated in diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift new file mode 100644 index 00000000000..b87262f2c02 --- /dev/null +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -0,0 +1,1282 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BundleIconComponent +import UndoUI +import Markdown +import TextFormat +import ButtonComponent +import PeerListItemComponent +import TelegramStringFormatting +import AvatarNode + +private final class ReplaceBoostScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peerId: EnginePeer.Id + let myBoostStatus: MyBoostStatus + let initiallySelectedSlot: Int32? + let selectedSlotsUpdated: ([Int32]) -> Void + let presentController: (ViewController) -> Void + + init(context: AccountContext, peerId: EnginePeer.Id, myBoostStatus: MyBoostStatus, initiallySelectedSlot: Int32?, selectedSlotsUpdated: @escaping ([Int32]) -> Void, presentController: @escaping (ViewController) -> Void) { + self.context = context + self.peerId = peerId + self.myBoostStatus = myBoostStatus + self.initiallySelectedSlot = initiallySelectedSlot + self.selectedSlotsUpdated = selectedSlotsUpdated + self.presentController = presentController + } + + static func ==(lhs: ReplaceBoostScreenComponent, rhs: ReplaceBoostScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.myBoostStatus != rhs.myBoostStatus { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private let disposable = MetaDisposable() + private var timer: SwiftSignalKit.Timer? + + var peer: EnginePeer? + var selectedSlots: [Int32] = [] + var currentTime: Int32 + + var cachedCloseImage: (UIImage, PresentationTheme)? + + init(context: AccountContext, peerId: EnginePeer.Id, initiallySelectedSlot: Int32?) { + self.context = context + + self.currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + super.init() + + if let initiallySelectedSlot { + self.selectedSlots.append(initiallySelectedSlot) + } + + self.disposable.set((context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStrict(next: { [weak self] peer in + guard let self else { + return + } + self.peer = peer + self.updated() + })) + + self.timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + if let self { + self.currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + self.updated() + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + deinit { + self.disposable.dispose() + self.timer?.invalidate() + } + } + + func makeState() -> State { + return State(context: self.context, peerId: self.peerId, initiallySelectedSlot: self.initiallySelectedSlot) + } + + static var body: Body { + let header = Child(ReplaceBoostHeaderComponent.self) + let description = Child(MultilineTextComponent.self) + let boostsBackground = Child(RoundedRectangle.self) + let boosts = Child(List.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let state = context.state + let availableSize = context.availableSize + let theme = environment.theme + let strings = environment.strings + + let textSideInset: CGFloat = 32.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + var boostItems: [AnyComponentWithIdentity] = [] + let myBoosts = context.component.myBoostStatus.boosts + + let occupiedBoosts = myBoosts.filter { $0.peer?.id != context.component.peerId && $0.peer != nil }.sorted { lhs, rhs in + return lhs.date < rhs.date + } + + var otherPeers: [EnginePeer] = [] + for slot in state.selectedSlots { + if let peer = occupiedBoosts.first(where: { $0.slot == slot })?.peer { + if !otherPeers.contains(where: { $0.id == peer.id }) { + otherPeers.append(peer) + } + } + } + if let mainPeer = state.peer { + let header = header.update( + component: ReplaceBoostHeaderComponent( + context: context.component.context, + theme: environment.theme, + mainPeer: mainPeer, + otherPeers: otherPeers.reversed() + ), + availableSize: availableSize, + transition: context.transition + ) + context.add(header + .position(CGPoint(x: availableSize.width / 2.0, y: 93.0)) + ) + } + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + + 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 premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.component.context.currentAppConfiguration.with({ $0 })) + + var channelName = state.peer?.compactDisplayTitle ?? "" + if channelName.count > 48 { + channelName = "\(channelName.prefix(48))..." + } + let descriptionString = strings.ReassignBoost_Description(channelName, "\(premiumConfiguration.boostsPerGiftCount)").string + + let description = description.update( + component: MultilineTextComponent( + text: .markdown(text: descriptionString, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + environment: {}, + availableSize: CGSize(width: availableSize.width - sideInset * 2.0 - textSideInset, height: availableSize.height), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: availableSize.width / 2.0, y: 172.0)) + ) + + let hasSelection = occupiedBoosts.count > 1 + + let selectedSlotsUpdated = context.component.selectedSlotsUpdated + let presentController = context.component.presentController + for i in 0 ..< occupiedBoosts.count { + let boost = occupiedBoosts[i] + guard let peer = boost.peer else { + continue + } + + var isEnabled = true + let subtitle: String + if let cooldownUntil = boost.cooldownUntil, cooldownUntil > state.currentTime { + let duration = cooldownUntil - state.currentTime + let durationValue = stringForDuration(duration, position: nil) + subtitle = strings.ReassignBoost_AvailableIn(durationValue).string + isEnabled = false + } else { + let expiresValue = stringForDate(timestamp: boost.expires, strings: strings) + subtitle = strings.ReassignBoost_ExpiresOn(expiresValue).string + } + + let accountContext = context.component.context + boostItems.append( + AnyComponentWithIdentity( + id: AnyHashable(boost.slot), + component: AnyComponent( + PeerListItemComponent( + context: context.component.context, + theme: theme, + strings: strings, + style: .generic, + sideInset: 0.0, + title: peer.compactDisplayTitle, + peer: peer, + subtitle: subtitle, + subtitleAccessory: .none, + presence: nil, + selectionState: hasSelection ? .editing(isSelected: state.selectedSlots.contains(boost.slot), isTinted: false) : .none, + selectionPosition: .right, + isEnabled: isEnabled, + hasNext: i != occupiedBoosts.count - 1, + action: { [weak state] _ in + guard let state, hasSelection else { + return + } + if isEnabled { + if state.selectedSlots.contains(boost.slot) { + state.selectedSlots.removeAll(where: { $0 == boost.slot }) + } else { + state.selectedSlots.append(boost.slot) + } + state.updated(transition: .easeInOut(duration: 0.2)) + selectedSlotsUpdated(state.selectedSlots) + } else { + let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } + + let undoController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: strings.ReassignBoost_WaitForCooldown("\(premiumConfiguration.boostsPerGiftCount)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true }) + presentController(undoController) + } + }) + ) + ) + ) + } + + let boosts = boosts.update( + component: List(boostItems), + environment: {}, + availableSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0), + transition: context.transition + ) + + let boostsBackground = boostsBackground.update( + component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 10.0), + environment: {}, + availableSize: CGSize(width: availableSize.width - sideInset * 2.0, height: boosts.size.height), + transition: context.transition + ) + + context.add(boostsBackground + .position(CGPoint(x: availableSize.width / 2.0, y: 226 + boosts.size.height / 2.0)) + ) + context.add(boosts + .position(CGPoint(x: availableSize.width / 2.0, y: 226 + boosts.size.height / 2.0)) + ) + + return CGSize(width: availableSize.width, height: 226.0 + boosts.size.height + environment.safeInsets.bottom + 91.0) + } + } +} + +public class ReplaceBoostScreen: ViewController { + final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: ReplaceBoostScreen? + + private let component: AnyComponent + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + let scrollView: UIScrollView + let hostView: ComponentHostView + + private let footerView: FooterView + private var footerHeight: CGFloat = 0.0 + private var bottomOffset: CGFloat = 1000.0 + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? + + var selectedSlots: [Int32] = [] { + didSet { + self.controller?.requestLayout(transition: .immediate) + } + } + + private var currentIsVisible: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + init(context: AccountContext, controller: ReplaceBoostScreen, component: AnyComponent) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.controller = controller + + self.component = component + + let effectiveTheme = self.presentationData.theme + + 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.scrollView = UIScrollView() + self.hostView = ComponentHostView() + + self.footerView = FooterView() + + super.init() + + self.scrollView.delegate = self + self.scrollView.showsVerticalScrollIndicator = false + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = effectiveTheme.list.blocksBackgroundColor + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.scrollView) + self.scrollView.addSubview(self.hostView) + + self.wrappingView.addSubview(self.footerView) + + self.footerView.action = { [weak self] in + guard let self else { + return + } + self.controller?.replaceBoosts?(self.selectedSlots) + } + self.footerView.updateBackgroundAlpha(1.0, transition: .immediate) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, !self.footerView.inProgress { + 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 + } + + private func updateFooterAlpha() { + guard let (layout, _) = self.currentLayout else { + return + } + let contentFrame = self.scrollView.convert(self.hostView.frame, to: self.view) + let bottomOffset = contentFrame.maxY - layout.size.height + + let backgroundAlpha: CGFloat = min(30.0, max(0.0, bottomOffset)) / 30.0 + self.footerView.updateBackgroundAlpha(backgroundAlpha, transition: .immediate) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = self.scrollView.contentOffset.y + self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) + + self.updateFooterAlpha() + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + 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) + + let footerTargetPosition = self.footerView.center + let footerStartPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + self.footerView.center = footerStartPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + self.footerView.center = footerTargetPosition + }, 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) + }) + positionTransition.updatePosition(layer: self.footerView.layer, position: CGPoint(x: self.footerView.center.x, y: self.bounds.height + self.footerView.bounds.height / 2.0)) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + let hadLayout = self.currentLayout != nil + self.currentLayout = (layout, navigationHeight) + + if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { + self.containerView.addSubview(navigationBar.view) + } + + 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 environment = ViewControllerComponentContainer.Environment( + statusBarHeight: 0.0, + navigationHeight: navigationHeight, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + isVisible: self.currentIsVisible, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + let contentSize = self.hostView.update( + transition: transition, + component: self.component, + environment: { + environment + }, + forceUpdate: true, + containerSize: CGSize(width: layout.size.width, height: 10000.0) + ) +// contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + + self.scrollView.contentSize = contentSize + + 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 { + 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) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) + + let footerInsets = UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) + + transition.setFrame(view: self.footerView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: layout.size)) + self.footerHeight = self.footerView.update(size: layout.size, insets: footerInsets, theme: self.presentationData.theme, strings: self.presentationData.strings, count: Int32(self.selectedSlots.count)) + + if !hadLayout { + self.updateFooterAlpha() + } + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var defaultTopInset: CGFloat { + guard let (layout, _) = self.currentLayout else{ + return 210.0 + } + if case .compact = layout.metrics.widthClass { + var factor: CGFloat = 0.2488 + if layout.size.width <= 320.0 { + factor = 0.15 + } + if self.scrollView.contentSize.height > 0.0 && self.scrollView.contentSize.height < layout.size.height / 2.0 { + return layout.size.height - self.scrollView.contentSize.height - layout.intrinsicInsets.bottom - 30.0 + } else { + return floor(max(layout.size.width, layout.size.height) * factor) + } + } else { + return 210.0 + } + } + + private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { + if let view = view { + if let view = view as? UIScrollView { + return (view, nil) + } + if let node = view.asyncdisplaykit_node as? ListView { + return (node.scroller, node) + } + return findScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollViewAndListNode = self.findScrollView(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) + } + } + + 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, navigationHeight: navigationHeight, transition: .immediate) + + self.updateFooterAlpha() + 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, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if (velocity.y < -300.0 || offset < topInset / 2.0) { + 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, navigationHeight: navigationHeight, 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, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + self.updateFooterAlpha() + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + + self.updateFooterAlpha() + default: + break + } + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.isExpanded = isExpanded + + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let component: AnyComponent + + private var replaceBoosts: (([Int32]) -> Void)? + + private var currentLayout: ContainerViewLayout? + + public convenience init(context: AccountContext, peerId: EnginePeer.Id, myBoostStatus: MyBoostStatus, replaceBoosts: @escaping ([Int32]) -> Void) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var initiallySelectedSlot: Int32? + let occupiedBoosts = myBoostStatus.boosts.filter { $0.peer?.id != peerId && $0.peer != nil }.sorted { lhs, rhs in + return lhs.date < rhs.date + } + if occupiedBoosts.count == 1, let boost = occupiedBoosts.first { + initiallySelectedSlot = boost.slot + } + + var selectedSlotsUpdatedImpl: (([Int32]) -> Void)? + var presentControllerImpl: ((ViewController) -> 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) + })) + + self.title = presentationData.strings.ReassignBoost_Title + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + selectedSlotsUpdatedImpl = { [weak self] selectedSlots in + self?.node.selectedSlots = selectedSlots + } + presentControllerImpl = { [weak self] c in + self?.dismissAllTooltips() + self?.present(c, in: .window(.root)) + } + + self.replaceBoosts = replaceBoosts + + if let initiallySelectedSlot { + self.node.selectedSlots = [initiallySelectedSlot] + } + } + + private init(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + self.context = context + self.component = AnyComponent(component) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, component: self.component) + self.displayNodeDidLoad() + } + + 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(animated: true, completion: nil) + } + + 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 viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var navigationLayout = self.navigationLayout(layout: layout) + var navigationFrame = navigationLayout.navigationFrame + + var layout = layout + if case .regular = layout.metrics.widthClass { + 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) + let clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + navigationFrame.size.width = clipFrame.width + layout.size = clipFrame.size + } + + navigationFrame.size.height = 56.0 + navigationLayout.navigationFrame = navigationFrame + navigationLayout.defaultContentHeight = 56.0 + + layout.statusBarHeight = nil + + self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, transition: transition) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + let navigationHeight: CGFloat = 56.0 + + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } +} + +private final class FooterView: UIView { + private let backgroundNode: NavigationBackgroundNode + private let separatorView: UIView + private let button = ComponentView() + + var action: () -> Void = {} + + init() { + self.backgroundNode = NavigationBackgroundNode(color: .clear) + self.separatorView = UIView() + + super.init(frame: .zero) + + self.addSubnode(self.backgroundNode) + self.addSubview(self.separatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate var inProgress = false + + private var currentLayout: (CGSize, UIEdgeInsets, PresentationTheme, PresentationStrings, Int32)? + func update(size: CGSize, insets: UIEdgeInsets, theme: PresentationTheme, strings: PresentationStrings, count: Int32) -> CGFloat { + let hadLayout = self.currentLayout != nil + self.currentLayout = (size, insets, theme, strings, count) + + self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate) + self.separatorView.backgroundColor = theme.rootController.tabBar.separatorColor + + let buttonInset: CGFloat = 16.0 + let buttonWidth = size.width - insets.left - insets.right - buttonInset * 2.0 + let inset: CGFloat = 9.0 + + var panelHeight: CGFloat = 50.0 + inset * 2.0 + panelHeight += insets.bottom + + let totalPanelHeight = panelHeight + + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - totalPanelHeight), size: CGSize(width: size.width, height: panelHeight)) + + var buttonTransition: Transition = .easeInOut(duration: 0.2) + if !hadLayout { + buttonTransition = .immediate + } + let buttonSize = self.button.update( + transition: buttonTransition, + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: strings.ReassignBoost_ReassignBoosts, + badge: Int(count), + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor, + badgeStyle: .roundedRectangle, + badgeIconName: "Premium/BoostButtonIcon", + combinedAlignment: true + )) + ), + isEnabled: true, + displaysProgress: self.inProgress, + action: { [weak self] in + guard let self, !self.inProgress else { + return + } + self.inProgress = true + if let (size, insets, theme, strings, count) = self.currentLayout { + let _ = self.update(size: size, insets: insets, theme: theme, strings: strings, count: count) + } + self.action() + } + ) + ), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: 50.0) + ) + if let view = self.button.view { + if view.superview == nil { + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: insets.left + buttonInset, y: panelFrame.minY + inset), size: buttonSize) + + buttonTransition.setAlpha(view: view, alpha: count > 0 ? 1.0 : 0.3) + view.isUserInteractionEnabled = count > 0 + } + + self.backgroundNode.frame = panelFrame + self.backgroundNode.update(size: panelFrame.size, transition: .immediate) + self.separatorView.frame = CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel)) + + return panelHeight + } + + func updateBackgroundAlpha(_ alpha: CGFloat, transition: Transition) { + transition.setAlpha(view: self.backgroundNode.view, alpha: alpha) + transition.setAlpha(view: self.separatorView, alpha: alpha) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.backgroundNode.frame.contains(point) { + return true + } else { + return false + } + } +} + +private func generateBoostIcon(theme: PresentationTheme) -> UIImage? { + if let image = UIImage(bundleImageName: "Premium/AvatarBoost") { + let size = CGSize(width: image.size.width + 4.0, height: image.size.height + 4.0) + return generateImage(size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: image.size)) + } + + let lineWidth = 2.0 - UIScreenPixel + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.list.blocksBackgroundColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel)) + }, opaque: false) + } + return nil +} + +private final class ReplaceBoostHeaderComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let mainPeer: EnginePeer + let otherPeers: [EnginePeer] + + init(context: AccountContext, theme: PresentationTheme, mainPeer: EnginePeer, otherPeers: [EnginePeer]) { + self.context = context + self.theme = theme + self.mainPeer = mainPeer + self.otherPeers = otherPeers + } + + static func ==(lhs: ReplaceBoostHeaderComponent, rhs: ReplaceBoostHeaderComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.mainPeer != rhs.mainPeer { + return false + } + if lhs.otherPeers != rhs.otherPeers { + return false + } + return true + } + + final class WrapperAvatarView: UIView { + let backgroundView = UIView() + let avatarNode: AvatarNode + let badgeImageView: UIImageView + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + self.avatarNode.frame = CGRect(origin: .zero, size: CGSize(width: 60.0, height: 60.0)) + self.backgroundView.frame = self.avatarNode.frame.insetBy(dx: -3.0 + UIScreenPixel, dy: -3.0 + UIScreenPixel) + self.backgroundView.layer.cornerRadius = self.backgroundView.frame.height / 2.0 + + self.badgeImageView = UIImageView(frame: CGRect(x: 60.0 - 24.0, y: 60.0 - 24.0, width: 28.0, height: 28.0)) + self.badgeImageView.alpha = 0.0 + + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + + self.addSubview(self.backgroundView) + self.addSubnode(self.avatarNode) + self.addSubview(self.badgeImageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + + final class View: UIView { + private let containerView = UIView() + private let avatarNode: AvatarNode + private let arrowView: UIImageView + + private var otherAvatarNodes: [EnginePeer.Id: WrapperAvatarView] = [:] + + private var component: ReplaceBoostHeaderComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + self.avatarNode.frame = CGRect(origin: .zero, size: CGSize(width: 60.0, height: 60.0)) + + self.arrowView = UIImageView(image: UIImage(bundleImageName: "Peer Info/AlertArrow")?.withRenderingMode(.alwaysTemplate)) + + super.init(frame: frame) + + self.addSubview(self.containerView) + self.containerView.addSubview(self.arrowView) + self.containerView.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var badgeImage: UIImage? + func update(component: ReplaceBoostHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let avatarSize = CGSize(width: 60.0, height: 60.0) + + let spacing: CGFloat = 27.0 + var totalWidth: CGFloat = avatarSize.width + if !component.otherPeers.isEmpty { + totalWidth += spacing + totalWidth += avatarSize.width + + if component.otherPeers.count > 1 { + totalWidth += (avatarSize.width / 2.0) * CGFloat(component.otherPeers.count - 1) + } + } + + transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: (availableSize.width - totalWidth) / 2.0, y: 0.0), size: CGSize(width: totalWidth, height: avatarSize.height))) + + var originX: CGFloat = 0.0 + var validIds: [EnginePeer.Id] = [] + for i in 0 ..< component.otherPeers.count { + let peer = component.otherPeers[i] + validIds.append(peer.id) + + let avatarView: WrapperAvatarView + var avatarTransition = transition + if let current = self.otherAvatarNodes[peer.id] { + avatarView = current + } else { + avatarTransition = .immediate + + avatarView = WrapperAvatarView() + avatarView.bounds = CGRect(origin: .zero, size: avatarSize) + avatarView.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, synchronousLoad: true) + avatarView.backgroundView.backgroundColor = component.theme.list.blocksBackgroundColor + + if self.badgeImage == nil { + self.badgeImage = generateBoostIcon(theme: component.theme) + } + avatarView.badgeImageView.image = self.badgeImage + + self.otherAvatarNodes[peer.id] = avatarView + self.containerView.insertSubview(avatarView, at: 0) + + avatarView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + avatarView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + let isLast = i == component.otherPeers.count - 1 + avatarTransition.setAlpha(view: avatarView.badgeImageView, alpha: isLast ? 1.0 : 0.0) + avatarTransition.setScale(view: avatarView.badgeImageView, scale: isLast ? 1.0 : 0.1) + + avatarTransition.setPosition(view: avatarView, position: CGPoint(x: originX + avatarSize.width / 2.0, y: avatarSize.height / 2.0)) + if isLast { + originX += avatarSize.width + } else { + originX += avatarSize.width / 2.0 + } + } + + if !component.otherPeers.isEmpty { + originX += spacing + } + + self.arrowView.tintColor = component.theme.list.itemSecondaryTextColor + transition.setAlpha(view: self.arrowView, alpha: component.otherPeers.isEmpty ? 0.0 : 1.0) + transition.setScale(view: self.arrowView, scale: component.otherPeers.isEmpty ? 0.1 : 1.0) + transition.setPosition(view: self.arrowView, position: CGPoint(x: originX - 13.0, y: avatarSize.height / 2.0)) + + transition.setFrame(view: self.avatarNode.view, frame: CGRect(origin: CGPoint(x: originX, y: 0.0), size: avatarSize)) + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.mainPeer, synchronousLoad: true) + + var removeIds: [EnginePeer.Id] = [] + for (id, avatarView) in self.otherAvatarNodes { + if !validIds.contains(id) { + removeIds.append(id) + avatarView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + avatarView.removeFromSuperview() + }) + avatarView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } + } + for id in removeIds { + self.otherAvatarNodes.removeValue(forKey: id) + } + + return CGSize(width: availableSize.width, height: avatarSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift b/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift new file mode 100644 index 00000000000..9f1d26a4939 --- /dev/null +++ b/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift @@ -0,0 +1,372 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import LegacyComponents +import ItemListUI +import PresentationDataUtils + +final class SubscriptionsCountItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let value: Int32 + let sectionId: ItemListSectionId + let updated: (Int32) -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + self.value = value + self.sectionId = sectionId + self.updated = updated + } + + 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 = SubscriptionsCountItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? SubscriptionsCountItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +private final class SubscriptionsCountItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let label1TextNode: TextNode + private let label3TextNode: TextNode + private let label5TextNode: TextNode + private let label7TextNode: TextNode + private let label10TextNode: TextNode + private let label25TextNode: TextNode + private let label50TextNode: TextNode + private var sliderView: TGPhotoEditorSliderView? + + private var item: SubscriptionsCountItem? + private var layoutParams: ListViewItemLayoutParams? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.label1TextNode = TextNode() + self.label1TextNode.isUserInteractionEnabled = false + self.label1TextNode.displaysAsynchronously = false + + self.label3TextNode = TextNode() + self.label3TextNode.isUserInteractionEnabled = false + self.label3TextNode.displaysAsynchronously = false + + self.label5TextNode = TextNode() + self.label5TextNode.isUserInteractionEnabled = false + self.label5TextNode.displaysAsynchronously = false + + self.label7TextNode = TextNode() + self.label7TextNode.isUserInteractionEnabled = false + self.label7TextNode.displaysAsynchronously = false + + self.label10TextNode = TextNode() + self.label10TextNode.isUserInteractionEnabled = false + self.label10TextNode.displaysAsynchronously = false + + self.label25TextNode = TextNode() + self.label25TextNode.isUserInteractionEnabled = false + self.label25TextNode.displaysAsynchronously = false + + self.label50TextNode = TextNode() + self.label50TextNode.isUserInteractionEnabled = false + self.label50TextNode.displaysAsynchronously = false + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.label1TextNode) + self.addSubnode(self.label3TextNode) + self.addSubnode(self.label5TextNode) + self.addSubnode(self.label7TextNode) + self.addSubnode(self.label10TextNode) + self.addSubnode(self.label25TextNode) + self.addSubnode(self.label50TextNode) + } + + override func didLoad() { + super.didLoad() + + let sliderView = TGPhotoEditorSliderView() + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 2.0 + sliderView.lineSize = 4.0 + sliderView.dotSize = 8.0 + sliderView.minimumValue = 0.0 + sliderView.maximumValue = 6.0 + sliderView.startValue = 0.0 + sliderView.positionsCount = 7 + sliderView.useLinesForPositions = true + sliderView.disablesInteractiveTransitionGestureRecognizer = true + if let item = self.item, let params = self.layoutParams { + var mappedValue: Int32 = 0 + switch Int(item.value) { + case 1: + mappedValue = 0 + case 3: + mappedValue = 1 + case 5: + mappedValue = 2 + case 7: + mappedValue = 3 + case 10: + mappedValue = 4 + case 25: + mappedValue = 5 + case 50: + mappedValue = 6 + default: + mappedValue = 0 + } + sliderView.value = CGFloat(mappedValue) + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSwitchColors.frameColor + sliderView.startColor = item.theme.list.itemSwitchColors.frameColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) + } + self.view.addSubview(sliderView) + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + self.sliderView = sliderView + } + + func asyncLayout() -> (_ item: SubscriptionsCountItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + let makeLabel1TextLayout = TextNode.asyncLayout(self.label1TextNode) + let makeLabel3TextLayout = TextNode.asyncLayout(self.label3TextNode) + let makeLabel5TextLayout = TextNode.asyncLayout(self.label5TextNode) + let makeLabel7TextLayout = TextNode.asyncLayout(self.label7TextNode) + let makeLabel10TextLayout = TextNode.asyncLayout(self.label10TextNode) + let makeLabel25TextLayout = TextNode.asyncLayout(self.label25TextNode) + let makeLabel50TextLayout = TextNode.asyncLayout(self.label50TextNode) + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let (label1TextLayout, label1TextApply) = makeLabel1TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "1", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (label3TextLayout, label3TextApply) = makeLabel3TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "3", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (label5TextLayout, label5TextApply) = makeLabel5TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "5", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (label7TextLayout, label7TextApply) = makeLabel7TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "7", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (label10TextLayout, label10TextApply) = makeLabel10TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "10", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (label25TextLayout, label25TextApply) = makeLabel25TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "25", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (label50TextLayout, label50TextApply) = makeLabel50TextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "50", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + contentSize = CGSize(width: params.width, height: 88.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + 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.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)) + + let _ = label1TextApply() + let _ = label3TextApply() + let _ = label5TextApply() + let _ = label7TextApply() + let _ = label10TextApply() + let _ = label25TextApply() + let _ = label50TextApply() + + let textNodes: [(TextNode, CGSize)] = [ + (strongSelf.label1TextNode, label1TextLayout.size), + (strongSelf.label3TextNode, label3TextLayout.size), + (strongSelf.label5TextNode, label5TextLayout.size), + (strongSelf.label7TextNode, label7TextLayout.size), + (strongSelf.label10TextNode, label10TextLayout.size), + (strongSelf.label25TextNode, label25TextLayout.size), + (strongSelf.label50TextNode, label50TextLayout.size) + ] + + let delta = (params.width - params.leftInset - params.rightInset - 20.0 * 2.0) / CGFloat(textNodes.count - 1) + for i in 0 ..< textNodes.count { + let (textNode, textSize) = textNodes[i] + + let position = params.leftInset + 20.0 + delta * CGFloat(i) + textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(position - textSize.width / 2.0), y: 15.0), size: textSize) + } + + if let sliderView = strongSelf.sliderView { + if themeUpdated { + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSwitchColors.frameColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + } + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) + + var mappedValue: Int32 = 0 + switch Int(item.value) { + case 1: + mappedValue = 0 + case 3: + mappedValue = 1 + case 5: + mappedValue = 2 + case 7: + mappedValue = 3 + case 10: + mappedValue = 4 + case 25: + mappedValue = 5 + case 50: + mappedValue = 6 + default: + mappedValue = 0 + } + if Int32(sliderView.value) != mappedValue { + sliderView.value = CGFloat(mappedValue) + } + } + } + }) + } + } + + 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc func sliderValueChanged() { + guard let sliderView = self.sliderView else { + return + } + + var mappedValue: Int32 = 1 + switch Int(sliderView.value) { + case 0: + mappedValue = 1 + case 1: + mappedValue = 3 + case 2: + mappedValue = 5 + case 3: + mappedValue = 7 + case 4: + mappedValue = 10 + case 5: + mappedValue = 25 + case 6: + mappedValue = 50 + default: + mappedValue = 1 + } + + self.item?.updated(Int32(mappedValue)) + } +} diff --git a/submodules/PresentationDataUtils/Sources/OpenUrl.swift b/submodules/PresentationDataUtils/Sources/OpenUrl.swift index 8ccc7d91f8c..6a7a7a52b6d 100644 --- a/submodules/PresentationDataUtils/Sources/OpenUrl.swift +++ b/submodules/PresentationDataUtils/Sources/OpenUrl.swift @@ -6,42 +6,73 @@ import AccountContext import OverlayStatusController import UrlWhitelist -public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void) { +public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise? = nil) -> Disposable { var concealed = concealed let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let openImpl: () -> Void = { + let openImpl: () -> Disposable = { let disposable = MetaDisposable() var cancelImpl: (() -> Void)? - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - present(controller) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() + let progressSignal: Signal + + if let progress { + progressSignal = Signal { subscriber in + progress.set(.single(true)) + return ActionDisposable { + progress.set(.single(false)) + } + } + |> runOn(Queue.mainQueue()) + } else { + progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + present(controller) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } } } + |> runOn(Queue.mainQueue()) } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() + let progressDisposable = MetaDisposable() + var didStartProgress = false cancelImpl = { disposable.dispose() } - disposable.set((context.sharedContext.resolveUrl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) + + var resolveSignal: Signal + resolveSignal = context.sharedContext.resolveUrlWithProgress(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) + #if DEBUG + //resolveSignal = .single(.progress) |> then(resolveSignal |> delay(2.0, queue: .mainQueue())) + #endif + + disposable.set((resolveSignal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } |> deliverOnMainQueue).start(next: { result in - progressDisposable.dispose() - openResolved(result) + switch result { + case .progress: + if !didStartProgress { + didStartProgress = true + progressDisposable.set(progressSignal.start()) + } + case let .result(result): + progressDisposable.dispose() + openResolved(result) + } })) + + return ActionDisposable { + cancelImpl?() + } } let (parsedString, parsedConcealed) = parseUrl(url: url, wasConcealed: concealed) @@ -55,10 +86,12 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: } var displayUrl = rawDisplayUrl displayUrl = displayUrl.replacingOccurrences(of: "\u{202e}", with: "") + let disposable = MetaDisposable() present(textAlertController(context: context, title: nil, text: presentationData.strings.Generic_OpenHiddenLinkAlert(displayUrl).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { - openImpl() + disposable.set(openImpl()) })])) + return disposable } else { - openImpl() + return openImpl() } } diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 9cfd90de3f8..b5dfcbedb55 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -390,7 +390,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie private weak var controller: QrCodeScanScreen? private let subject: QrCodeScanScreen.Subject - private let previewNode: CameraPreviewNode + private let previewView: CameraSimplePreviewView private let fadeNode: ASDisplayNode private let topDimNode: ASDisplayNode private let bottomDimNode: ASDisplayNode @@ -436,8 +436,8 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.controller = controller self.subject = subject - self.previewNode = CameraPreviewNode() - self.previewNode.backgroundColor = .black + self.previewView = CameraSimplePreviewView(frame: .zero, main: true) + self.previewView.backgroundColor = .black self.fadeNode = ASDisplayNode() self.fadeNode.alpha = 0.0 @@ -513,7 +513,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.errorTextNode.textAlignment = .center self.errorTextNode.isHidden = true - self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false, photo: true, metadata: true, preferredFps: 60)) + self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false, photo: true, metadata: true, preferredFps: 60), previewView: self.previewView) super.init() @@ -526,7 +526,6 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie } }) - self.addSubnode(self.previewNode) self.addSubnode(self.fadeNode) self.addSubnode(self.topDimNode) self.addSubnode(self.bottomDimNode) @@ -544,6 +543,20 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.galleryButtonNode.addTarget(self, action: #selector(self.galleryPressed), forControlEvents: .touchUpInside) self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside) + + self.previewView.resetPlaceholder(front: false) + if #available(iOS 13.0, *) { + let _ = (self.previewView.isPreviewing + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + self?.previewView.removePlaceholder(delay: 0.15) + }) + } else { + Queue.mainQueue().after(0.35) { + self.previewView.removePlaceholder(delay: 0.15) + } + } } deinit { @@ -564,7 +577,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie override func didLoad() { super.didLoad() - self.camera.attachPreviewNode(self.previewNode) + self.view.insertSubview(self.previewView, at: 0) self.camera.startCapture() let throttledSignal = self.camera.detectedCodes @@ -671,14 +684,14 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie if case .tablet = layout.deviceMetrics.type { if UIDevice.current.orientation == .landscapeLeft { - self.previewNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.previewView.layer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } else if UIDevice.current.orientation == .landscapeRight { - self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.previewView.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } else { - self.previewNode.transform = CATransform3DIdentity + self.previewView.layer.transform = CATransform3DIdentity } } - transition.updateFrame(node: self.previewNode, frame: bounds) + transition.updateFrame(view: self.previewView, frame: bounds) transition.updateFrame(node: self.fadeNode, frame: bounds) let frameSide = max(240.0, layout.size.width - sideInset * 2.0) diff --git a/submodules/Reachability/LegacyReachability/Package.swift b/submodules/Reachability/LegacyReachability/Package.swift index d5d9a0aa998..d0ddfe915b9 100644 --- a/submodules/Reachability/LegacyReachability/Package.swift +++ b/submodules/Reachability/LegacyReachability/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "LegacyReachability", - platforms: [.macOS(.v10_12)], + 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/Reachability/Package.swift b/submodules/Reachability/Package.swift index ce0c93317f3..6becc10d3c5 100644 --- a/submodules/Reachability/Package.swift +++ b/submodules/Reachability/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "Reachability", - platforms: [.macOS(.v10_12)], + 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/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index e74d55a2634..d7d088dbdd3 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1677,7 +1677,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { chatPeerId: nil, peekBehavior: nil, customLayout: emojiContentLayout, - externalBackground: EmojiPagerContentComponent.ExternalBackground( + externalBackground: self.backgroundNode.vibrancyEffectView == nil ? nil : EmojiPagerContentComponent.ExternalBackground( effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView ), externalExpansionView: self.view, diff --git a/submodules/SSignalKit/Package.swift b/submodules/SSignalKit/Package.swift index b6cf5c3a192..fe090562c64 100644 --- a/submodules/SSignalKit/Package.swift +++ b/submodules/SSignalKit/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "SSignalKit", - platforms: [.macOS(.v10_12)], + 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/SSignalKit/SSignalKit/BUILD b/submodules/SSignalKit/SSignalKit/BUILD index eb2b9a82f7a..98f1b6f95cf 100644 --- a/submodules/SSignalKit/SSignalKit/BUILD +++ b/submodules/SSignalKit/SSignalKit/BUILD @@ -16,7 +16,6 @@ objc_library( "Source", ], deps = [ - "//submodules/SSignalKit/SwiftSignalKit" ], sdk_frameworks = [ "Foundation", diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 546e19475ea..924374f2509 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -119,6 +119,7 @@ swift_library( "//submodules/ImageBlur:ImageBlur", "//submodules/AttachmentUI:AttachmentUI", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index ae792406473..b549d3d41e8 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -163,8 +163,8 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) 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: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) @@ -172,7 +172,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift index efa45cab1c4..a423c640a1b 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -466,7 +466,7 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat let presentGlobalController = context.sharedContext.presentGlobalController let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: false).start(completed: { Queue.mainQueue().after(0.1) { - presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) + presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success, timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) } }) }) @@ -486,18 +486,22 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat action(peers, nil) }) case .phone: - var phoneNumber: String? + var code: String? + var number: String? controller?.forEachItemNode { itemNode in if let itemNode = itemNode as? DeleteAccountPhoneItemNode { - var phoneValue = itemNode.phoneNumber - if phoneValue.hasPrefix("+939998") { - phoneValue = phoneValue.replacingOccurrences(of: "+939998", with: "+9998") + let value = itemNode.codeNumberAndFullNumber + if value.0 == "+93" && value.1.hasPrefix("9998") { + code = "+" + number = value.1 + } else { + code = value.0 + number = value.1 } - phoneNumber = phoneValue } } - if let phoneNumber = phoneNumber, phoneNumber.count > 4 { + if let code, var number, (code + number).count > 4 { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue) .start(next: { accountPeer in @@ -505,7 +509,20 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat if !phone.hasPrefix("+") { phone = "+\(phone)" } - if phone != phoneNumber { + + var matches = false + if phone == (code + number) { + matches = true + } else { + while number.hasPrefix("0") { + number.removeFirst() + if phone == (code + number) { + matches = true + } + } + } + + if !matches { secondaryActionDisabled = false presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.DeleteAccount_InvalidPhoneNumberError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) return diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index 81d93fe73db..4ca46c872f3 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -290,6 +290,12 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo faqUrl = "https://telegram.org/faq#q-can-i-delete-my-messages" } let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } let resolvedUrlPromise = Promise() resolvedUrlPromise.set(resolvedUrl) @@ -325,6 +331,12 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo faqUrl = "https://telegram.org/faq#general" } let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } let resolvedUrlPromise = Promise() resolvedUrlPromise.set(resolvedUrl) diff --git a/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift index caf368cf73b..a64fdfdd586 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift @@ -188,13 +188,13 @@ class DeleteAccountPhoneItemNode: ListViewItemNode, ItemListItemNode { let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: item.strings) ?? country.name strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: []) - let maskFont = Font.with(size: 20.0, design: .regular, traits: [.monospacedNumbers]) + let maskFont = Font.with(size: 17.0, design: .regular, traits: [.monospacedNumbers]) if let mask = AuthorizationSequenceCountrySelectionController.lookupPatternByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode).flatMap({ NSAttributedString(string: $0, font: maskFont, textColor: item.theme.list.itemPlaceholderTextColor) }) { strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = nil strongSelf.phoneInputNode.mask = mask } else { strongSelf.phoneInputNode.mask = nil - strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: item.strings.Login_PhonePlaceholder, font: Font.regular(20.0), textColor: item.theme.list.itemPlaceholderTextColor) + strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: item.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) } return true } else { @@ -269,6 +269,10 @@ class DeleteAccountPhoneItemNode: ListViewItemNode, ItemListItemNode { return self.phoneInputNode.number } + var codeNumberAndFullNumber: (String, String, String) { + return self.phoneInputNode.codeNumberAndFullNumber + } + func updateCountryCode() { self.phoneInputNode.codeAndNumber = self.phoneInputNode.codeAndNumber } @@ -397,7 +401,7 @@ class DeleteAccountPhoneItemNode: ListViewItemNode, ItemListItemNode { strongSelf.phoneInputNode.frame = phoneInputFrame strongSelf.phoneInputNode.countryCodeField.frame = countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) strongSelf.phoneInputNode.numberField.frame = numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) - strongSelf.phoneInputNode.placeholderNode.frame = placeholderFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY) + strongSelf.phoneInputNode.placeholderNode.frame = placeholderFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY + 4.0 + UIScreenPixel) } }) } diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index cb61de7e195..e8b399efe74 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -732,8 +732,10 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } strongSelf.applyingCode.set(.single(info.languageCode)) strongSelf.applyDisposable.set((strongSelf.context.engine.localization.downloadAndApplyLocalization(accountManager: strongSelf.context.sharedContext.accountManager, languageCode: info.languageCode) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).start(completed: { [weak self] in self?.applyingCode.set(.single(nil)) + + self?.context.engine.messages.refreshAttachMenuBots() })) } if info.isOfficial { diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 63128bbadb5..8fb11718742 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -197,6 +197,12 @@ public func logoutOptionsController(context: AccountContext, navigationControlle faqUrl = "https://telegram.org/faq#general" } let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } let resolvedUrlPromise = Promise() resolvedUrlPromise.set(resolvedUrl) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift index 855d3bfc456..f5ad9800547 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift @@ -377,7 +377,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { text = nil } if let text = text { - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil), elevatedLayout: false, action: { _ in return false })) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false })) } })) } @@ -426,7 +426,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil), elevatedLayout: false, action: { _ in return false })) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false })) })) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) } @@ -476,7 +476,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted, timeout: nil), elevatedLayout: false, action: { _ in return false })) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false })) })) } dismissAction() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 54a23f53117..22ab048ed3a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -103,8 +103,8 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { self.containerNode = ASDisplayNode() self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - self.tooltipContainerNode = ContextMenuContainerNode(blurred: false) - self.tooltipContainerNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) + self.tooltipContainerNode = ContextMenuContainerNode(isBlurred: false, isDark: true) + self.tooltipContainerNode.containerNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8) self.textNode = ImmediateTextNode() self.textNode.isUserInteractionEnabled = false @@ -119,7 +119,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { self.addSubnode(self.containerNode) - self.tooltipContainerNode.addSubnode(self.textNode) + self.tooltipContainerNode.containerNode.addSubnode(self.textNode) self.addSubnode(self.tooltipContainerNode) } @@ -133,9 +133,9 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { return { item, params, neighbors in if currentBackgroundNode == nil { currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + currentBackgroundNode?.update(wallpaper: item.wallpaper) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) } - currentBackgroundNode?.update(wallpaper: item.wallpaper) - currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) let insets: UIEdgeInsets let separatorHeight = UIScreenPixel @@ -145,7 +145,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { var peers = SimpleDictionary() let messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: item.peerName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: item.peerName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: []) @@ -189,6 +189,11 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + if let currentBackgroundNode { + currentBackgroundNode.update(wallpaper: item.wallpaper) + currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + } + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) var topOffset: CGFloat = 8.0 diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 188374b8302..3c686730abb 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -951,7 +951,7 @@ public func privacyAndSecurityController( hapticFeedback.impact() var alreadyPresented = false - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in if action == .info { if !alreadyPresented { let controller = PremiumIntroScreen(context: context, source: .settings) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift index a4640d3867f..53754a8eac9 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift @@ -613,7 +613,7 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } })) - let contextMenuController = ContextMenuController(actions: actions) + let contextMenuController = makeContextMenuController(actions: actions) self.controller?.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (node, node.bounds.insetBy(dx: 0.0, dy: -2.0), strongSelf, strongSelf.view.bounds) diff --git a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift index 6d9eb02631c..5c3ddeec9e1 100644 --- a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift +++ b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift @@ -345,11 +345,8 @@ public func quickReactionSetupController( animationCache: context.animationCache, animationRenderer: context.animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: true, - isEmojiSelection: false, + subject: .quickReaction, hasTrending: false, - isQuickReactionSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index e3791455ad1..33ffc6a2fc7 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -261,10 +261,10 @@ class ReactionChatPreviewItemNode: ListViewItemNode { return { item, params, neighbors in if currentBackgroundNode == nil { currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + currentBackgroundNode?.update(wallpaper: item.wallpaper) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) } - currentBackgroundNode?.update(wallpaper: item.wallpaper) - currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) - + let insets: UIEdgeInsets let separatorHeight = UIScreenPixel @@ -274,7 +274,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { var peers = SimpleDictionary() let messages = SimpleDictionary() - peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) let messageText = item.strings.Settings_QuickReactionSetup_DemoMessageText @@ -330,6 +330,11 @@ class ReactionChatPreviewItemNode: ListViewItemNode { strongSelf.item = item + if let currentBackgroundNode { + currentBackgroundNode.update(wallpaper: item.wallpaper) + currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + } + if strongSelf.genericReactionEffectDisposable == nil { strongSelf.loadNextGenericReactionEffect(context: item.context) } diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index fa6bbb2bddb..6ec4032274e 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -732,7 +732,14 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openStickersBot: { - resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") |> deliverOnMainQueue).start(next: { peer in + resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { peer in if let peer = peer { navigateToChatControllerImpl?(peer.id) } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 7c06b37f1a8..ca37f1a9d8a 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -304,14 +304,14 @@ 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) let timestamp = self.referenceTimestamp @@ -427,8 +427,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) 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: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) @@ -436,7 +436,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index 1fbbbe59c96..c4513612c64 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -537,7 +537,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll |> take(1)).start(next: { previewTheme, settings in let saveThemeTemplateFile: (String, LocalFileMediaResource, @escaping () -> Void) -> Void = { title, resource, completion in let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")]) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: context.account.peerId, messages: [message]).start() diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 6b5749db51c..cdf27920f1d 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -937,12 +937,12 @@ 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)) - 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)) - 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)) - 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)) - 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)) - 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) let timestamp = self.referenceTimestamp @@ -1030,8 +1030,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) var sampleMessages: [Message] = [] @@ -1048,7 +1048,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate messages[message4.id] = message4 sampleMessages.append(message4) - let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages[message5.id] = message5 sampleMessages.append(message5) @@ -1059,7 +1059,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) - let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message7) let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift index 1f879b764e4..7d849797f99 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift @@ -469,6 +469,12 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { return .single(nil) } return context.engine.peers.resolvePeerByName(name: name) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in if let peer = peer { return .single(peer._asPeer()) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index a79f4d6c36a..e7540c31c9c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -452,15 +452,15 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let chatListPresentationData = ChatListPresentationData(theme: self.previewTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) - 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) let timestamp = self.referenceTimestamp @@ -581,8 +581,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) var sampleMessages: [Message] = [] @@ -599,7 +599,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { messages[message4.id] = message4 sampleMessages.append(message4) - let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages[message5.id] = message5 sampleMessages.append(message5) @@ -610,7 +610,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) - let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message7) let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index 2e7d5b3a82b..a329a970dc8 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -155,11 +155,11 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) if let (author, text) = messageItem.reply { - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil) messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) } - let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, isCentered: false)) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index d0cdb5c54f9..81a873fc874 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -18,6 +18,7 @@ import AccountContext import ContextUI import UndoUI import PremiumUI +import PeerNameColorScreen func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { let name: String @@ -50,6 +51,7 @@ private final class ThemeSettingsControllerArguments { let selectTheme: (PresentationThemeReference) -> Void let openThemeSettings: () -> Void let openWallpaperSettings: () -> Void + let openNameColorSettings: () -> Void let selectAccentColor: (PresentationThemeAccentColor?) -> Void let openAccentColorPicker: (PresentationThemeReference, Bool) -> Void let toggleNightTheme: (Bool) -> Void @@ -64,11 +66,12 @@ private final class ThemeSettingsControllerArguments { let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, openPowerSavingSettings: @escaping () -> Void, openStickersAndEmoji: @escaping () -> Void, toggleShowNextMediaOnTap: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) { + init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, openNameColorSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, openPowerSavingSettings: @escaping () -> Void, openStickersAndEmoji: @escaping () -> Void, toggleShowNextMediaOnTap: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) { self.context = context self.selectTheme = selectTheme self.openThemeSettings = openThemeSettings self.openWallpaperSettings = openWallpaperSettings + self.openNameColorSettings = openNameColorSettings self.selectAccentColor = selectAccentColor self.openAccentColorPicker = openAccentColorPicker self.toggleNightTheme = toggleNightTheme @@ -119,6 +122,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case themes(PresentationTheme, PresentationStrings, [PresentationThemeReference], PresentationThemeReference, Bool, [String: [StickerPackItem]], [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper]) case chatTheme(PresentationTheme, String) case wallpaper(PresentationTheme, String) + case nameColor(PresentationTheme, String, String, UIColor) case autoNight(PresentationTheme, String, Bool, Bool) case autoNightTheme(PresentationTheme, String, String) case textSize(PresentationTheme, String, String) @@ -133,7 +137,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .themeListHeader, .chatPreview, .themes, .chatTheme, .wallpaper: + case .themeListHeader, .chatPreview, .themes, .chatTheme, .wallpaper, .nameColor: return ThemeSettingsControllerSection.chatPreview.rawValue case .autoNight, .autoNightTheme: return ThemeSettingsControllerSection.nightMode.rawValue @@ -150,38 +154,40 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { var stableId: Int32 { switch self { - case .themeListHeader: - return 0 - case .chatPreview: - return 1 - case .themes: - return 2 - case .chatTheme: - return 3 - case .wallpaper: - return 4 - case .autoNight: - return 5 - case .autoNightTheme: - return 6 - case .textSize: - return 7 - case .bubbleSettings: - return 8 - case .powerSaving: - return 9 - case .stickersAndEmoji: - return 10 - case .iconHeader: - return 11 - case .iconItem: - return 12 - case .otherHeader: - return 13 - case .showNextMediaOnTap: - return 14 - case .showNextMediaOnTapInfo: - return 15 + case .themeListHeader: + return 0 + case .chatPreview: + return 1 + case .themes: + return 2 + case .chatTheme: + return 3 + case .wallpaper: + return 4 + case .nameColor: + return 5 + case .autoNight: + return 6 + case .autoNightTheme: + return 7 + case .textSize: + return 8 + case .bubbleSettings: + return 9 + case .powerSaving: + return 10 + case .stickersAndEmoji: + return 11 + case .iconHeader: + return 12 + case .iconItem: + return 13 + case .otherHeader: + return 14 + case .showNextMediaOnTap: + return 15 + case .showNextMediaOnTapInfo: + return 16 } } @@ -211,6 +217,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .nameColor(lhsTheme, lhsText, lhsName, lhsColor): + if case let .nameColor(rhsTheme, rhsText, rhsName, rhsColor) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsName == rhsName, lhsColor == rhsColor { + return true + } else { + return false + } case let .autoNight(lhsTheme, lhsText, lhsValue, lhsEnabled): if case let .autoNight(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true @@ -297,10 +309,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { 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) - }, contextAction: { theme, node, gesture in - arguments.themeContextAction(false, theme, node, gesture) - }, tag: ThemeSettingsEntryTag.theme) + arguments.selectTheme(theme) + }, contextAction: { theme, node, gesture in + arguments.themeContextAction(false, theme, node, gesture) + }, tag: ThemeSettingsEntryTag.theme) case let .chatTheme(_, text): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openThemeSettings() @@ -309,6 +321,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openWallpaperSettings() }) + case let .nameColor(_, text, name, color): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: name, labelStyle: .semitransparentBadge(color), sectionId: self.section, style: .blocks, action: { + arguments.openNameColorSettings() + }) case let .autoNight(_, title, value, enabled): return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleNightTheme(value) @@ -353,7 +369,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } } -private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]]) -> [ThemeSettingsControllerEntry] { +private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], accountPeer: EnginePeer?, nameColors: PeerNameColors) -> [ThemeSettingsControllerEntry] { var entries: [ThemeSettingsControllerEntry] = [] let strings = presentationData.strings @@ -365,6 +381,11 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, entries.append(.chatTheme(presentationData.theme, strings.Settings_ChatThemes)) entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground)) + if let accountPeer { + let colors = nameColors.get(accountPeer.nameColor ?? .blue) + entries.append(.nameColor(presentationData.theme, strings.Appearance_NameColor, accountPeer.compactDisplayTitle, colors.main)) + } + entries.append(.autoNight(presentationData.theme, strings.Appearance_NightTheme, presentationThemeSettings.automaticThemeSwitchSetting.force, !presentationData.autoNightModeTriggered || presentationThemeSettings.automaticThemeSwitchSetting.force)) let autoNightMode: String switch presentationThemeSettings.automaticThemeSwitchSetting.trigger { @@ -488,6 +509,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The pushControllerImpl?(themePickerController(context: context)) }, openWallpaperSettings: { pushControllerImpl?(ThemeGridController(context: context)) + }, openNameColorSettings: { + pushControllerImpl?(PeerNameColorScreen(context: context, subject: .account)) }, selectAccentColor: { accentColor in selectAccentColorImpl?(accentColor) }, openAccentColorPicker: { themeReference, create in @@ -1000,8 +1023,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }) }) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId)) - |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))) + |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings @@ -1042,7 +1065,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The chatThemes.insert(.builtin(.dayClassic), at: 0) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 288b2d3764e..6b721d5afb1 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -1485,8 +1485,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { 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) - 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) + 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) + 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) var topMessageText = "" var bottomMessageText = "" diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 63599c953d0..f89ce097eba 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -1908,15 +1908,19 @@ public final class ShareController: ViewController { } var replyToMessageId: MessageId? + var threadId: Int64? if let topicId = topicIds[peerId] { replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)) + threadId = topicId } var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: url + "\n\n" + text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: url + "\n\n" + text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil) + }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } else { - messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) @@ -1941,15 +1945,17 @@ public final class ShareController: ViewController { } var replyToMessageId: MessageId? + var threadId: Int64? if let topicId = topicIds[peerId] { replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)) + threadId = topicId } var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -1973,18 +1979,20 @@ public final class ShareController: ViewController { } var replyToMessageId: MessageId? + var threadId: Int64? if let topicId = topicIds[peerId] { replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)) + threadId = topicId } var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } let attributedText = NSMutableAttributedString(string: string, attributes: [ChatTextInputAttributes.italic: true as NSNumber]) attributedText.append(NSAttributedString(string: "\n\n\(url)")) let entities = generateChatInputTextEntities(attributedText) - messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2008,12 +2016,14 @@ public final class ShareController: ViewController { } var replyToMessageId: MessageId? + var threadId: Int64? if let topicId = topicIds[peerId] { replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)) + threadId = topicId } var messages: [EnqueueMessage] = [] - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2082,15 +2092,17 @@ public final class ShareController: ViewController { } var replyToMessageId: MessageId? + var threadId: Int64? if let topicId = topicIds[peerId] { replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)) + threadId = topicId } var messages: [EnqueueMessage] = [] if !text.isEmpty && !sendTextAsCaption { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: sendTextAsCaption ? text : "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: sendTextAsCaption ? text : "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2114,15 +2126,17 @@ public final class ShareController: ViewController { } var replyToMessageId: MessageId? + var threadId: Int64? if let topicId = topicIds[peerId] { replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)) + threadId = topicId } var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2154,7 +2168,7 @@ public final class ShareController: ViewController { return .fail(.generic) } - messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } for message in messages { for media in message.media { diff --git a/submodules/StatisticsUI/Sources/BoostsTabsItem.swift b/submodules/StatisticsUI/Sources/BoostsTabsItem.swift new file mode 100644 index 00000000000..c5e30fa02e5 --- /dev/null +++ b/submodules/StatisticsUI/Sources/BoostsTabsItem.swift @@ -0,0 +1,267 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import LegacyComponents +import ItemListUI +import PresentationDataUtils + +final class BoostsTabsItem: ListViewItem, ItemListItem { + enum Tab { + case boosts + case gifts + } + + let theme: PresentationTheme + + let boostsText: String + let giftsText: String + let selectedTab: Tab + + let sectionId: ItemListSectionId + let selectionUpdated: (Tab) -> Void + + init(theme: PresentationTheme, boostsText: String, giftsText: String, selectedTab: Tab, sectionId: ItemListSectionId, selectionUpdated: @escaping (Tab) -> Void) { + self.theme = theme + self.boostsText = boostsText + self.giftsText = giftsText + self.selectedTab = selectedTab + self.sectionId = sectionId + self.selectionUpdated = selectionUpdated + } + + 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 = BoostsTabsItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? BoostsTabsItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +private final class BoostsTabsItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let boostsButton: HighlightTrackingButtonNode + private let boostsTextNode: TextNode + + private let giftsButton: HighlightTrackingButtonNode + private let giftsTextNode: TextNode + + private let selectionNode: ASImageNode + + private var item: BoostsTabsItem? + private var layoutParams: ListViewItemLayoutParams? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.boostsButton = HighlightTrackingButtonNode() + + self.boostsTextNode = TextNode() + self.boostsTextNode.isUserInteractionEnabled = false + self.boostsTextNode.displaysAsynchronously = false + + self.giftsButton = HighlightTrackingButtonNode() + + self.giftsTextNode = TextNode() + self.giftsTextNode.isUserInteractionEnabled = false + self.giftsTextNode.displaysAsynchronously = false + + self.selectionNode = ASImageNode() + self.selectionNode.displaysAsynchronously = false + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.boostsTextNode) + self.addSubnode(self.giftsTextNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.boostsButton) + self.addSubnode(self.giftsButton) + + self.boostsButton.addTarget(self, action: #selector(self.boostsPressed), forControlEvents: .touchUpInside) + self.giftsButton.addTarget(self, action: #selector(self.giftsPressed), forControlEvents: .touchUpInside) + } + + @objc private func boostsPressed() { + if let item = self.item { + item.selectionUpdated(.boosts) + } + } + + @objc private func giftsPressed() { + if let item = self.item { + item.selectionUpdated(.gifts) + } + } + + func asyncLayout() -> (_ item: BoostsTabsItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + let makeBoostsTextLayout = TextNode.asyncLayout(self.boostsTextNode) + let makeGiftsTextLayout = TextNode.asyncLayout(self.giftsTextNode) + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let accentColor = item.theme.list.itemAccentColor + let secondaryColor = item.theme.list.itemSecondaryTextColor + + let (boostsTextLayout, boostsTextApply) = makeBoostsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.boostsText, font: Font.medium(14.0), textColor: item.selectedTab == .boosts ? accentColor : secondaryColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (giftsTextLayout, giftsTextApply) = makeGiftsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.giftsText, font: Font.medium(14.0), textColor: item.selectedTab == .gifts ? accentColor : secondaryColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + contentSize = CGSize(width: params.width, height: 48.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if themeUpdated { + strongSelf.selectionNode.image = generateImage(CGSize(width: 4.0, height: 3.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(item.theme.list.itemAccentColor.cgColor) + + let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width, height: 4.0)), cornerRadius: 2.0) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 0) + } + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + 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.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)) + + let _ = boostsTextApply() + let _ = giftsTextApply() + + strongSelf.boostsTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: boostsTextLayout.size) + strongSelf.giftsTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0 + boostsTextLayout.size.width + 27.0, y: 16.0), size: giftsTextLayout.size) + + strongSelf.boostsButton.frame = strongSelf.boostsTextNode.frame.insetBy(dx: -10.0, dy: -10.0) + strongSelf.giftsButton.frame = strongSelf.giftsTextNode.frame.insetBy(dx: -10.0, dy: -10.0) + + let selectionHeight: CGFloat = 3.0 + let selectionFrame: CGRect + + switch item.selectedTab { + case .boosts: + selectionFrame = CGRect(x: strongSelf.boostsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.boostsTextNode.frame.width, height: selectionHeight) + case .gifts: + selectionFrame = CGRect(x: strongSelf.giftsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.giftsTextNode.frame.width, height: selectionHeight) + } + + var transition: ContainedViewLayoutTransition = .immediate + if let currentItem, currentItem.selectedTab != item.selectedTab { + transition = .animated(duration: 0.3, curve: .spring) + } + transition.updateFrame(node: strongSelf.selectionNode, frame: selectionFrame) + } + }) + } + } + + 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 25ac446003c..6a93e6f1b17 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -20,8 +20,9 @@ import InviteLinksUI import UndoUI import ShareController import ItemListPeerActionItem +import PremiumUI -private let maxUsersDisplayedLimit: Int32 = 50 +private let maxUsersDisplayedLimit: Int32 = 5 private final class ChannelStatsControllerArguments { let context: AccountContext @@ -30,18 +31,24 @@ private final class ChannelStatsControllerArguments { let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void let copyBoostLink: (String) -> Void let shareBoostLink: (String) -> Void - let openPeer: (EnginePeer) -> Void + let openBoost: (ChannelBoostersContext.State.Boost) -> Void let expandBoosters: () -> Void + let openGifts: () -> Void + let createPrepaidGiveaway: (PrepaidGiveaway) -> Void + let updateGiftsSelected: (Bool) -> Void - init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void) { + init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openMessageStats = openMessage self.contextAction = contextAction self.copyBoostLink = copyBoostLink self.shareBoostLink = shareBoostLink - self.openPeer = openPeer + self.openBoost = openBoost self.expandBoosters = expandBoosters + self.openGifts = openGifts + self.createPrepaidGiveaway = createPrepaidGiveaway + self.updateGiftsSelected = updateGiftsSelected } } @@ -59,8 +66,10 @@ private enum StatsSection: Int32 { case instantPageInteractions case boostLevel case boostOverview + case boostPrepaid case boosters case boostLink + case gifts } private enum StatsEntry: ItemListNodeEntry { @@ -102,9 +111,14 @@ private enum StatsEntry: ItemListNodeEntry { case boostOverviewTitle(PresentationTheme, String) case boostOverview(PresentationTheme, ChannelBoostStatus) + case boostPrepaidTitle(PresentationTheme, String) + case boostPrepaid(Int32, PresentationTheme, String, String, PrepaidGiveaway) + case boostPrepaidInfo(PresentationTheme, String) + case boostersTitle(PresentationTheme, String) case boostersPlaceholder(PresentationTheme, String) - case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32) + case boosterTabs(PresentationTheme, String, String, Bool) + case booster(Int32, PresentationTheme, PresentationDateTimeFormat, ChannelBoostersContext.State.Boost) case boostersExpand(PresentationTheme, String) case boostersInfo(PresentationTheme, String) @@ -112,6 +126,9 @@ private enum StatsEntry: ItemListNodeEntry { case boostLink(PresentationTheme, String) case boostLinkInfo(PresentationTheme, String) + case gifts(PresentationTheme, String) + case giftsInfo(PresentationTheme, String) + var section: ItemListSectionId { switch self { case .overviewTitle, .overview: @@ -140,10 +157,14 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.boostLevel.rawValue case .boostOverviewTitle, .boostOverview: return StatsSection.boostOverview.rawValue - case .boostersTitle, .boostersPlaceholder, .booster, .boostersExpand, .boostersInfo: + case .boostPrepaidTitle, .boostPrepaid, .boostPrepaidInfo: + return StatsSection.boostPrepaid.rawValue + case .boostersTitle, .boostersPlaceholder, .boosterTabs, .booster, .boostersExpand, .boostersInfo: return StatsSection.boosters.rawValue case .boostLinkTitle, .boostLink, .boostLinkInfo: return StatsSection.boostLink.rawValue + case .gifts, .giftsInfo: + return StatsSection.gifts.rawValue } } @@ -199,12 +220,20 @@ private enum StatsEntry: ItemListNodeEntry { return 2001 case .boostOverview: return 2002 - case .boostersTitle: + case .boostPrepaidTitle: return 2003 + case let .boostPrepaid(index, _, _, _, _): + return 2004 + index + case .boostPrepaidInfo: + return 2100 + case .boostersTitle: + return 2101 case .boostersPlaceholder: - return 2004 - case let .booster(index, _, _, _, _): - return 2005 + index + return 2102 + case .boosterTabs: + return 2103 + case let .booster(index, _, _, _): + return 2104 + index case .boostersExpand: return 10000 case .boostersInfo: @@ -215,6 +244,10 @@ private enum StatsEntry: ItemListNodeEntry { return 10003 case .boostLinkInfo: return 10004 + case .gifts: + return 10005 + case .giftsInfo: + return 10006 } } @@ -370,6 +403,24 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case let .boostPrepaidTitle(lhsTheme, lhsText): + if case let .boostPrepaidTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .boostPrepaid(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsPrepaidGiveaway): + if case let .boostPrepaid(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsPrepaidGiveaway) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsPrepaidGiveaway == rhsPrepaidGiveaway { + return true + } else { + return false + } + case let .boostPrepaidInfo(lhsTheme, lhsText): + if case let .boostPrepaidInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .boostersTitle(lhsTheme, lhsText): if case let .boostersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -382,8 +433,14 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsExpires): - if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsExpires) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsExpires == rhsExpires { + case let .boosterTabs(lhsTheme, lhsBoostText, lhsGiftText, lhsGiftSelected): + if case let .boosterTabs(rhsTheme, rhsBoostText, rhsGiftText, rhsGiftSelected) = rhs, lhsTheme === rhsTheme, lhsBoostText == rhsBoostText, lhsGiftText == rhsGiftText, lhsGiftSelected == rhsGiftSelected { + return true + } else { + return false + } + case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsBoost): + if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsBoost) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsBoost == rhsBoost { return true } else { return false @@ -418,6 +475,18 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case let .gifts(lhsTheme, lhsText): + if case let .gifts(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .giftsInfo(lhsTheme, lhsText): + if case let .giftsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -441,11 +510,14 @@ private enum StatsEntry: ItemListNodeEntry { let .postsTitle(_, text), let .instantPageInteractionsTitle(_, text), let .boostOverviewTitle(_, text), + let .boostPrepaidTitle(_, text), let .boostersTitle(_, text), let .boostLinkTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .boostersInfo(_, text), - let .boostLinkInfo(_, text): + case let .boostPrepaidInfo(_, text), + let .boostersInfo(_, text), + let .boostLinkInfo(_, text), + let .giftsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .overview(_, stats): return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks) @@ -472,11 +544,64 @@ private enum StatsEntry: ItemListNodeEntry { }, contextAction: { node, gesture in arguments.contextAction(message.id, node, gesture) }) - case let .booster(_, _, dateTimeFormat, peer, expires): - let expiresValue = stringForMediumDate(timestamp: expires, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { - arguments.openPeer(peer) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) + case let .boosterTabs(_, boostText, giftText, giftSelected): + return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in + arguments.updateGiftsSelected(tab == .gifts) + }) + case let .booster(_, _, _, boost): + let count = boost.multiplier + let expiresValue = stringForDate(timestamp: boost.expires, strings: presentationData.strings) + let expiresString: String + + let durationMonths = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0))) + let durationString = presentationData.strings.Stats_Boosts_ShortMonth("\(durationMonths)").string + + let title: String + let icon: GiftOptionItem.Icon + var label: String? + if boost.flags.contains(.isGiveaway) { + label = "🏆 \(presentationData.strings.Stats_Boosts_Giveaway)" + } else if boost.flags.contains(.isGift) { + label = "🎁 \(presentationData.strings.Stats_Boosts_Gift)" + } + + let color: GiftOptionItem.Icon.Color + if durationMonths > 11 { + color = .red + } else if durationMonths > 5 { + color = .blue + } else { + color = .green + } + + if boost.flags.contains(.isUnclaimed) { + title = presentationData.strings.Stats_Boosts_Unclaimed + icon = .image(color: color, name: "Premium/Unclaimed") + expiresString = "\(durationString) • \(expiresValue)" + } else if let peer = boost.peer { + title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + icon = .peer(peer) + if let _ = label { + expiresString = expiresValue + } else { + expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string + } + } else { + if boost.flags.contains(.isUnclaimed) { + title = presentationData.strings.Stats_Boosts_Unclaimed + icon = .image(color: color, name: "Premium/Unclaimed") + } else if boost.flags.contains(.isGiveaway) { + title = presentationData.strings.Stats_Boosts_ToBeDistributed + icon = .image(color: color, name: "Premium/ToBeDistributed") + } else { + title = "Unknown" + icon = .image(color: color, name: "Premium/ToBeDistributed") + } + expiresString = "\(durationString) • \(expiresValue)" + } + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: icon, title: title, titleFont: .bold, titleBadge: count > 1 ? "\(count)" : nil, subtitle: expiresString, label: label.flatMap { .semitransparent($0) }, sectionId: self.section, action: { + arguments.openBoost(boost) + }) case let .boostersExpand(theme, title): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandBoosters() @@ -496,6 +621,25 @@ private enum StatsEntry: ItemListNodeEntry { }, contextAction: nil, viewAction: nil, tag: nil) case let .boostersPlaceholder(_, text): return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks) + case let .gifts(theme, title): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addBoostsIcon(theme), title: title, sectionId: self.section, editing: false, action: { + arguments.openGifts() + }) + case let .boostPrepaid(_, _, title, subtitle, prepaidGiveaway): + let color: GiftOptionItem.Icon.Color + switch prepaidGiveaway.months { + case 3: + color = .green + case 6: + color = .blue + case 12: + color = .red + default: + color = .blue + } + return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity * 4)", subtitle: subtitle, label: nil, sectionId: self.section, action: { + arguments.createPrepaidGiveaway(prepaidGiveaway) + }) } } } @@ -508,15 +652,18 @@ public enum ChannelStatsSection { private struct ChannelStatsControllerState: Equatable { let section: ChannelStatsSection let boostersExpanded: Bool + let giftsSelected: Bool init() { self.section = .stats self.boostersExpanded = false + self.giftsSelected = false } - init(section: ChannelStatsSection, boostersExpanded: Bool) { + init(section: ChannelStatsSection, boostersExpanded: Bool, giftsSelected: Bool) { self.section = section self.boostersExpanded = boostersExpanded + self.giftsSelected = giftsSelected } static func ==(lhs: ChannelStatsControllerState, rhs: ChannelStatsControllerState) -> Bool { @@ -526,20 +673,27 @@ private struct ChannelStatsControllerState: Equatable { if lhs.boostersExpanded != rhs.boostersExpanded { return false } + if lhs.giftsSelected != rhs.giftsSelected { + return false + } return true } func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded) + return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, giftsSelected: self.giftsSelected) } func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded) + return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, giftsSelected: self.giftsSelected) + } + + func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState { + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, giftsSelected: giftsSelected) } } -private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, interactions: [MessageId: ChannelStatsMessageInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, presentationData: PresentationData) -> [StatsEntry] { +private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, interactions: [MessageId: ChannelStatsMessageInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, presentationData: PresentationData, giveawayAvailable: Bool) -> [StatsEntry] { var entries: [StatsEntry] = [] switch state.section { @@ -620,15 +774,25 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader)) entries.append(.boostOverview(presentationData.theme, boostData)) + if !boostData.prepaidGiveaways.isEmpty { + entries.append(.boostPrepaidTitle(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysTitle)) + var i: Int32 = 0 + for giveaway in boostData.prepaidGiveaways { + entries.append(.boostPrepaid(i, presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawayCount(giveaway.quantity), presentationData.strings.Stats_Boosts_PrepaidGiveawayMonths("\(giveaway.months)").string, giveaway)) + i += 1 + } + entries.append(.boostPrepaidInfo(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysInfo)) + } + let boostersTitle: String let boostersPlaceholder: String? let boostersFooter: String? if let boostersState, boostersState.count > 0 { - boostersTitle = presentationData.strings.Stats_Boosts_Boosters(boostersState.count) + boostersTitle = presentationData.strings.Stats_Boosts_Boosts(boostersState.count) boostersPlaceholder = nil boostersFooter = presentationData.strings.Stats_Boosts_BoostersInfo } else { - boostersTitle = presentationData.strings.Stats_Boosts_BoostersNone + boostersTitle = presentationData.strings.Stats_Boosts_BoostsNone boostersPlaceholder = presentationData.strings.Stats_Boosts_NoBoostersYet boostersFooter = nil } @@ -638,10 +802,30 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p entries.append(.boostersPlaceholder(presentationData.theme, boostersPlaceholder)) } + var boostsCount: Int32 = 0 if let boostersState { + boostsCount = boostersState.count + } + var giftsCount: Int32 = 0 + if let giftsState { + giftsCount = giftsState.count + } + + if boostsCount > 0 && giftsCount > 0 && boostsCount != giftsCount { + entries.append(.boosterTabs(presentationData.theme, presentationData.strings.Stats_Boosts_TabBoosts(boostsCount), presentationData.strings.Stats_Boosts_TabGifts(giftsCount), state.giftsSelected)) + } + + let selectedState: ChannelBoostersContext.State? + if state.giftsSelected { + selectedState = giftsState + } else { + selectedState = boostersState + } + + if let selectedState { var boosterIndex: Int32 = 0 - var boosters: [ChannelBoostersContext.State.Booster] = boostersState.boosters + var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts var effectiveExpanded = state.boostersExpanded if boosters.count > maxUsersDisplayedLimit && !state.boostersExpanded { boosters = Array(boosters.prefix(Int(maxUsersDisplayedLimit))) @@ -650,12 +834,12 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p } for booster in boosters { - entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster.peer, booster.expires)) + entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster)) boosterIndex += 1 } if !effectiveExpanded { - entries.append(.boostersExpand(presentationData.theme, presentationData.strings.PeopleNearby_ShowMorePeople(Int32(boostersState.count) - maxUsersDisplayedLimit))) + entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(Int32(selectedState.count) - maxUsersDisplayedLimit))) } } @@ -664,18 +848,13 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p } entries.append(.boostLinkTitle(presentationData.theme, presentationData.strings.Stats_Boosts_LinkHeader)) + entries.append(.boostLink(presentationData.theme, boostData.url)) + entries.append(.boostLinkInfo(presentationData.theme, presentationData.strings.Stats_Boosts_LinkInfo)) - if let peer { - let link: String - if let addressName = peer.addressName, !addressName.isEmpty { - link = "t.me/\(addressName)?boost" - } else { - link = "t.me/c/\(peer.id.id._internalGetInt64Value())?boost" - } - entries.append(.boostLink(presentationData.theme, link)) + if giveawayAvailable { + entries.append(.gifts(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoosts)) + entries.append(.giftsInfo(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoostsInfo)) } - - entries.append(.boostLinkInfo(presentationData.theme, presentationData.strings.Stats_Boosts_LinkInfo)) } } @@ -683,12 +862,14 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p } public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController { - let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false), ignoreRepeated: true) - let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false)) + let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + var openMessageStatsImpl: ((MessageId) -> Void)? var contextActionImpl: ((MessageId, ASDisplayNode, ContextGesture?) -> Void)? @@ -721,12 +902,16 @@ public func channelStatsController(context: AccountContext, updatedPresentationD if let boostStatus { boostData = .single(boostStatus) } else { - boostData = context.engine.peers.getChannelBoostStatus(peerId: peerId) + boostData = .single(nil) |> then(context.engine.peers.getChannelBoostStatus(peerId: peerId)) } - let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId) + let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) + let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true) + var dismissAllTooltipsImpl: (() -> Void)? var presentImpl: ((ViewController) -> Void)? - var navigateToProfileImpl: ((EnginePeer) -> Void)? + var pushImpl: ((ViewController) -> Void)? + var navigateToChatImpl: ((EnginePeer) -> Void)? + var navigateToMessageImpl: ((EngineMessage.Id) -> Void)? let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal in return statsContext.loadDetailedGraph(graph, x: x) @@ -735,13 +920,11 @@ public func channelStatsController(context: AccountContext, updatedPresentationD }, contextAction: { messageId, node, gesture in contextActionImpl?(messageId, node, gesture) }, copyBoostLink: { link in - UIPasteboard.general.string = "https://\(link)" + UIPasteboard.general.string = link let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) - }, shareBoostLink: { link in - let link = "https://\(link)" - + }, shareBoostLink: { link in let shareController = ShareController(context: context, subject: .url(link), updatedPresentationData: updatedPresentationData) shareController.completed = { peerIds in let _ = (context.engine.data.get( @@ -783,11 +966,45 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } presentImpl?(shareController) }, - openPeer: { peer in - navigateToProfileImpl?(peer) + openBoost: { boost in + dismissAllTooltipsImpl?() + + if let peer = boost.peer, !boost.flags.contains(.isGiveaway) && !boost.flags.contains(.isGift) { + navigateToChatImpl?(peer) + return + } + + if boost.peer == nil, boost.flags.contains(.isGiveaway) && !boost.flags.contains(.isUnclaimed) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Stats_Boosts_TooltipToBeDistributed, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) + return + } + + let controller = PremiumGiftCodeScreen( + context: context, + subject: .boost(peerId, boost), + action: {}, + openPeer: { peer in + navigateToChatImpl?(peer) + }, + openMessage: { messageId in + navigateToMessageImpl?(messageId) + }) + pushImpl?(controller) }, expandBoosters: { updateState { $0.withUpdatedBoostersExpanded(true) } + }, + openGifts: { + let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) + pushImpl?(controller) + }, + createPrepaidGiveaway: { prepaidGiveaway in + let controller = createGiveawayController(context: context, peerId: peerId, subject: .prepaid(prepaidGiveaway)) + pushImpl?(controller) + }, + updateGiftsSelected: { selected in + updateState { $0.withUpdatedGiftsSelected(selected).withUpdatedBoostersExpanded(false) } }) let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil) @@ -808,11 +1025,12 @@ public func channelStatsController(context: AccountContext, updatedPresentationD dataPromise.get(), messagesPromise.get(), boostData, - boostersContext.state, + boostsContext.state, + giftsContext.state, longLoadingSignal ) |> deliverOnMainQueue - |> map { presentationData, state, peer, data, messageView, boostData, boostersState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, peer, data, messageView, boostData, boostersState, giftsState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in let previous = previousData.swap(data) var emptyStateItem: ItemListControllerEmptyStateItem? switch state.section { @@ -838,11 +1056,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD map[interactions.messageId] = interactions return map } - - - + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Stats_Statistics, presentationData.strings.Stats_Boosts], state.section == .boosts ? 1 : 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(state: state, peer: peer, data: data, messages: messages, interactions: interactions, boostData: boostData, boostersState: boostersState, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(state: state, peer: peer, data: data, messages: messages, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, presentationData: presentationData, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -862,7 +1078,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD controller.visibleBottomContentOffsetChanged = { offset in let state = stateValue.with { $0 } if case let .known(value) = offset, value < 100.0, case .boosts = state.section, state.boostersExpanded { - boostersContext.loadMore() + boostsContext.loadMore() } } controller.titleControlValueChanged = { value in @@ -891,7 +1107,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) } }) }) @@ -900,14 +1116,45 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let contextController = ContextController(presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } + dismissAllTooltipsImpl = { [weak controller] in + if let controller { + controller.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + }) + controller.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + } + } presentImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) } - navigateToProfileImpl = { [weak controller] peer in - if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { - navigationController.pushViewController(controller) + pushImpl = { [weak controller] c in + controller?.push(c) + } + navigateToChatImpl = { [weak controller] peer in + if let navigationController = controller?.navigationController as? NavigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) } } + navigateToMessageImpl = { [weak controller] messageId in + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + if let navigationController = controller?.navigationController as? NavigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) + } + }) + } return controller } diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index 2a84ae83e60..e4bef176be2 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -266,7 +266,7 @@ public func messageStatsController(context: AccountContext, messageId: EngineMes return } if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) } }) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift index 4978f0b836a..3d629bea087 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift @@ -422,6 +422,8 @@ final class StickerPackEmojisItemNode: GridItemNode { itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor case .primary: itemLayer.layerTintColor = theme.list.itemPrimaryTextColor.cgColor + case let .custom(color): + itemLayer.layerTintColor = color.cgColor } var itemFrame = itemLayout.frame(itemIndex: index) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index 3b56f2e42e1..ba685bfdb42 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -136,6 +136,12 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } strongSelf.openMentionDisposable.set((strongSelf.context.engine.peers.resolvePeerByName(name: mention) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in if let peer = peer { return .single(peer._asPeer()) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 911fce41371..08e505d5551 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -2051,7 +2051,7 @@ public final class StickerPackScreenImpl: ViewController { } })) - let contextMenuController = ContextMenuController(actions: actions) + let contextMenuController = makeContextMenuController(actions: actions) strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (node, frame.insetBy(dx: -40.0, dy: 0.0), strongSelf.controllerNode, strongSelf.controllerNode.view.bounds) @@ -2065,6 +2065,12 @@ public final class StickerPackScreenImpl: ViewController { } strongSelf.openMentionDisposable.set((strongSelf.context.engine.peers.resolvePeerByName(name: mention) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in if let peer = peer { return .single(peer._asPeer()) diff --git a/submodules/StringTransliteration/Package.swift b/submodules/StringTransliteration/Package.swift index 1066784488f..4b06d92f789 100644 --- a/submodules/StringTransliteration/Package.swift +++ b/submodules/StringTransliteration/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "StringTransliteration", - platforms: [.macOS(.v10_12)], + 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/TelegramApi/Package.swift b/submodules/TelegramApi/Package.swift index ca9c6e31716..dc1ffdc6716 100644 --- a/submodules/TelegramApi/Package.swift +++ b/submodules/TelegramApi/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "TelegramApi", - platforms: [.macOS(.v10_12)], + 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/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index a36744e986a..cbb6492fe04 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -11,6 +11,7 @@ public enum Api { public enum payments {} public enum phone {} public enum photos {} + public enum premium {} public enum stats {} public enum stickers {} public enum storage {} @@ -32,6 +33,7 @@ public enum Api { public enum payments {} public enum phone {} public enum photos {} + public enum premium {} public enum stats {} public enum stickers {} public enum stories {} @@ -73,7 +75,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1834973166] = { return Api.BaseTheme.parse_baseThemeTinted($0) } dict[-1132882121] = { return Api.Bool.parse_boolFalse($0) } dict[-1720552011] = { return Api.Bool.parse_boolTrue($0) } - dict[245261184] = { return Api.Booster.parse_booster($0) } + dict[706514033] = { return Api.Boost.parse_boost($0) } dict[-1778593322] = { return Api.BotApp.parse_botApp($0) } dict[1571189943] = { return Api.BotApp.parse_botAppNotModified($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } @@ -90,6 +92,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[85477117] = { return Api.BotInlineMessage.parse_botInlineMessageMediaGeo($0) } dict[894081801] = { return Api.BotInlineMessage.parse_botInlineMessageMediaInvoice($0) } dict[-1970903652] = { return Api.BotInlineMessage.parse_botInlineMessageMediaVenue($0) } + dict[-2137335386] = { return Api.BotInlineMessage.parse_botInlineMessageMediaWebPage($0) } dict[-1937807902] = { return Api.BotInlineMessage.parse_botInlineMessageText($0) } dict[400266251] = { return Api.BotInlineResult.parse_botInlineMediaResult($0) } dict[295067450] = { return Api.BotInlineResult.parse_botInlineResult($0) } @@ -101,6 +104,8 @@ 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[1855199800] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeHistoryTTL($0) } dict[84703944] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeLinkedChat($0) } dict[241923758] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeLocation($0) } @@ -161,7 +166,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[-1795845413] = { return Api.Chat.parse_channel($0) } + dict[427944574] = { 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) } @@ -171,7 +176,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1626209256] = { return Api.ChatBannedRights.parse_chatBannedRights($0) } dict[1915758525] = { return Api.ChatFull.parse_channelFull($0) } dict[-908914376] = { return Api.ChatFull.parse_chatFull($0) } - dict[806110401] = { return Api.ChatInvite.parse_chatInvite($0) } + dict[-840897472] = { return Api.ChatInvite.parse_chatInvite($0) } dict[1516793212] = { return Api.ChatInvite.parse_chatInviteAlready($0) } dict[1634294960] = { return Api.ChatInvite.parse_chatInvitePeek($0) } dict[-1940201511] = { return Api.ChatInviteImporter.parse_chatInviteImporter($0) } @@ -211,7 +216,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) } dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) } dict[-745541182] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } - dict[-40996577] = { return Api.DraftMessage.parse_draftMessage($0) } + dict[1070397423] = { return Api.DraftMessage.parse_draftMessage($0) } dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) } dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) } dict[-1842457175] = { return Api.EmailVerification.parse_emailVerificationCode($0) } @@ -279,6 +284,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1768777083] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaGeo($0) } dict[-672693723] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaInvoice($0) } dict[1098628881] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaVenue($0) } + dict[-1109605104] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaWebPage($0) } dict[1036876423] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageText($0) } dict[-1995686519] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID($0) } dict[-1227287081] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID64($0) } @@ -325,6 +331,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) } dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } + dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) } dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) } dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) } @@ -342,6 +349,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1530447553] = { return Api.InputMedia.parse_inputMediaUploadedDocument($0) } dict[505969924] = { return Api.InputMedia.parse_inputMediaUploadedPhoto($0) } dict[-1052959727] = { return Api.InputMedia.parse_inputMediaVenue($0) } + dict[-1038383031] = { return Api.InputMedia.parse_inputMediaWebPage($0) } dict[-1392895362] = { return Api.InputMessage.parse_inputMessageCallbackQuery($0) } dict[-1502174430] = { return Api.InputMessage.parse_inputMessageID($0) } dict[-2037963464] = { return Api.InputMessage.parse_inputMessagePinned($0) } @@ -385,7 +393,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-380694650] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowChatParticipants($0) } dict[195371015] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowContacts($0) } dict[-1877932953] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowUsers($0) } - dict[-1672247580] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) } + dict[121554949] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) } dict[363917955] = { return Api.InputReplyTo.parse_inputReplyToStory($0) } dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) } dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) } @@ -405,6 +413,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } 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[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) } dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } @@ -477,7 +487,9 @@ 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[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } + dict[858499565] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) } dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) } dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($0) } @@ -531,13 +543,14 @@ 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[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) } dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) } dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) } dict[1758159491] = { return Api.MessageMedia.parse_messageMediaStory($0) } dict[-1618676578] = { return Api.MessageMedia.parse_messageMediaUnsupported($0) } dict[784356159] = { return Api.MessageMedia.parse_messageMediaVenue($0) } - dict[-1557277184] = { return Api.MessageMedia.parse_messageMediaWebPage($0) } + dict[-571405253] = { return Api.MessageMedia.parse_messageMediaWebPage($0) } dict[-1938180548] = { return Api.MessagePeerReaction.parse_messagePeerReaction($0) } dict[-1228133028] = { return Api.MessagePeerVote.parse_messagePeerVote($0) } dict[1959634180] = { return Api.MessagePeerVote.parse_messagePeerVoteInputOption($0) } @@ -545,7 +558,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[182649427] = { return Api.MessageRange.parse_messageRange($0) } dict[1328256121] = { return Api.MessageReactions.parse_messageReactions($0) } dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) } - dict[-1495959709] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) } + dict[1860946621] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) } dict[-1667711039] = { return Api.MessageReplyHeader.parse_messageReplyStoryHeader($0) } dict[1163625789] = { return Api.MessageViews.parse_messageViews($0) } dict[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($0) } @@ -565,6 +578,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2129714567] = { return Api.MessagesFilter.parse_inputMessagesFilterUrl($0) } dict[-1614803355] = { return Api.MessagesFilter.parse_inputMessagesFilterVideo($0) } dict[1358283666] = { return Api.MessagesFilter.parse_inputMessagesFilterVoice($0) } + dict[-1001897636] = { return Api.MyBoost.parse_myBoost($0) } dict[-1910892683] = { return Api.NearestDc.parse_nearestDc($0) } dict[-1746354498] = { return Api.NotificationSound.parse_notificationSoundDefault($0) } dict[-2096391452] = { return Api.NotificationSound.parse_notificationSoundLocal($0) } @@ -655,8 +669,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2061444128] = { return Api.PollResults.parse_pollResults($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } dict[512535275] = { return Api.PostAddress.parse_postAddress($0) } + dict[629052971] = { return Api.PremiumGiftCodeOption.parse_premiumGiftCodeOption($0) } dict[1958953753] = { return Api.PremiumGiftOption.parse_premiumGiftOption($0) } dict[1596792306] = { return Api.PremiumSubscriptionOption.parse_premiumSubscriptionOption($0) } + dict[-1303143084] = { return Api.PrepaidGiveaway.parse_prepaidGiveaway($0) } dict[-1534675103] = { return Api.PrivacyKey.parse_privacyKeyAbout($0) } dict[1124062251] = { return Api.PrivacyKey.parse_privacyKeyAddedByPhone($0) } dict[1343122938] = { return Api.PrivacyKey.parse_privacyKeyChatInvite($0) } @@ -946,7 +962,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1886646706] = { return Api.UrlAuthResult.parse_urlAuthResultAccepted($0) } dict[-1445536993] = { return Api.UrlAuthResult.parse_urlAuthResultDefault($0) } dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } - dict[-1414139616] = { return Api.User.parse_user($0) } + dict[-346018011] = { return Api.User.parse_user($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) } dict[-1179571092] = { return Api.UserFull.parse_userFull($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } @@ -968,9 +984,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[475467473] = { return Api.WebDocument.parse_webDocument($0) } dict[-104284986] = { return Api.WebDocument.parse_webDocumentNoProxy($0) } dict[-392411726] = { return Api.WebPage.parse_webPage($0) } - dict[-350980120] = { return Api.WebPage.parse_webPageEmpty($0) } + dict[555358088] = { return Api.WebPage.parse_webPageEmpty($0) } dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) } - dict[-981018084] = { return Api.WebPage.parse_webPagePending($0) } + dict[-1328464313] = { return Api.WebPage.parse_webPagePending($0) } dict[781501415] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) } dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) } dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) } @@ -1142,8 +1158,12 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1821037486] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) } dict[870003448] = { return Api.messages.TranslatedText.parse_translateResult($0) } 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[-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) } dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) } dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } @@ -1160,6 +1180,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[539045032] = { return Api.photos.Photo.parse_photo($0) } dict[-1916114267] = { return Api.photos.Photos.parse_photos($0) } dict[352657236] = { return Api.photos.Photos.parse_photosSlice($0) } + dict[-2030542532] = { return Api.premium.BoostsList.parse_boostsList($0) } + dict[1230586490] = { return Api.premium.BoostsStatus.parse_boostsStatus($0) } + dict[-1696454430] = { return Api.premium.MyBoosts.parse_myBoosts($0) } dict[-1107852396] = { return Api.stats.BroadcastStats.parse_broadcastStats($0) } dict[-276825834] = { return Api.stats.MegagroupStats.parse_megagroupStats($0) } dict[-1986399595] = { return Api.stats.MessageStats.parse_messageStats($0) } @@ -1176,10 +1199,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) } dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } - dict[-203604707] = { return Api.stories.BoostersList.parse_boostersList($0) } - dict[1726619631] = { return Api.stories.BoostsStatus.parse_boostsStatus($0) } - dict[-1021889145] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostOk($0) } - dict[1898726997] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostReplace($0) } dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) } dict[1574486984] = { return Api.stories.Stories.parse_stories($0) } dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($0) } @@ -1289,7 +1308,7 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.Bool: _1.serialize(buffer, boxed) - case let _1 as Api.Booster: + case let _1 as Api.Boost: _1.serialize(buffer, boxed) case let _1 as Api.BotApp: _1.serialize(buffer, boxed) @@ -1589,6 +1608,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.MessagesFilter: _1.serialize(buffer, boxed) + case let _1 as Api.MyBoost: + _1.serialize(buffer, boxed) case let _1 as Api.NearestDc: _1.serialize(buffer, boxed) case let _1 as Api.NotificationSound: @@ -1657,10 +1678,14 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.PostAddress: _1.serialize(buffer, boxed) + case let _1 as Api.PremiumGiftCodeOption: + _1.serialize(buffer, boxed) case let _1 as Api.PremiumGiftOption: _1.serialize(buffer, boxed) case let _1 as Api.PremiumSubscriptionOption: _1.serialize(buffer, boxed) + case let _1 as Api.PrepaidGiveaway: + _1.serialize(buffer, boxed) case let _1 as Api.PrivacyKey: _1.serialize(buffer, boxed) case let _1 as Api.PrivacyRule: @@ -2017,10 +2042,16 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.VotesList: _1.serialize(buffer, boxed) + case let _1 as Api.messages.WebPage: + _1.serialize(buffer, boxed) case let _1 as Api.payments.BankCardData: _1.serialize(buffer, boxed) + case let _1 as Api.payments.CheckedGiftCode: + _1.serialize(buffer, boxed) case let _1 as Api.payments.ExportedInvoice: _1.serialize(buffer, boxed) + case let _1 as Api.payments.GiveawayInfo: + _1.serialize(buffer, boxed) case let _1 as Api.payments.PaymentForm: _1.serialize(buffer, boxed) case let _1 as Api.payments.PaymentReceipt: @@ -2049,6 +2080,12 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.photos.Photos: _1.serialize(buffer, boxed) + case let _1 as Api.premium.BoostsList: + _1.serialize(buffer, boxed) + case let _1 as Api.premium.BoostsStatus: + _1.serialize(buffer, boxed) + case let _1 as Api.premium.MyBoosts: + _1.serialize(buffer, boxed) case let _1 as Api.stats.BroadcastStats: _1.serialize(buffer, boxed) case let _1 as Api.stats.MegagroupStats: @@ -2061,12 +2098,6 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stories.AllStories: _1.serialize(buffer, boxed) - case let _1 as Api.stories.BoostersList: - _1.serialize(buffer, boxed) - case let _1 as Api.stories.BoostsStatus: - _1.serialize(buffer, boxed) - case let _1 as Api.stories.CanApplyBoostResult: - _1.serialize(buffer, boxed) case let _1 as Api.stories.PeerStories: _1.serialize(buffer, boxed) case let _1 as Api.stories.Stories: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index d6409da3852..f223f6a5e62 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -893,37 +893,61 @@ public extension Api { } } public extension Api { - enum Booster: TypeConstructorDescription { - case booster(userId: Int64, expires: Int32) + enum Boost: TypeConstructorDescription { + case boost(flags: Int32, id: String, userId: Int64?, giveawayMsgId: Int32?, date: Int32, expires: Int32, usedGiftSlug: String?, multiplier: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .booster(let userId, let expires): + case .boost(let flags, let id, let userId, let giveawayMsgId, let date, let expires, let usedGiftSlug, let multiplier): if boxed { - buffer.appendInt32(245261184) + buffer.appendInt32(706514033) } - serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(userId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(giveawayMsgId!, buffer: buffer, boxed: false)} + serializeInt32(date, buffer: buffer, boxed: false) serializeInt32(expires, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {serializeString(usedGiftSlug!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {serializeInt32(multiplier!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .booster(let userId, let expires): - return ("booster", [("userId", userId as Any), ("expires", expires as Any)]) + case .boost(let flags, let id, let userId, let giveawayMsgId, let date, let expires, let usedGiftSlug, let multiplier): + return ("boost", [("flags", flags as Any), ("id", id as Any), ("userId", userId as Any), ("giveawayMsgId", giveawayMsgId as Any), ("date", date as Any), ("expires", expires as Any), ("usedGiftSlug", usedGiftSlug as Any), ("multiplier", multiplier as Any)]) } } - public static func parse_booster(_ reader: BufferReader) -> Booster? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() + public static func parse_boost(_ reader: BufferReader) -> Boost? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt64() } + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: String? + if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } + var _8: Int32? + if Int(_1!) & Int(1 << 5) != 0 {_8 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.Booster.booster(userId: _1!, expires: _2!) + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.Boost.boost(flags: _1!, id: _2!, userId: _3, giveawayMsgId: _4, date: _5!, expires: _6!, usedGiftSlug: _7, multiplier: _8) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index c916dec5ee6..2402ce132c7 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1,17 +1,24 @@ public extension Api { indirect enum InputReplyTo: TypeConstructorDescription { - case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?) + case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?) case inputReplyToStory(userId: Api.InputUser, storyId: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId): + case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities): if boxed { - buffer.appendInt32(-1672247580) + buffer.appendInt32(121554949) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(replyToMsgId, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {replyToPeerId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(quoteEntities!.count)) + for item in quoteEntities! { + item.serialize(buffer, true) + }} break case .inputReplyToStory(let userId, let storyId): if boxed { @@ -25,8 +32,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId): - return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any)]) + case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities): + return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any)]) case .inputReplyToStory(let userId, let storyId): return ("inputReplyToStory", [("userId", userId as Any), ("storyId", storyId as Any)]) } @@ -39,11 +46,24 @@ public extension Api { _2 = reader.readInt32() var _3: Int32? if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } + var _4: Api.InputPeer? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputPeer + } } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + var _6: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3) + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3, replyToPeerId: _4, quoteText: _5, quoteEntities: _6) } else { return nil @@ -579,6 +599,8 @@ public extension Api { 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 inputStorePaymentPremiumSubscription(flags: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -591,6 +613,41 @@ public extension Api { serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) break + case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): + if boxed { + buffer.appendInt32(-1551868097) + } + serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)} + 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): + if boxed { + buffer.appendInt32(2090038758) + } + serializeInt32(flags, buffer: buffer, boxed: false) + boostPeer.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(additionalPeers!.count)) + for item in additionalPeers! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(countriesIso2!.count)) + for item in countriesIso2! { + serializeString(item, buffer: buffer, boxed: false) + }} + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(untilDate, buffer: buffer, boxed: false) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break case .inputStorePaymentPremiumSubscription(let flags): if boxed { buffer.appendInt32(-1502273946) @@ -604,6 +661,10 @@ public extension Api { switch self { case .inputStorePaymentGiftPremium(let userId, let currency, let amount): 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 .inputStorePaymentPremiumSubscription(let flags): return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)]) } @@ -628,6 +689,71 @@ public extension Api { return nil } } + public static func parse_inputStorePaymentPremiumGiftCode(_ reader: BufferReader) -> InputStorePaymentPurpose? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.InputUser]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } + var _3: Api.InputPeer? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.InputPeer + } } + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!) + } + else { + return nil + } + } + public static func parse_inputStorePaymentPremiumGiveaway(_ reader: BufferReader) -> InputStorePaymentPurpose? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputPeer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _3: [Api.InputPeer]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) + } } + var _4: [String]? + 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() + 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 _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!) + } + else { + return nil + } + } public static func parse_inputStorePaymentPremiumSubscription(_ reader: BufferReader) -> InputStorePaymentPurpose? { var _1: Int32? _1 = reader.readInt32() @@ -948,57 +1074,3 @@ public extension Api { } } -public extension Api { - enum InputWebDocument: TypeConstructorDescription { - case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputWebDocument(let url, let size, let mimeType, let attributes): - if boxed { - buffer.appendInt32(-1678949555) - } - serializeString(url, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputWebDocument(let url, let size, let mimeType, let attributes): - return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)]) - } - } - - public static func parse_inputWebDocument(_ reader: BufferReader) -> InputWebDocument? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: [Api.DocumentAttribute]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputWebDocument.inputWebDocument(url: _1!, size: _2!, mimeType: _3!, attributes: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 714fc857803..daac038500f 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1,3 +1,57 @@ +public extension Api { + enum InputWebDocument: TypeConstructorDescription { + case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputWebDocument(let url, let size, let mimeType, let attributes): + if boxed { + buffer.appendInt32(-1678949555) + } + serializeString(url, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputWebDocument(let url, let size, let mimeType, let attributes): + return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)]) + } + } + + public static func parse_inputWebDocument(_ reader: BufferReader) -> InputWebDocument? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: [Api.DocumentAttribute]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputWebDocument.inputWebDocument(url: _1!, size: _2!, mimeType: _3!, attributes: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputWebFileLocation: TypeConstructorDescription { case inputWebFileAudioAlbumThumbLocation(flags: Int32, document: Api.InputDocument?, title: String?, performer: String?) @@ -1008,111 +1062,3 @@ public extension Api { } } -public extension Api { - enum LangPackString: TypeConstructorDescription { - case langPackString(key: String, value: String) - case langPackStringDeleted(key: String) - case langPackStringPluralized(flags: Int32, key: String, zeroValue: String?, oneValue: String?, twoValue: String?, fewValue: String?, manyValue: String?, otherValue: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackString(let key, let value): - if boxed { - buffer.appendInt32(-892239370) - } - serializeString(key, buffer: buffer, boxed: false) - serializeString(value, buffer: buffer, boxed: false) - break - case .langPackStringDeleted(let key): - if boxed { - buffer.appendInt32(695856818) - } - serializeString(key, buffer: buffer, boxed: false) - break - case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): - if boxed { - buffer.appendInt32(1816636575) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(key, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(zeroValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(oneValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeString(twoValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeString(fewValue!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {serializeString(manyValue!, buffer: buffer, boxed: false)} - serializeString(otherValue, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackString(let key, let value): - return ("langPackString", [("key", key as Any), ("value", value as Any)]) - case .langPackStringDeleted(let key): - return ("langPackStringDeleted", [("key", key as Any)]) - case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): - return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)]) - } - } - - public static func parse_langPackString(_ reader: BufferReader) -> LangPackString? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.LangPackString.langPackString(key: _1!, value: _2!) - } - else { - return nil - } - } - public static func parse_langPackStringDeleted(_ reader: BufferReader) -> LangPackString? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.LangPackString.langPackStringDeleted(key: _1!) - } - else { - return nil - } - } - public static func parse_langPackStringPluralized(_ reader: BufferReader) -> LangPackString? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: String? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } - var _6: String? - if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } - var _7: String? - if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } - var _8: String? - _8 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.LangPackString.langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 07992a59080..4b6509524cc 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,3 +1,111 @@ +public extension Api { + enum LangPackString: TypeConstructorDescription { + case langPackString(key: String, value: String) + case langPackStringDeleted(key: String) + case langPackStringPluralized(flags: Int32, key: String, zeroValue: String?, oneValue: String?, twoValue: String?, fewValue: String?, manyValue: String?, otherValue: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackString(let key, let value): + if boxed { + buffer.appendInt32(-892239370) + } + serializeString(key, buffer: buffer, boxed: false) + serializeString(value, buffer: buffer, boxed: false) + break + case .langPackStringDeleted(let key): + if boxed { + buffer.appendInt32(695856818) + } + serializeString(key, buffer: buffer, boxed: false) + break + case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): + if boxed { + buffer.appendInt32(1816636575) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(key, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(zeroValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(oneValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(twoValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeString(fewValue!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeString(manyValue!, buffer: buffer, boxed: false)} + serializeString(otherValue, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackString(let key, let value): + return ("langPackString", [("key", key as Any), ("value", value as Any)]) + case .langPackStringDeleted(let key): + return ("langPackStringDeleted", [("key", key as Any)]) + case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): + return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)]) + } + } + + public static func parse_langPackString(_ reader: BufferReader) -> LangPackString? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.LangPackString.langPackString(key: _1!, value: _2!) + } + else { + return nil + } + } + public static func parse_langPackStringDeleted(_ reader: BufferReader) -> LangPackString? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.LangPackString.langPackStringDeleted(key: _1!) + } + else { + return nil + } + } + public static func parse_langPackStringPluralized(_ reader: BufferReader) -> LangPackString? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + var _6: String? + if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } + var _7: String? + if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } + var _8: String? + _8 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.LangPackString.langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!) + } + else { + return nil + } + } + + } +} public extension Api { enum MaskCoords: TypeConstructorDescription { case maskCoords(n: Int32, x: Double, y: Double, zoom: Double) @@ -501,7 +609,9 @@ 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 messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case messageActionGiveawayLaunch case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?) case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionHistoryClear @@ -643,6 +753,15 @@ public extension Api { toId.serialize(buffer, true) serializeInt32(distance, buffer: buffer, boxed: false) break + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug): + if boxed { + buffer.appendInt32(-758129906) + } + 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) + break case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): if boxed { buffer.appendInt32(-935499028) @@ -653,6 +772,12 @@ public extension Api { serializeInt32(months, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + break + case .messageActionGiveawayLaunch: + if boxed { + buffer.appendInt32(858499565) + } + break case .messageActionGroupCall(let flags, let call, let duration): if boxed { @@ -859,8 +984,12 @@ 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 .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: + return ("messageActionGiveawayLaunch", []) case .messageActionGroupCall(let flags, let call, let duration): return ("messageActionGroupCall", [("flags", flags as Any), ("call", call as Any), ("duration", duration as Any)]) case .messageActionGroupCallScheduled(let call, let scheduleDate): @@ -1094,6 +1223,28 @@ public extension Api { return nil } } + public static func parse_messageActionGiftCode(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + 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!) + } + else { + return nil + } + } public static func parse_messageActionGiftPremium(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() @@ -1120,6 +1271,9 @@ public extension Api { return nil } } + public static func parse_messageActionGiveawayLaunch(_ reader: BufferReader) -> MessageAction? { + return Api.MessageAction.messageActionGiveawayLaunch + } public static func parse_messageActionGroupCall(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 609644708dd..bfeed9804ab 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -741,13 +741,14 @@ 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 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) case messageMediaStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?) case messageMediaUnsupported case messageMediaVenue(geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) - case messageMediaWebPage(webpage: Api.WebPage) + case messageMediaWebPage(flags: Int32, webpage: Api.WebPage) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -805,6 +806,25 @@ 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): + if boxed { + buffer.appendInt32(1478887012) + } + serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(channels.count)) + for item in channels { + serializeInt64(item, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(countriesIso2!.count)) + for item in countriesIso2! { + serializeString(item, buffer: buffer, boxed: false) + }} + serializeInt32(quantity, buffer: buffer, boxed: false) + serializeInt32(months, 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) @@ -860,10 +880,11 @@ public extension Api { serializeString(venueId, buffer: buffer, boxed: false) serializeString(venueType, buffer: buffer, boxed: false) break - case .messageMediaWebPage(let webpage): + case .messageMediaWebPage(let flags, let webpage): if boxed { - buffer.appendInt32(-1557277184) + buffer.appendInt32(-571405253) } + serializeInt32(flags, buffer: buffer, boxed: false) webpage.serialize(buffer, true) break } @@ -885,6 +906,8 @@ 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 .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): @@ -897,8 +920,8 @@ public extension Api { return ("messageMediaUnsupported", []) case .messageMediaVenue(let geo, let title, let address, let provider, let venueId, let venueType): return ("messageMediaVenue", [("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)]) - case .messageMediaWebPage(let webpage): - return ("messageMediaWebPage", [("webpage", webpage as Any)]) + case .messageMediaWebPage(let flags, let webpage): + return ("messageMediaWebPage", [("flags", flags as Any), ("webpage", webpage as Any)]) } } @@ -1017,6 +1040,36 @@ public extension Api { return nil } } + public static func parse_messageMediaGiveaway(_ reader: BufferReader) -> MessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Int64]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + var _3: [String]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } } + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + 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 + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.MessageMedia.messageMediaGiveaway(flags: _1!, channels: _2!, countriesIso2: _3, quantity: _4!, months: _5!, untilDate: _6!) + } + else { + return nil + } + } public static func parse_messageMediaInvoice(_ reader: BufferReader) -> MessageMedia? { var _1: Int32? _1 = reader.readInt32() @@ -1149,13 +1202,16 @@ public extension Api { } } public static func parse_messageMediaWebPage(_ reader: BufferReader) -> MessageMedia? { - var _1: Api.WebPage? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.WebPage? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.WebPage + _2 = Api.parse(reader, signature: signature) as? Api.WebPage } let _c1 = _1 != nil - if _c1 { - return Api.MessageMedia.messageMediaWebPage(webpage: _1!) + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageMedia.messageMediaWebPage(flags: _1!, webpage: _2!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index f1ea5107f57..6ccc340764b 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -321,20 +321,28 @@ public extension Api { } } public extension Api { - enum MessageReplyHeader: TypeConstructorDescription { - case messageReplyHeader(flags: Int32, replyToMsgId: Int32, replyToPeerId: Api.Peer?, replyToTopId: Int32?) + indirect enum MessageReplyHeader: TypeConstructorDescription { + case messageReplyHeader(flags: Int32, replyToMsgId: Int32?, replyToPeerId: Api.Peer?, replyFrom: Api.MessageFwdHeader?, replyMedia: Api.MessageMedia?, replyToTopId: Int32?, quoteText: String?, quoteEntities: [Api.MessageEntity]?) case messageReplyStoryHeader(userId: Int64, storyId: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyToTopId): + case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyFrom, let replyMedia, let replyToTopId, let quoteText, let quoteEntities): if boxed { - buffer.appendInt32(-1495959709) + buffer.appendInt32(1860946621) } serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(replyToMsgId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(replyToMsgId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {replyToPeerId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 5) != 0 {replyFrom!.serialize(buffer, true)} + if Int(flags) & Int(1 << 8) != 0 {replyMedia!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(replyToTopId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 6) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 7) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(quoteEntities!.count)) + for item in quoteEntities! { + item.serialize(buffer, true) + }} break case .messageReplyStoryHeader(let userId, let storyId): if boxed { @@ -348,8 +356,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyToTopId): - return ("messageReplyHeader", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("replyToTopId", replyToTopId as Any)]) + case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyFrom, let replyMedia, let replyToTopId, let quoteText, let quoteEntities): + return ("messageReplyHeader", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("replyFrom", replyFrom as Any), ("replyMedia", replyMedia as Any), ("replyToTopId", replyToTopId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any)]) case .messageReplyStoryHeader(let userId, let storyId): return ("messageReplyStoryHeader", [("userId", userId as Any), ("storyId", storyId as Any)]) } @@ -359,19 +367,37 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - _2 = reader.readInt32() + if Int(_1!) & Int(1 << 4) != 0 {_2 = reader.readInt32() } var _3: Api.Peer? if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_4 = reader.readInt32() } + var _4: Api.MessageFwdHeader? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader + } } + var _5: Api.MessageMedia? + if Int(_1!) & Int(1 << 8) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.MessageMedia + } } + var _6: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() } + var _7: String? + if Int(_1!) & Int(1 << 6) != 0 {_7 = parseString(reader) } + var _8: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.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 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.MessageReplyHeader.messageReplyHeader(flags: _1!, replyToMsgId: _2!, replyToPeerId: _3, replyToTopId: _4) + let _c4 = (Int(_1!) & Int(1 << 5) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 8) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.MessageReplyHeader.messageReplyHeader(flags: _1!, replyToMsgId: _2, replyToPeerId: _3, replyFrom: _4, replyMedia: _5, replyToTopId: _6, quoteText: _7, quoteEntities: _8) } else { return nil @@ -672,6 +698,64 @@ public extension Api { } } +public extension Api { + enum MyBoost: TypeConstructorDescription { + case myBoost(flags: Int32, slot: Int32, peer: Api.Peer?, date: Int32, expires: Int32, cooldownUntilDate: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .myBoost(let flags, let slot, let peer, let date, let expires, let cooldownUntilDate): + if boxed { + buffer.appendInt32(-1001897636) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(slot, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {peer!.serialize(buffer, true)} + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(expires, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(cooldownUntilDate!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .myBoost(let flags, let slot, let peer, let date, let expires, let cooldownUntilDate): + return ("myBoost", [("flags", flags as Any), ("slot", slot as Any), ("peer", peer as Any), ("date", date as Any), ("expires", expires as Any), ("cooldownUntilDate", cooldownUntilDate as Any)]) + } + } + + public static func parse_myBoost(_ reader: BufferReader) -> MyBoost? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.Peer? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.MyBoost.myBoost(flags: _1!, slot: _2!, peer: _3, date: _4!, expires: _5!, cooldownUntilDate: _6) + } + else { + return nil + } + } + + } +} public extension Api { enum NearestDc: TypeConstructorDescription { case nearestDc(country: String, thisDc: Int32, nearestDc: Int32) diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index c50124ce4cf..ef702e0207c 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -750,6 +750,66 @@ public extension Api { } } +public extension Api { + enum PremiumGiftCodeOption: TypeConstructorDescription { + case premiumGiftCodeOption(flags: Int32, users: Int32, months: Int32, storeProduct: String?, storeQuantity: Int32?, currency: String, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct, let storeQuantity, let currency, let amount): + if boxed { + buffer.appendInt32(629052971) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(users, buffer: buffer, boxed: false) + serializeInt32(months, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(storeProduct!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(storeQuantity!, buffer: buffer, boxed: false)} + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct, let storeQuantity, let currency, let amount): + return ("premiumGiftCodeOption", [("flags", flags as Any), ("users", users as Any), ("months", months as Any), ("storeProduct", storeProduct as Any), ("storeQuantity", storeQuantity as Any), ("currency", currency as Any), ("amount", amount as Any)]) + } + } + + public static func parse_premiumGiftCodeOption(_ reader: BufferReader) -> PremiumGiftCodeOption? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } + var _5: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } + var _6: String? + _6 = parseString(reader) + var _7: Int64? + _7 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.PremiumGiftCodeOption.premiumGiftCodeOption(flags: _1!, users: _2!, months: _3!, storeProduct: _4, storeQuantity: _5, currency: _6!, amount: _7!) + } + else { + return nil + } + } + + } +} public extension Api { enum PremiumGiftOption: TypeConstructorDescription { case premiumGiftOption(flags: Int32, months: Int32, currency: String, amount: Int64, botUrl: String, storeProduct: String?) @@ -866,6 +926,54 @@ public extension Api { } } +public extension Api { + enum PrepaidGiveaway: TypeConstructorDescription { + case prepaidGiveaway(id: Int64, months: Int32, quantity: Int32, date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .prepaidGiveaway(let id, let months, let quantity, let date): + if boxed { + buffer.appendInt32(-1303143084) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt32(months, buffer: buffer, boxed: false) + serializeInt32(quantity, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .prepaidGiveaway(let id, let months, let quantity, let date): + return ("prepaidGiveaway", [("id", id as Any), ("months", months as Any), ("quantity", quantity as Any), ("date", date as Any)]) + } + } + + public static func parse_prepaidGiveaway(_ reader: BufferReader) -> PrepaidGiveaway? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.PrepaidGiveaway.prepaidGiveaway(id: _1!, months: _2!, quantity: _3!, date: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum PrivacyKey: TypeConstructorDescription { case privacyKeyAbout diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index d64f7a61e95..70febe0a838 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -77,6 +77,7 @@ public extension Api { case botInlineMessageMediaGeo(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?, replyMarkup: Api.ReplyMarkup?) case botInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, currency: String, totalAmount: Int64, replyMarkup: Api.ReplyMarkup?) case botInlineMessageMediaVenue(flags: Int32, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) + case botInlineMessageMediaWebPage(flags: Int32, message: String, entities: [Api.MessageEntity]?, url: String, replyMarkup: Api.ReplyMarkup?) case botInlineMessageText(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -141,6 +142,20 @@ public extension Api { serializeString(venueType, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} break + case .botInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + if boxed { + buffer.appendInt32(-2137335386) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + serializeString(url, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break case .botInlineMessageText(let flags, let message, let entities, let replyMarkup): if boxed { buffer.appendInt32(-1937807902) @@ -169,6 +184,8 @@ public extension Api { return ("botInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageMediaVenue(let flags, let geo, let title, let address, let provider, let venueId, let venueType, let replyMarkup): return ("botInlineMessageMediaVenue", [("flags", flags as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) + case .botInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + return ("botInlineMessageMediaWebPage", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("url", url as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageText(let flags, let message, let entities, let replyMarkup): return ("botInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) } @@ -325,6 +342,33 @@ public extension Api { return nil } } + public static func parse_botInlineMessageMediaWebPage(_ reader: BufferReader) -> BotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _4: String? + _4 = parseString(reader) + var _5: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.BotInlineMessage.botInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5) + } + else { + return nil + } + } public static func parse_botInlineMessageText(_ reader: BufferReader) -> BotInlineMessage? { var _1: Int32? _1 = reader.readInt32() @@ -680,6 +724,8 @@ 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 channelAdminLogEventActionChangeHistoryTTL(prevValue: Int32, newValue: Int32) case channelAdminLogEventActionChangeLinkedChat(prevValue: Int64, newValue: Int64) case channelAdminLogEventActionChangeLocation(prevValue: Api.ChannelLocation, newValue: Api.ChannelLocation) @@ -738,6 +784,20 @@ public extension Api { prevValue.serialize(buffer, true) newValue.serialize(buffer, true) break + case .channelAdminLogEventActionChangeBackgroundEmoji(let prevValue, let newValue): + if boxed { + buffer.appendInt32(1147126836) + } + 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) + break case .channelAdminLogEventActionChangeHistoryTTL(let prevValue, let newValue): if boxed { buffer.appendInt32(1855199800) @@ -1020,6 +1080,10 @@ 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 .channelAdminLogEventActionChangeHistoryTTL(let prevValue, let newValue): return ("channelAdminLogEventActionChangeHistoryTTL", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeLinkedChat(let prevValue, let newValue): @@ -1137,6 +1201,34 @@ 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!) + } + else { + return nil + } + } + 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!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionChangeHistoryTTL(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index b99387ca58d..b99155ae2cf 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -452,14 +452,14 @@ public extension Api { } public extension Api { enum User: TypeConstructorDescription { - case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?) + case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Int32?, backgroundEmojiId: Int64?) case userEmpty(id: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId): + case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let backgroundEmojiId): if boxed { - buffer.appendInt32(-1414139616) + buffer.appendInt32(-346018011) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -486,6 +486,8 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags2) & Int(1 << 5) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 7) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 6) != 0 {serializeInt64(backgroundEmojiId!, buffer: buffer, boxed: false)} break case .userEmpty(let id): if boxed { @@ -498,8 +500,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId): - return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any)]) + case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let backgroundEmojiId): + return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("backgroundEmojiId", backgroundEmojiId as Any)]) case .userEmpty(let id): return ("userEmpty", [("id", id as Any)]) } @@ -550,6 +552,10 @@ public extension Api { } } var _17: Int32? if Int(_2!) & Int(1 << 5) != 0 {_17 = reader.readInt32() } + var _18: Int32? + if Int(_2!) & Int(1 << 7) != 0 {_18 = reader.readInt32() } + var _19: Int64? + if Int(_2!) & Int(1 << 6) != 0 {_19 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -567,8 +573,10 @@ public extension Api { let _c15 = (Int(_1!) & Int(1 << 30) == 0) || _15 != nil let _c16 = (Int(_2!) & Int(1 << 0) == 0) || _16 != nil let _c17 = (Int(_2!) & Int(1 << 5) == 0) || _17 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { - return Api.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17) + let _c18 = (Int(_2!) & Int(1 << 7) == 0) || _18 != nil + let _c19 = (Int(_2!) & Int(1 << 6) == 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.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, backgroundEmojiId: _19) } else { return nil @@ -1369,9 +1377,9 @@ public extension Api { public extension Api { enum WebPage: TypeConstructorDescription { case webPage(flags: Int32, id: Int64, url: String, displayUrl: String, hash: Int32, type: String?, siteName: String?, title: String?, description: String?, photo: Api.Photo?, embedUrl: String?, embedType: String?, embedWidth: Int32?, embedHeight: Int32?, duration: Int32?, author: String?, document: Api.Document?, cachedPage: Api.Page?, attributes: [Api.WebPageAttribute]?) - case webPageEmpty(id: Int64) + case webPageEmpty(flags: Int32, id: Int64, url: String?) case webPageNotModified(flags: Int32, cachedPageViews: Int32?) - case webPagePending(id: Int64, date: Int32) + case webPagePending(flags: Int32, id: Int64, url: String?, date: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -1403,11 +1411,13 @@ public extension Api { item.serialize(buffer, true) }} break - case .webPageEmpty(let id): + case .webPageEmpty(let flags, let id, let url): if boxed { - buffer.appendInt32(-350980120) + buffer.appendInt32(555358088) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(url!, buffer: buffer, boxed: false)} break case .webPageNotModified(let flags, let cachedPageViews): if boxed { @@ -1416,11 +1426,13 @@ public extension Api { serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(cachedPageViews!, buffer: buffer, boxed: false)} break - case .webPagePending(let id, let date): + case .webPagePending(let flags, let id, let url, let date): if boxed { - buffer.appendInt32(-981018084) + buffer.appendInt32(-1328464313) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(url!, buffer: buffer, boxed: false)} serializeInt32(date, buffer: buffer, boxed: false) break } @@ -1430,12 +1442,12 @@ public extension Api { switch self { case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): return ("webPage", [("flags", flags as Any), ("id", id as Any), ("url", url as Any), ("displayUrl", displayUrl as Any), ("hash", hash as Any), ("type", type as Any), ("siteName", siteName as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("embedUrl", embedUrl as Any), ("embedType", embedType as Any), ("embedWidth", embedWidth as Any), ("embedHeight", embedHeight as Any), ("duration", duration as Any), ("author", author as Any), ("document", document as Any), ("cachedPage", cachedPage as Any), ("attributes", attributes as Any)]) - case .webPageEmpty(let id): - return ("webPageEmpty", [("id", id as Any)]) + case .webPageEmpty(let flags, let id, let url): + return ("webPageEmpty", [("flags", flags as Any), ("id", id as Any), ("url", url as Any)]) case .webPageNotModified(let flags, let cachedPageViews): return ("webPageNotModified", [("flags", flags as Any), ("cachedPageViews", cachedPageViews as Any)]) - case .webPagePending(let id, let date): - return ("webPagePending", [("id", id as Any), ("date", date as Any)]) + case .webPagePending(let flags, let id, let url, let date): + return ("webPagePending", [("flags", flags as Any), ("id", id as Any), ("url", url as Any), ("date", date as Any)]) } } @@ -1513,11 +1525,17 @@ public extension Api { } } public static func parse_webPageEmpty(_ reader: BufferReader) -> WebPage? { - var _1: Int64? - _1 = reader.readInt64() + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } let _c1 = _1 != nil - if _c1 { - return Api.WebPage.webPageEmpty(id: _1!) + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.WebPage.webPageEmpty(flags: _1!, id: _2!, url: _3) } else { return nil @@ -1538,14 +1556,20 @@ public extension Api { } } public static func parse_webPagePending(_ reader: BufferReader) -> WebPage? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: Int32? + _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.WebPage.webPagePending(id: _1!, date: _2!) + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.WebPage.webPagePending(flags: _1!, id: _2!, url: _3, date: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index f647d8d535f..15ed48860f2 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -432,6 +432,64 @@ public extension Api.messages { } } +public extension Api.messages { + enum WebPage: TypeConstructorDescription { + case webPage(webpage: Api.WebPage, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webPage(let webpage, let chats, let users): + if boxed { + buffer.appendInt32(-44166467) + } + webpage.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 .webPage(let webpage, let chats, let users): + return ("webPage", [("webpage", webpage as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_webPage(_ reader: BufferReader) -> WebPage? { + var _1: Api.WebPage? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.WebPage + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.WebPage.webPage(webpage: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum BankCardData: TypeConstructorDescription { case bankCardData(title: String, openUrls: [Api.BankCardOpenUrl]) @@ -478,6 +536,88 @@ 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]) + + 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) + } + serializeInt32(flags, buffer: buffer, boxed: false) + 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) + serializeInt32(months, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(usedDate!, buffer: buffer, boxed: false)} + 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 .checkedGiftCode(let flags, let fromId, let giveawayMsgId, let toId, let date, let months, let usedDate, let chats, let users): + return ("checkedGiftCode", [("flags", flags as Any), ("fromId", fromId as Any), ("giveawayMsgId", giveawayMsgId as Any), ("toId", toId as Any), ("date", date as Any), ("months", months as Any), ("usedDate", usedDate as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_checkedGiftCode(_ reader: BufferReader) -> CheckedGiftCode? { + 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 _3: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_3 = reader.readInt32() } + var _4: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt64() } + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_7 = reader.readInt32() } + var _8: [Api.Chat]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _9: [Api.User]? + if let _ = reader.readInt32() { + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _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 + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + 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!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum ExportedInvoice: TypeConstructorDescription { case exportedInvoice(url: String) @@ -514,6 +654,98 @@ public extension Api.payments { } } +public extension Api.payments { + enum GiveawayInfo: TypeConstructorDescription { + case giveawayInfo(flags: Int32, startDate: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?, disallowedCountry: String?) + case giveawayInfoResults(flags: Int32, startDate: Int32, giftCodeSlug: String?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .giveawayInfo(let flags, let startDate, let joinedTooEarlyDate, let adminDisallowedChatId, let disallowedCountry): + if boxed { + buffer.appendInt32(1130879648) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(startDate, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(joinedTooEarlyDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt64(adminDisallowedChatId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeString(disallowedCountry!, buffer: buffer, boxed: false)} + break + case .giveawayInfoResults(let flags, let startDate, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount): + if boxed { + buffer.appendInt32(13456752) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(startDate, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(giftCodeSlug!, buffer: buffer, boxed: false)} + serializeInt32(finishDate, buffer: buffer, boxed: false) + serializeInt32(winnersCount, buffer: buffer, boxed: false) + serializeInt32(activatedCount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .giveawayInfo(let flags, let startDate, let joinedTooEarlyDate, let adminDisallowedChatId, let disallowedCountry): + return ("giveawayInfo", [("flags", flags as Any), ("startDate", startDate as Any), ("joinedTooEarlyDate", joinedTooEarlyDate as Any), ("adminDisallowedChatId", adminDisallowedChatId as Any), ("disallowedCountry", disallowedCountry as Any)]) + case .giveawayInfoResults(let flags, let startDate, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount): + return ("giveawayInfoResults", [("flags", flags as Any), ("startDate", startDate as Any), ("giftCodeSlug", giftCodeSlug as Any), ("finishDate", finishDate as Any), ("winnersCount", winnersCount as Any), ("activatedCount", activatedCount as Any)]) + } + } + + public static func parse_giveawayInfo(_ reader: BufferReader) -> GiveawayInfo? { + 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: Int64? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt64() } + var _5: String? + if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) } + 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 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.payments.GiveawayInfo.giveawayInfo(flags: _1!, startDate: _2!, joinedTooEarlyDate: _3, adminDisallowedChatId: _4, disallowedCountry: _5) + } + else { + return nil + } + } + public static func parse_giveawayInfoResults(_ reader: BufferReader) -> GiveawayInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.payments.GiveawayInfo.giveawayInfoResults(flags: _1!, startDate: _2!, giftCodeSlug: _3, finishDate: _4!, winnersCount: _5!, activatedCount: _6!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum PaymentForm: TypeConstructorDescription { case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, additionalMethods: [Api.PaymentFormMethod]?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: [Api.PaymentSavedCredentials]?, users: [Api.User]) @@ -1402,317 +1634,3 @@ public extension Api.photos { } } -public extension Api.stats { - enum BroadcastStats: TypeConstructorDescription { - case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, recentMessageInteractions: [Api.MessageInteractionCounters]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions): - if boxed { - buffer.appendInt32(-1107852396) - } - period.serialize(buffer, true) - followers.serialize(buffer, true) - viewsPerPost.serialize(buffer, true) - sharesPerPost.serialize(buffer, true) - enabledNotifications.serialize(buffer, true) - growthGraph.serialize(buffer, true) - followersGraph.serialize(buffer, true) - muteGraph.serialize(buffer, true) - topHoursGraph.serialize(buffer, true) - interactionsGraph.serialize(buffer, true) - ivInteractionsGraph.serialize(buffer, true) - viewsBySourceGraph.serialize(buffer, true) - newFollowersBySourceGraph.serialize(buffer, true) - languagesGraph.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(recentMessageInteractions.count)) - for item in recentMessageInteractions { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions): - return ("broadcastStats", [("period", period as Any), ("followers", followers as Any), ("viewsPerPost", viewsPerPost as Any), ("sharesPerPost", sharesPerPost as Any), ("enabledNotifications", enabledNotifications as Any), ("growthGraph", growthGraph as Any), ("followersGraph", followersGraph as Any), ("muteGraph", muteGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("interactionsGraph", interactionsGraph as Any), ("ivInteractionsGraph", ivInteractionsGraph as Any), ("viewsBySourceGraph", viewsBySourceGraph as Any), ("newFollowersBySourceGraph", newFollowersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("recentMessageInteractions", recentMessageInteractions as Any)]) - } - } - - public static func parse_broadcastStats(_ reader: BufferReader) -> BroadcastStats? { - var _1: Api.StatsDateRangeDays? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays - } - var _2: Api.StatsAbsValueAndPrev? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev - } - var _3: Api.StatsAbsValueAndPrev? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev - } - var _4: Api.StatsAbsValueAndPrev? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev - } - var _5: Api.StatsPercentValue? - if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue - } - var _6: Api.StatsGraph? - if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _7: Api.StatsGraph? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _8: Api.StatsGraph? - if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _9: Api.StatsGraph? - if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _10: Api.StatsGraph? - if let signature = reader.readInt32() { - _10 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _11: Api.StatsGraph? - if let signature = reader.readInt32() { - _11 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _12: Api.StatsGraph? - if let signature = reader.readInt32() { - _12 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _13: Api.StatsGraph? - if let signature = reader.readInt32() { - _13 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _14: Api.StatsGraph? - if let signature = reader.readInt32() { - _14 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _15: [Api.MessageInteractionCounters]? - if let _ = reader.readInt32() { - _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageInteractionCounters.self) - } - 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 - let _c8 = _8 != nil - let _c9 = _9 != nil - let _c10 = _10 != nil - let _c11 = _11 != nil - let _c12 = _12 != nil - let _c13 = _13 != nil - let _c14 = _14 != nil - let _c15 = _15 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { - return Api.stats.BroadcastStats.broadcastStats(period: _1!, followers: _2!, viewsPerPost: _3!, sharesPerPost: _4!, enabledNotifications: _5!, growthGraph: _6!, followersGraph: _7!, muteGraph: _8!, topHoursGraph: _9!, interactionsGraph: _10!, ivInteractionsGraph: _11!, viewsBySourceGraph: _12!, newFollowersBySourceGraph: _13!, languagesGraph: _14!, recentMessageInteractions: _15!) - } - else { - return nil - } - } - - } -} -public extension Api.stats { - enum MegagroupStats: TypeConstructorDescription { - case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, weekdaysGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users): - if boxed { - buffer.appendInt32(-276825834) - } - period.serialize(buffer, true) - members.serialize(buffer, true) - messages.serialize(buffer, true) - viewers.serialize(buffer, true) - posters.serialize(buffer, true) - growthGraph.serialize(buffer, true) - membersGraph.serialize(buffer, true) - newMembersBySourceGraph.serialize(buffer, true) - languagesGraph.serialize(buffer, true) - messagesGraph.serialize(buffer, true) - actionsGraph.serialize(buffer, true) - topHoursGraph.serialize(buffer, true) - weekdaysGraph.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(topPosters.count)) - for item in topPosters { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(topAdmins.count)) - for item in topAdmins { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(topInviters.count)) - for item in topInviters { - 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 .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users): - return ("megagroupStats", [("period", period as Any), ("members", members as Any), ("messages", messages as Any), ("viewers", viewers as Any), ("posters", posters as Any), ("growthGraph", growthGraph as Any), ("membersGraph", membersGraph as Any), ("newMembersBySourceGraph", newMembersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("messagesGraph", messagesGraph as Any), ("actionsGraph", actionsGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("weekdaysGraph", weekdaysGraph as Any), ("topPosters", topPosters as Any), ("topAdmins", topAdmins as Any), ("topInviters", topInviters as Any), ("users", users as Any)]) - } - } - - public static func parse_megagroupStats(_ reader: BufferReader) -> MegagroupStats? { - var _1: Api.StatsDateRangeDays? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays - } - var _2: Api.StatsAbsValueAndPrev? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev - } - var _3: Api.StatsAbsValueAndPrev? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev - } - var _4: Api.StatsAbsValueAndPrev? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev - } - var _5: Api.StatsAbsValueAndPrev? - if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev - } - var _6: Api.StatsGraph? - if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _7: Api.StatsGraph? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _8: Api.StatsGraph? - if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _9: Api.StatsGraph? - if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _10: Api.StatsGraph? - if let signature = reader.readInt32() { - _10 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _11: Api.StatsGraph? - if let signature = reader.readInt32() { - _11 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _12: Api.StatsGraph? - if let signature = reader.readInt32() { - _12 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _13: Api.StatsGraph? - if let signature = reader.readInt32() { - _13 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _14: [Api.StatsGroupTopPoster]? - if let _ = reader.readInt32() { - _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopPoster.self) - } - var _15: [Api.StatsGroupTopAdmin]? - if let _ = reader.readInt32() { - _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopAdmin.self) - } - var _16: [Api.StatsGroupTopInviter]? - if let _ = reader.readInt32() { - _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopInviter.self) - } - var _17: [Api.User]? - if let _ = reader.readInt32() { - _17 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - 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 - let _c8 = _8 != nil - let _c9 = _9 != nil - let _c10 = _10 != nil - let _c11 = _11 != nil - let _c12 = _12 != nil - let _c13 = _13 != nil - let _c14 = _14 != nil - let _c15 = _15 != nil - let _c16 = _16 != nil - let _c17 = _17 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { - return Api.stats.MegagroupStats.megagroupStats(period: _1!, members: _2!, messages: _3!, viewers: _4!, posters: _5!, growthGraph: _6!, membersGraph: _7!, newMembersBySourceGraph: _8!, languagesGraph: _9!, messagesGraph: _10!, actionsGraph: _11!, topHoursGraph: _12!, weekdaysGraph: _13!, topPosters: _14!, topAdmins: _15!, topInviters: _16!, users: _17!) - } - else { - return nil - } - } - - } -} -public extension Api.stats { - enum MessageStats: TypeConstructorDescription { - case messageStats(viewsGraph: Api.StatsGraph) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageStats(let viewsGraph): - if boxed { - buffer.appendInt32(-1986399595) - } - viewsGraph.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messageStats(let viewsGraph): - return ("messageStats", [("viewsGraph", viewsGraph as Any)]) - } - } - - public static func parse_messageStats(_ reader: BufferReader) -> MessageStats? { - var _1: Api.StatsGraph? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - let _c1 = _1 != nil - if _c1 { - return Api.stats.MessageStats.messageStats(viewsGraph: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index b7979ad0895..e4681269b70 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,529 @@ +public extension Api.premium { + enum BoostsList: TypeConstructorDescription { + case boostsList(flags: Int32, count: Int32, boosts: [Api.Boost], nextOffset: String?, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boostsList(let flags, let count, let boosts, let nextOffset, let users): + if boxed { + buffer.appendInt32(-2030542532) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(boosts.count)) + for item in boosts { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + 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 .boostsList(let flags, let count, let boosts, let nextOffset, let users): + return ("boostsList", [("flags", flags as Any), ("count", count as Any), ("boosts", boosts as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) + } + } + + public static func parse_boostsList(_ reader: BufferReader) -> BoostsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.Boost]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Boost.self) + } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.premium.BoostsList.boostsList(flags: _1!, count: _2!, boosts: _3!, nextOffset: _4, users: _5!) + } + else { + return nil + } + } + + } +} +public extension Api.premium { + enum BoostsStatus: TypeConstructorDescription { + case boostsStatus(flags: Int32, level: Int32, currentLevelBoosts: Int32, boosts: Int32, giftBoosts: Int32?, nextLevelBoosts: Int32?, premiumAudience: Api.StatsPercentValue?, boostUrl: String, prepaidGiveaways: [Api.PrepaidGiveaway]?, myBoostSlots: [Int32]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let giftBoosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let prepaidGiveaways, let myBoostSlots): + if boxed { + buffer.appendInt32(1230586490) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(level, buffer: buffer, boxed: false) + serializeInt32(currentLevelBoosts, buffer: buffer, boxed: false) + serializeInt32(boosts, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(giftBoosts!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextLevelBoosts!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {premiumAudience!.serialize(buffer, true)} + serializeString(boostUrl, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(prepaidGiveaways!.count)) + for item in prepaidGiveaways! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(myBoostSlots!.count)) + for item in myBoostSlots! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let giftBoosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let prepaidGiveaways, let myBoostSlots): + return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("currentLevelBoosts", currentLevelBoosts as Any), ("boosts", boosts as Any), ("giftBoosts", giftBoosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any), ("premiumAudience", premiumAudience as Any), ("boostUrl", boostUrl as Any), ("prepaidGiveaways", prepaidGiveaways as Any), ("myBoostSlots", myBoostSlots as Any)]) + } + } + + public static func parse_boostsStatus(_ reader: BufferReader) -> BoostsStatus? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } + var _6: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } + var _7: Api.StatsPercentValue? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue + } } + var _8: String? + _8 = parseString(reader) + var _9: [Api.PrepaidGiveaway]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrepaidGiveaway.self) + } } + var _10: [Int32]? + if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() { + _10 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 2) == 0) || _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.premium.BoostsStatus.boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, giftBoosts: _5, nextLevelBoosts: _6, premiumAudience: _7, boostUrl: _8!, prepaidGiveaways: _9, myBoostSlots: _10) + } + else { + return nil + } + } + + } +} +public extension Api.premium { + enum MyBoosts: TypeConstructorDescription { + case myBoosts(myBoosts: [Api.MyBoost], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .myBoosts(let myBoosts, let chats, let users): + if boxed { + buffer.appendInt32(-1696454430) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(myBoosts.count)) + for item in myBoosts { + 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 .myBoosts(let myBoosts, let chats, let users): + return ("myBoosts", [("myBoosts", myBoosts as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_myBoosts(_ reader: BufferReader) -> MyBoosts? { + var _1: [Api.MyBoost]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MyBoost.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.premium.MyBoosts.myBoosts(myBoosts: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.stats { + enum BroadcastStats: TypeConstructorDescription { + case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, recentMessageInteractions: [Api.MessageInteractionCounters]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions): + if boxed { + buffer.appendInt32(-1107852396) + } + period.serialize(buffer, true) + followers.serialize(buffer, true) + viewsPerPost.serialize(buffer, true) + sharesPerPost.serialize(buffer, true) + enabledNotifications.serialize(buffer, true) + growthGraph.serialize(buffer, true) + followersGraph.serialize(buffer, true) + muteGraph.serialize(buffer, true) + topHoursGraph.serialize(buffer, true) + interactionsGraph.serialize(buffer, true) + ivInteractionsGraph.serialize(buffer, true) + viewsBySourceGraph.serialize(buffer, true) + newFollowersBySourceGraph.serialize(buffer, true) + languagesGraph.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(recentMessageInteractions.count)) + for item in recentMessageInteractions { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions): + return ("broadcastStats", [("period", period as Any), ("followers", followers as Any), ("viewsPerPost", viewsPerPost as Any), ("sharesPerPost", sharesPerPost as Any), ("enabledNotifications", enabledNotifications as Any), ("growthGraph", growthGraph as Any), ("followersGraph", followersGraph as Any), ("muteGraph", muteGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("interactionsGraph", interactionsGraph as Any), ("ivInteractionsGraph", ivInteractionsGraph as Any), ("viewsBySourceGraph", viewsBySourceGraph as Any), ("newFollowersBySourceGraph", newFollowersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("recentMessageInteractions", recentMessageInteractions as Any)]) + } + } + + public static func parse_broadcastStats(_ reader: BufferReader) -> BroadcastStats? { + var _1: Api.StatsDateRangeDays? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays + } + var _2: Api.StatsAbsValueAndPrev? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev + } + var _3: Api.StatsAbsValueAndPrev? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev + } + var _4: Api.StatsAbsValueAndPrev? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev + } + var _5: Api.StatsPercentValue? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue + } + var _6: Api.StatsGraph? + if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _7: Api.StatsGraph? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _8: Api.StatsGraph? + if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _9: Api.StatsGraph? + if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _10: Api.StatsGraph? + if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _11: Api.StatsGraph? + if let signature = reader.readInt32() { + _11 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _12: Api.StatsGraph? + if let signature = reader.readInt32() { + _12 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _13: Api.StatsGraph? + if let signature = reader.readInt32() { + _13 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _14: Api.StatsGraph? + if let signature = reader.readInt32() { + _14 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _15: [Api.MessageInteractionCounters]? + if let _ = reader.readInt32() { + _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageInteractionCounters.self) + } + 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 + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + let _c11 = _11 != nil + let _c12 = _12 != nil + let _c13 = _13 != nil + let _c14 = _14 != nil + let _c15 = _15 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { + return Api.stats.BroadcastStats.broadcastStats(period: _1!, followers: _2!, viewsPerPost: _3!, sharesPerPost: _4!, enabledNotifications: _5!, growthGraph: _6!, followersGraph: _7!, muteGraph: _8!, topHoursGraph: _9!, interactionsGraph: _10!, ivInteractionsGraph: _11!, viewsBySourceGraph: _12!, newFollowersBySourceGraph: _13!, languagesGraph: _14!, recentMessageInteractions: _15!) + } + else { + return nil + } + } + + } +} +public extension Api.stats { + enum MegagroupStats: TypeConstructorDescription { + case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, weekdaysGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users): + if boxed { + buffer.appendInt32(-276825834) + } + period.serialize(buffer, true) + members.serialize(buffer, true) + messages.serialize(buffer, true) + viewers.serialize(buffer, true) + posters.serialize(buffer, true) + growthGraph.serialize(buffer, true) + membersGraph.serialize(buffer, true) + newMembersBySourceGraph.serialize(buffer, true) + languagesGraph.serialize(buffer, true) + messagesGraph.serialize(buffer, true) + actionsGraph.serialize(buffer, true) + topHoursGraph.serialize(buffer, true) + weekdaysGraph.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(topPosters.count)) + for item in topPosters { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(topAdmins.count)) + for item in topAdmins { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(topInviters.count)) + for item in topInviters { + 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 .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users): + return ("megagroupStats", [("period", period as Any), ("members", members as Any), ("messages", messages as Any), ("viewers", viewers as Any), ("posters", posters as Any), ("growthGraph", growthGraph as Any), ("membersGraph", membersGraph as Any), ("newMembersBySourceGraph", newMembersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("messagesGraph", messagesGraph as Any), ("actionsGraph", actionsGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("weekdaysGraph", weekdaysGraph as Any), ("topPosters", topPosters as Any), ("topAdmins", topAdmins as Any), ("topInviters", topInviters as Any), ("users", users as Any)]) + } + } + + public static func parse_megagroupStats(_ reader: BufferReader) -> MegagroupStats? { + var _1: Api.StatsDateRangeDays? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays + } + var _2: Api.StatsAbsValueAndPrev? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev + } + var _3: Api.StatsAbsValueAndPrev? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev + } + var _4: Api.StatsAbsValueAndPrev? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev + } + var _5: Api.StatsAbsValueAndPrev? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev + } + var _6: Api.StatsGraph? + if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _7: Api.StatsGraph? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _8: Api.StatsGraph? + if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _9: Api.StatsGraph? + if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _10: Api.StatsGraph? + if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _11: Api.StatsGraph? + if let signature = reader.readInt32() { + _11 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _12: Api.StatsGraph? + if let signature = reader.readInt32() { + _12 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _13: Api.StatsGraph? + if let signature = reader.readInt32() { + _13 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _14: [Api.StatsGroupTopPoster]? + if let _ = reader.readInt32() { + _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopPoster.self) + } + var _15: [Api.StatsGroupTopAdmin]? + if let _ = reader.readInt32() { + _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopAdmin.self) + } + var _16: [Api.StatsGroupTopInviter]? + if let _ = reader.readInt32() { + _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopInviter.self) + } + var _17: [Api.User]? + if let _ = reader.readInt32() { + _17 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + 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 + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + let _c11 = _11 != nil + let _c12 = _12 != nil + let _c13 = _13 != nil + let _c14 = _14 != nil + let _c15 = _15 != nil + let _c16 = _16 != nil + let _c17 = _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.stats.MegagroupStats.megagroupStats(period: _1!, members: _2!, messages: _3!, viewers: _4!, posters: _5!, growthGraph: _6!, membersGraph: _7!, newMembersBySourceGraph: _8!, languagesGraph: _9!, messagesGraph: _10!, actionsGraph: _11!, topHoursGraph: _12!, weekdaysGraph: _13!, topPosters: _14!, topAdmins: _15!, topInviters: _16!, users: _17!) + } + else { + return nil + } + } + + } +} +public extension Api.stats { + enum MessageStats: TypeConstructorDescription { + case messageStats(viewsGraph: Api.StatsGraph) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageStats(let viewsGraph): + if boxed { + buffer.appendInt32(-1986399595) + } + viewsGraph.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageStats(let viewsGraph): + return ("messageStats", [("viewsGraph", viewsGraph as Any)]) + } + } + + public static func parse_messageStats(_ reader: BufferReader) -> MessageStats? { + var _1: Api.StatsGraph? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + let _c1 = _1 != nil + if _c1 { + return Api.stats.MessageStats.messageStats(viewsGraph: _1!) + } + else { + return nil + } + } + + } +} public extension Api.stickers { enum SuggestedShortName: TypeConstructorDescription { case suggestedShortName(shortName: String) @@ -280,188 +806,6 @@ public extension Api.stories { } } -public extension Api.stories { - enum BoostersList: TypeConstructorDescription { - case boostersList(flags: Int32, count: Int32, boosters: [Api.Booster], nextOffset: String?, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .boostersList(let flags, let count, let boosters, let nextOffset, let users): - if boxed { - buffer.appendInt32(-203604707) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(boosters.count)) - for item in boosters { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - 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 .boostersList(let flags, let count, let boosters, let nextOffset, let users): - return ("boostersList", [("flags", flags as Any), ("count", count as Any), ("boosters", boosters as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) - } - } - - public static func parse_boostersList(_ reader: BufferReader) -> BoostersList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.Booster]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Booster.self) - } - var _4: String? - if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.stories.BoostersList.boostersList(flags: _1!, count: _2!, boosters: _3!, nextOffset: _4, users: _5!) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum BoostsStatus: TypeConstructorDescription { - case boostsStatus(flags: Int32, level: Int32, currentLevelBoosts: Int32, boosts: Int32, nextLevelBoosts: Int32?, premiumAudience: Api.StatsPercentValue?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience): - if boxed { - buffer.appendInt32(1726619631) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(level, buffer: buffer, boxed: false) - serializeInt32(currentLevelBoosts, buffer: buffer, boxed: false) - serializeInt32(boosts, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextLevelBoosts!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {premiumAudience!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience): - return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("currentLevelBoosts", currentLevelBoosts as Any), ("boosts", boosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any), ("premiumAudience", premiumAudience as Any)]) - } - } - - public static func parse_boostsStatus(_ reader: BufferReader) -> BoostsStatus? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } - var _6: Api.StatsPercentValue? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _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.stories.BoostsStatus.boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, nextLevelBoosts: _5, premiumAudience: _6) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum CanApplyBoostResult: TypeConstructorDescription { - case canApplyBoostOk - case canApplyBoostReplace(currentBoost: Api.Peer, chats: [Api.Chat]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .canApplyBoostOk: - if boxed { - buffer.appendInt32(-1021889145) - } - - break - case .canApplyBoostReplace(let currentBoost, let chats): - if boxed { - buffer.appendInt32(1898726997) - } - currentBoost.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .canApplyBoostOk: - return ("canApplyBoostOk", []) - case .canApplyBoostReplace(let currentBoost, let chats): - return ("canApplyBoostReplace", [("currentBoost", currentBoost as Any), ("chats", chats as Any)]) - } - } - - public static func parse_canApplyBoostOk(_ reader: BufferReader) -> CanApplyBoostResult? { - return Api.stories.CanApplyBoostResult.canApplyBoostOk - } - public static func parse_canApplyBoostReplace(_ reader: BufferReader) -> CanApplyBoostResult? { - var _1: Api.Peer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stories.CanApplyBoostResult.canApplyBoostReplace(currentBoost: _1!, chats: _2!) - } - else { - return nil - } - } - - } -} public extension Api.stories { enum PeerStories: TypeConstructorDescription { case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) @@ -707,7 +1051,7 @@ public extension Api.stories { } } public extension Api.updates { - enum ChannelDifference: TypeConstructorDescription { + 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]) @@ -1190,91 +1534,3 @@ public extension Api.upload { } } -public extension Api.upload { - enum File: TypeConstructorDescription { - case file(type: Api.storage.FileType, mtime: Int32, bytes: Buffer) - case fileCdnRedirect(dcId: Int32, fileToken: Buffer, encryptionKey: Buffer, encryptionIv: Buffer, fileHashes: [Api.FileHash]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .file(let type, let mtime, let bytes): - if boxed { - buffer.appendInt32(157948117) - } - type.serialize(buffer, true) - serializeInt32(mtime, buffer: buffer, boxed: false) - serializeBytes(bytes, buffer: buffer, boxed: false) - break - case .fileCdnRedirect(let dcId, let fileToken, let encryptionKey, let encryptionIv, let fileHashes): - if boxed { - buffer.appendInt32(-242427324) - } - serializeInt32(dcId, buffer: buffer, boxed: false) - serializeBytes(fileToken, buffer: buffer, boxed: false) - serializeBytes(encryptionKey, buffer: buffer, boxed: false) - serializeBytes(encryptionIv, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(fileHashes.count)) - for item in fileHashes { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .file(let type, let mtime, let bytes): - return ("file", [("type", type as Any), ("mtime", mtime as Any), ("bytes", bytes as Any)]) - case .fileCdnRedirect(let dcId, let fileToken, let encryptionKey, let encryptionIv, let fileHashes): - return ("fileCdnRedirect", [("dcId", dcId as Any), ("fileToken", fileToken as Any), ("encryptionKey", encryptionKey as Any), ("encryptionIv", encryptionIv as Any), ("fileHashes", fileHashes as Any)]) - } - } - - public static func parse_file(_ reader: BufferReader) -> File? { - var _1: Api.storage.FileType? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.storage.FileType - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Buffer? - _3 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.upload.File.file(type: _1!, mtime: _2!, bytes: _3!) - } - else { - return nil - } - } - public static func parse_fileCdnRedirect(_ reader: BufferReader) -> File? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Buffer? - _2 = parseBytes(reader) - var _3: Buffer? - _3 = parseBytes(reader) - var _4: Buffer? - _4 = parseBytes(reader) - var _5: [Api.FileHash]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FileHash.self) - } - 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.upload.File.fileCdnRedirect(dcId: _1!, fileToken: _2!, encryptionKey: _3!, encryptionIv: _4!, fileHashes: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index e715e24eebf..b3d8cbead55 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?) + 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: Int32?, backgroundEmojiId: Int64?) 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): + 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 backgroundEmojiId): if boxed { - buffer.appendInt32(-1795845413) + buffer.appendInt32(427944574) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -557,6 +557,8 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags2) & Int(1 << 4) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 6) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 5) != 0 {serializeInt64(backgroundEmojiId!, buffer: buffer, boxed: false)} break case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): if boxed { @@ -601,8 +603,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): - 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)]) + 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 backgroundEmojiId): + 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), ("backgroundEmojiId", backgroundEmojiId 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): @@ -657,6 +659,10 @@ public extension Api { } } var _15: Int32? if Int(_2!) & Int(1 << 4) != 0 {_15 = reader.readInt32() } + var _16: Int32? + if Int(_2!) & Int(1 << 6) != 0 {_16 = reader.readInt32() } + var _17: Int64? + if Int(_2!) & Int(1 << 5) != 0 {_17 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -672,8 +678,10 @@ public extension Api { let _c13 = (Int(_1!) & Int(1 << 17) == 0) || _13 != nil let _c14 = (Int(_2!) & Int(1 << 0) == 0) || _14 != nil let _c15 = (Int(_2!) & Int(1 << 4) == 0) || _15 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { - 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) + let _c16 = (Int(_2!) & Int(1 << 6) == 0) || _16 != nil + let _c17 = (Int(_2!) & Int(1 << 5) == 0) || _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + 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, backgroundEmojiId: _17) } else { return nil @@ -1230,15 +1238,15 @@ public extension Api { } public extension Api { indirect enum ChatInvite: TypeConstructorDescription { - case chatInvite(flags: Int32, title: String, about: String?, photo: Api.Photo, participantsCount: Int32, participants: [Api.User]?) + case chatInvite(flags: Int32, title: String, about: String?, photo: Api.Photo, participantsCount: Int32, participants: [Api.User]?, color: Int32) case chatInviteAlready(chat: Api.Chat) case chatInvitePeek(chat: Api.Chat, expires: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants): + case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color): if boxed { - buffer.appendInt32(806110401) + buffer.appendInt32(-840897472) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) @@ -1250,6 +1258,7 @@ public extension Api { for item in participants! { item.serialize(buffer, true) }} + serializeInt32(color, buffer: buffer, boxed: false) break case .chatInviteAlready(let chat): if boxed { @@ -1269,8 +1278,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants): - return ("chatInvite", [("flags", flags as Any), ("title", title as Any), ("about", about as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("participants", participants as Any)]) + case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color): + return ("chatInvite", [("flags", flags as Any), ("title", title as Any), ("about", about as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("participants", participants as Any), ("color", color as Any)]) case .chatInviteAlready(let chat): return ("chatInviteAlready", [("chat", chat as Any)]) case .chatInvitePeek(let chat, let expires): @@ -1295,14 +1304,17 @@ public extension Api { if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } } + var _7: Int32? + _7 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.ChatInvite.chatInvite(flags: _1!, title: _2!, about: _3, photo: _4!, participantsCount: _5!, participants: _6) + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.ChatInvite.chatInvite(flags: _1!, title: _2!, about: _3, photo: _4!, participantsCount: _5!, participants: _6, color: _7!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 37333fb4c50..10815203ddf 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,91 @@ +public extension Api.upload { + enum File: TypeConstructorDescription { + case file(type: Api.storage.FileType, mtime: Int32, bytes: Buffer) + case fileCdnRedirect(dcId: Int32, fileToken: Buffer, encryptionKey: Buffer, encryptionIv: Buffer, fileHashes: [Api.FileHash]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .file(let type, let mtime, let bytes): + if boxed { + buffer.appendInt32(157948117) + } + type.serialize(buffer, true) + serializeInt32(mtime, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .fileCdnRedirect(let dcId, let fileToken, let encryptionKey, let encryptionIv, let fileHashes): + if boxed { + buffer.appendInt32(-242427324) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeBytes(fileToken, buffer: buffer, boxed: false) + serializeBytes(encryptionKey, buffer: buffer, boxed: false) + serializeBytes(encryptionIv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(fileHashes.count)) + for item in fileHashes { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .file(let type, let mtime, let bytes): + return ("file", [("type", type as Any), ("mtime", mtime as Any), ("bytes", bytes as Any)]) + case .fileCdnRedirect(let dcId, let fileToken, let encryptionKey, let encryptionIv, let fileHashes): + return ("fileCdnRedirect", [("dcId", dcId as Any), ("fileToken", fileToken as Any), ("encryptionKey", encryptionKey as Any), ("encryptionIv", encryptionIv as Any), ("fileHashes", fileHashes as Any)]) + } + } + + public static func parse_file(_ reader: BufferReader) -> File? { + var _1: Api.storage.FileType? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.storage.FileType + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Buffer? + _3 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.upload.File.file(type: _1!, mtime: _2!, bytes: _3!) + } + else { + return nil + } + } + public static func parse_fileCdnRedirect(_ reader: BufferReader) -> File? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Buffer? + _3 = parseBytes(reader) + var _4: Buffer? + _4 = parseBytes(reader) + var _5: [Api.FileHash]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FileHash.self) + } + 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.upload.File.fileCdnRedirect(dcId: _1!, fileToken: _2!, encryptionKey: _3!, encryptionIv: _4!, fileHashes: _5!) + } + else { + return nil + } + } + + } +} public extension Api.upload { enum WebFile: TypeConstructorDescription { case webFile(size: Int32, mimeType: String, fileType: Api.storage.FileType, mtime: Int32, bytes: Buffer) diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 19f01cb8d7b..881d3faef4d 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -373,6 +373,21 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func getDefaultBackgroundEmojis(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1509246514) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "account.getDefaultBackgroundEmojis", 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 getDefaultEmojiStatuses(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -1216,6 +1231,23 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func updateColor(flags: Int32, color: Int32, backgroundEmojiId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1610494909) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(color, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(backgroundEmojiId!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "account.updateColor", parameters: [("flags", String(describing: flags)), ("color", String(describing: color)), ("backgroundEmojiId", String(describing: backgroundEmojiId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func updateDeviceLocked(period: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2986,6 +3018,24 @@ 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) { + let buffer = Buffer() + buffer.appendInt32(1645879327) + serializeInt32(flags, buffer: buffer, boxed: false) + channel.serialize(buffer, true) + 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) + 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() @@ -5868,16 +5918,16 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getWebPage(url: String, hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getWebPage(url: String, hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(852135825) + buffer.appendInt32(-1919511901) serializeString(url, buffer: buffer, boxed: false) serializeInt32(hash, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.getWebPage", parameters: [("url", String(describing: url)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.WebPage? in + return (FunctionDescription(name: "messages.getWebPage", parameters: [("url", String(describing: url)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.WebPage? in let reader = BufferReader(buffer) - var result: Api.WebPage? + var result: Api.messages.WebPage? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.WebPage + result = Api.parse(reader, signature: signature) as? Api.messages.WebPage } return result }) @@ -6447,12 +6497,11 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func saveDraft(flags: Int32, replyToMsgId: Int32?, topMsgId: Int32?, peer: Api.InputPeer, message: String, entities: [Api.MessageEntity]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func saveDraft(flags: Int32, replyTo: Api.InputReplyTo?, peer: Api.InputPeer, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1271718337) + buffer.appendInt32(2146678790) serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(replyToMsgId!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {replyTo!.serialize(buffer, true)} peer.serialize(buffer, true) serializeString(message, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) @@ -6460,7 +6509,8 @@ public extension Api.functions.messages { for item in entities! { item.serialize(buffer, true) }} - return (FunctionDescription(name: "messages.saveDraft", parameters: [("flags", String(describing: flags)), ("replyToMsgId", String(describing: replyToMsgId)), ("topMsgId", String(describing: topMsgId)), ("peer", String(describing: peer)), ("message", String(describing: message)), ("entities", String(describing: entities))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + if Int(flags) & Int(1 << 5) != 0 {media!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.saveDraft", parameters: [("flags", String(describing: flags)), ("replyTo", String(describing: replyTo)), ("peer", String(describing: peer)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { @@ -7440,6 +7490,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.payments { + static func applyGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-152934316) + serializeString(slug, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.applyGiftCode", parameters: [("slug", String(describing: slug))]), 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.payments { static func assignAppStoreTransaction(receipt: Buffer, purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7487,6 +7552,21 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func checkGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1907247935) + serializeString(slug, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.checkGiftCode", parameters: [("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.CheckedGiftCode? in + let reader = BufferReader(buffer) + var result: Api.payments.CheckedGiftCode? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.CheckedGiftCode + } + return result + }) + } +} public extension Api.functions.payments { static func clearSavedInfo(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7532,6 +7612,22 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getGiveawayInfo(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-198994907) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getGiveawayInfo", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.GiveawayInfo? in + let reader = BufferReader(buffer) + var result: Api.payments.GiveawayInfo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.GiveawayInfo + } + return result + }) + } +} public extension Api.functions.payments { static func getPaymentForm(flags: Int32, invoice: Api.InputInvoice, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7565,6 +7661,22 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getPremiumGiftCodeOptions(flags: Int32, boostPeer: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.PremiumGiftCodeOption]>) { + let buffer = Buffer() + buffer.appendInt32(660060756) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)} + return (FunctionDescription(name: "payments.getPremiumGiftCodeOptions", parameters: [("flags", String(describing: flags)), ("boostPeer", String(describing: boostPeer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.PremiumGiftCodeOption]? in + let reader = BufferReader(buffer) + var result: [Api.PremiumGiftCodeOption]? + if let _ = reader.readInt32() { + result = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumGiftCodeOption.self) + } + return result + }) + } +} public extension Api.functions.payments { static func getSavedInfo() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7580,6 +7692,23 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func launchPrepaidGiveaway(peer: Api.InputPeer, giveawayId: Int64, purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1609928480) + peer.serialize(buffer, true) + serializeInt64(giveawayId, buffer: buffer, boxed: false) + purpose.serialize(buffer, true) + return (FunctionDescription(name: "payments.launchPrepaidGiveaway", parameters: [("peer", String(describing: peer)), ("giveawayId", String(describing: giveawayId)), ("purpose", String(describing: purpose))]), 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.payments { static func sendPaymentForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8247,6 +8376,75 @@ public extension Api.functions.photos { }) } } +public extension Api.functions.premium { + static func applyBoost(flags: Int32, slots: [Int32]?, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1803396934) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(slots!.count)) + for item in slots! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + peer.serialize(buffer, true) + return (FunctionDescription(name: "premium.applyBoost", parameters: [("flags", String(describing: flags)), ("slots", String(describing: slots)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.premium.MyBoosts? in + let reader = BufferReader(buffer) + var result: Api.premium.MyBoosts? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.premium.MyBoosts + } + return result + }) + } +} +public extension Api.functions.premium { + static func getBoostsList(flags: Int32, peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1626764896) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeString(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "premium.getBoostsList", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.premium.BoostsList? in + let reader = BufferReader(buffer) + var result: Api.premium.BoostsList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.premium.BoostsList + } + return result + }) + } +} +public extension Api.functions.premium { + static func getBoostsStatus(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(70197089) + peer.serialize(buffer, true) + return (FunctionDescription(name: "premium.getBoostsStatus", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.premium.BoostsStatus? in + let reader = BufferReader(buffer) + var result: Api.premium.BoostsStatus? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.premium.BoostsStatus + } + return result + }) + } +} +public extension Api.functions.premium { + static func getMyBoosts() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(199719754) + + return (FunctionDescription(name: "premium.getMyBoosts", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.premium.MyBoosts? in + let reader = BufferReader(buffer) + var result: Api.premium.MyBoosts? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.premium.MyBoosts + } + return result + }) + } +} public extension Api.functions.stats { static func getBroadcastStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8518,36 +8716,6 @@ public extension Api.functions.stories { }) } } -public extension Api.functions.stories { - static func applyBoost(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-224560085) - peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.applyBoost", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} -public extension Api.functions.stories { - static func canApplyBoost(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-620379715) - peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.canApplyBoost", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.CanApplyBoostResult? in - let reader = BufferReader(buffer) - var result: Api.stories.CanApplyBoostResult? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.stories.CanApplyBoostResult - } - return result - }) - } -} public extension Api.functions.stories { static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8664,38 +8832,6 @@ public extension Api.functions.stories { }) } } -public extension Api.functions.stories { - static func getBoostersList(peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(863959424) - peer.serialize(buffer, true) - serializeString(offset, buffer: buffer, boxed: false) - serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.getBoostersList", parameters: [("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.BoostersList? in - let reader = BufferReader(buffer) - var result: Api.stories.BoostersList? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.stories.BoostersList - } - return result - }) - } -} -public extension Api.functions.stories { - static func getBoostsStatus(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(1279562866) - peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.getBoostsStatus", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.BoostsStatus? in - let reader = BufferReader(buffer) - var result: Api.stories.BoostsStatus? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.stories.BoostsStatus - } - return result - }) - } -} public extension Api.functions.stories { static func getChatsToSend() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index c73745ecd00..9a71f34213d 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -871,7 +871,7 @@ public extension Api { } } public extension Api { - enum Dialog: TypeConstructorDescription { + indirect enum Dialog: TypeConstructorDescription { case dialog(flags: Int32, peer: Api.Peer, topMessage: Int32, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32, notifySettings: Api.PeerNotifySettings, pts: Int32?, draft: Api.DraftMessage?, folderId: Int32?, ttlPeriod: Int32?) case dialogFolder(flags: Int32, folder: Api.Folder, peer: Api.Peer, topMessage: Int32, unreadMutedPeersCount: Int32, unreadUnmutedPeersCount: Int32, unreadMutedMessagesCount: Int32, unreadUnmutedMessagesCount: Int32) diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index 751e130562d..b54c445aa26 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1,22 +1,23 @@ public extension Api { - enum DraftMessage: TypeConstructorDescription { - case draftMessage(flags: Int32, replyToMsgId: Int32?, message: String, entities: [Api.MessageEntity]?, date: Int32) + indirect enum DraftMessage: TypeConstructorDescription { + case draftMessage(flags: Int32, replyTo: Api.InputReplyTo?, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, date: Int32) case draftMessageEmpty(flags: Int32, date: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .draftMessage(let flags, let replyToMsgId, let message, let entities, let date): + case .draftMessage(let flags, let replyTo, let message, let entities, let media, let date): if boxed { - buffer.appendInt32(-40996577) + buffer.appendInt32(1070397423) } serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(replyToMsgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {replyTo!.serialize(buffer, true)} serializeString(message, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) buffer.appendInt32(Int32(entities!.count)) for item in entities! { item.serialize(buffer, true) }} + if Int(flags) & Int(1 << 5) != 0 {media!.serialize(buffer, true)} serializeInt32(date, buffer: buffer, boxed: false) break case .draftMessageEmpty(let flags, let date): @@ -31,8 +32,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .draftMessage(let flags, let replyToMsgId, let message, let entities, let date): - return ("draftMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("message", message as Any), ("entities", entities as Any), ("date", date as Any)]) + case .draftMessage(let flags, let replyTo, let message, let entities, let media, let date): + return ("draftMessage", [("flags", flags as Any), ("replyTo", replyTo as Any), ("message", message as Any), ("entities", entities as Any), ("media", media as Any), ("date", date as Any)]) case .draftMessageEmpty(let flags, let date): return ("draftMessageEmpty", [("flags", flags as Any), ("date", date as Any)]) } @@ -41,23 +42,30 @@ public extension Api { public static func parse_draftMessage(_ reader: BufferReader) -> DraftMessage? { var _1: Int32? _1 = reader.readInt32() - var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } + var _2: Api.InputReplyTo? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputReplyTo + } } var _3: String? _3 = parseString(reader) var _4: [Api.MessageEntity]? if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } - var _5: Int32? - _5 = reader.readInt32() + var _5: Api.InputMedia? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.InputMedia + } } + var _6: Int32? + _6 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.DraftMessage.draftMessage(flags: _1!, replyToMsgId: _2, message: _3!, entities: _4, date: _5!) + let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.DraftMessage.draftMessage(flags: _1!, replyTo: _2, message: _3!, entities: _4, media: _5, date: _6!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 6c3196fbe20..60cf7f42908 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -211,7 +211,7 @@ public extension Api { } } public extension Api { - enum ForumTopic: TypeConstructorDescription { + indirect enum ForumTopic: TypeConstructorDescription { case forumTopic(flags: Int32, id: Int32, date: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?, topMessage: Int32, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32, fromId: Api.Peer, notifySettings: Api.PeerNotifySettings, draft: Api.DraftMessage?) case forumTopicDeleted(id: Int32) @@ -1198,6 +1198,7 @@ public extension Api { case inputBotInlineMessageMediaGeo(flags: Int32, geoPoint: Api.InputGeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String, providerData: Api.DataJSON, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageMediaVenue(flags: Int32, geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaWebPage(flags: Int32, message: String, entities: [Api.MessageEntity]?, url: String, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageText(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -1271,6 +1272,20 @@ public extension Api { serializeString(venueType, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} break + case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + if boxed { + buffer.appendInt32(-1109605104) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + serializeString(url, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): if boxed { buffer.appendInt32(1036876423) @@ -1301,6 +1316,8 @@ public extension Api { return ("inputBotInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageMediaVenue(let flags, let geoPoint, let title, let address, let provider, let venueId, let venueType, let replyMarkup): return ("inputBotInlineMessageMediaVenue", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + return ("inputBotInlineMessageMediaWebPage", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("url", url as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): return ("inputBotInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) } @@ -1483,6 +1500,33 @@ public extension Api { return nil } } + public static func parse_inputBotInlineMessageMediaWebPage(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _4: String? + _4 = parseString(reader) + var _5: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5) + } + else { + return nil + } + } public static func parse_inputBotInlineMessageText(_ reader: BufferReader) -> InputBotInlineMessage? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index a2c1c1095d3..8048250b313 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -209,6 +209,7 @@ public extension Api { public extension Api { indirect enum InputInvoice: TypeConstructorDescription { case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) + case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) case inputInvoiceSlug(slug: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -220,6 +221,13 @@ public extension Api { peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) break + case .inputInvoicePremiumGiftCode(let purpose, let option): + if boxed { + buffer.appendInt32(-1734841331) + } + purpose.serialize(buffer, true) + option.serialize(buffer, true) + break case .inputInvoiceSlug(let slug): if boxed { buffer.appendInt32(-1020867857) @@ -233,6 +241,8 @@ public extension Api { switch self { case .inputInvoiceMessage(let peer, let msgId): return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) + case .inputInvoicePremiumGiftCode(let purpose, let option): + return ("inputInvoicePremiumGiftCode", [("purpose", purpose as Any), ("option", option as Any)]) case .inputInvoiceSlug(let slug): return ("inputInvoiceSlug", [("slug", slug as Any)]) } @@ -254,6 +264,24 @@ public extension Api { return nil } } + public static func parse_inputInvoicePremiumGiftCode(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputStorePaymentPurpose? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose + } + var _2: Api.PremiumGiftCodeOption? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.PremiumGiftCodeOption + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoicePremiumGiftCode(purpose: _1!, option: _2!) + } + else { + return nil + } + } public static func parse_inputInvoiceSlug(_ reader: BufferReader) -> InputInvoice? { var _1: String? _1 = parseString(reader) @@ -286,6 +314,7 @@ public extension Api { case inputMediaUploadedDocument(flags: Int32, file: Api.InputFile, thumb: Api.InputFile?, mimeType: String, attributes: [Api.DocumentAttribute], stickers: [Api.InputDocument]?, ttlSeconds: Int32?) case inputMediaUploadedPhoto(flags: Int32, file: Api.InputFile, stickers: [Api.InputDocument]?, ttlSeconds: Int32?) case inputMediaVenue(geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) + case inputMediaWebPage(flags: Int32, url: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -449,6 +478,13 @@ public extension Api { serializeString(venueId, buffer: buffer, boxed: false) serializeString(venueType, buffer: buffer, boxed: false) break + case .inputMediaWebPage(let flags, let url): + if boxed { + buffer.appendInt32(-1038383031) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break } } @@ -486,6 +522,8 @@ public extension Api { return ("inputMediaUploadedPhoto", [("flags", flags as Any), ("file", file as Any), ("stickers", stickers as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaVenue(let geoPoint, let title, let address, let provider, let venueId, let venueType): return ("inputMediaVenue", [("geoPoint", geoPoint as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)]) + case .inputMediaWebPage(let flags, let url): + return ("inputMediaWebPage", [("flags", flags as Any), ("url", url as Any)]) } } @@ -829,6 +867,20 @@ public extension Api { return nil } } + public static func parse_inputMediaWebPage(_ reader: BufferReader) -> InputMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputMedia.inputMediaWebPage(flags: _1!, url: _2!) + } + else { + return nil + } + } } } diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 4f344a515ce..4538013b8ad 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -818,7 +818,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if case let .messages(chatLocation, _, _) = playlistLocation { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramCallsUI/Sources/CallRatingController.swift b/submodules/TelegramCallsUI/Sources/CallRatingController.swift index c1ab8823a1d..d9c9d9b8921 100644 --- a/submodules/TelegramCallsUI/Sources/CallRatingController.swift +++ b/submodules/TelegramCallsUI/Sources/CallRatingController.swift @@ -292,7 +292,7 @@ func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int let name = "\(callId.id)_\(callId.accessHash).log.json" let path = callLogsPath(account: engine.account) + "/" + name let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - let message = EnqueueMessage.message(text: comment, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: comment, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) return rate |> then(enqueueMessages(account: engine.account, peerId: peerId, messages: [message]) |> mapToSignal({ _ -> Signal in @@ -300,7 +300,7 @@ func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int })) } else if !comment.isEmpty { return rate - |> then(enqueueMessages(account: engine.account, peerId: peerId, messages: [.message(text: comment, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + |> then(enqueueMessages(account: engine.account, peerId: peerId, messages: [.message(text: comment, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) |> mapToSignal({ _ -> Signal in return .single(Void()) })) diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 5944f985df1..553f2bf350e 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -270,7 +270,7 @@ public final class MediaStreamComponent: CombinedComponent { let moreButtonTag = local.moreButtonTag let moreAnimationTag = local.moreAnimationTag - func makeBody() -> CGSize { + let makeBody: () -> CGSize = { [weak local] in let canEnforceOrientation = UIDevice.current.model != "iPad" var forceFullScreenInLandscape: Bool { canEnforceOrientation && true } let environment = context.environment[ViewControllerComponentContainer.Environment.self].value @@ -289,13 +289,20 @@ public final class MediaStreamComponent: CombinedComponent { let state = context.state let controller = environment.controller - context.state.deactivatePictureInPictureIfVisible.connect { + context.state.deactivatePictureInPictureIfVisible.connect { [weak state] in guard let controller = controller(), controller.view.window != nil else { return } + guard let state else { + return + } state.updated(transition: .easeInOut(duration: 3)) - deactivatePictureInPicture.invoke(Void()) + + guard let local else { + return + } + local.deactivatePictureInPicture.invoke(Void()) } let isFullscreen: Bool let isLandscape = context.availableSize.width > context.availableSize.height @@ -347,8 +354,7 @@ public final class MediaStreamComponent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight), transition: context.transition ) - // (controller() as? MediaStreamComponentController)?.prefersOnScreenNavigationHidden = isFullscreen - // (controller() as? MediaStreamComponentController)?.window?.invalidatePrefersOnScreenNavigationHidden() + let video = video.update( component: MediaStreamVideoComponent( call: context.component.call, @@ -832,6 +838,8 @@ public final class MediaStreamComponent: CombinedComponent { UIColor(red: 0.314, green: 0.161, blue: 0.197, alpha: 1).cgColor ], image: generateImage(CGSize(width: 44.0 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2, y: size.height / 2) context.scaleBy(x: 0.4, y: 0.4) context.translateBy(x: -size.width / 2, y: -size.height / 2) @@ -1989,7 +1997,7 @@ final class RoundGradientButtonComponent: Component { final class View: UIView { let gradientLayer = CAGradientLayer() let iconView = UIImageView() - let titleLabel = UILabel() + let titleLabel = ComponentView() override init(frame: CGRect = .zero) { super.init(frame: frame) @@ -2001,27 +2009,43 @@ final class RoundGradientButtonComponent: Component { self.layer.addSublayer(gradientLayer) self.addSubview(iconView) self.clipsToBounds = false - - self.addSubview(titleLabel) - titleLabel.textAlignment = .center - iconView.contentMode = .scaleAspectFit - titleLabel.font = .systemFont(ofSize: 13) - titleLabel.textColor = .white } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func layoutSubviews() { - super.layoutSubviews() - titleLabel.invalidateIntrinsicContentSize() - let heightForIcon = bounds.height - max(round(titleLabel.intrinsicContentSize.height), 12) - 8.0 - iconView.frame = .init(x: bounds.midX - heightForIcon / 2, y: 0, width: heightForIcon, height: heightForIcon) - gradientLayer.masksToBounds = true - gradientLayer.cornerRadius = min(iconView.frame.width, iconView.frame.height) / 2 - gradientLayer.frame = iconView.frame - titleLabel.frame = .init(x: 0, y: bounds.height - titleLabel.intrinsicContentSize.height, width: bounds.width, height: titleLabel.intrinsicContentSize.height) + func update(component: RoundGradientButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.iconView.image = component.image ?? component.icon.flatMap { UIImage(bundleImageName: $0) } + let gradientColors: [CGColor] + if component.gradientColors.count == 1 { + gradientColors = [component.gradientColors[0], component.gradientColors[0]] + } else { + gradientColors = component.gradientColors + } + self.gradientLayer.colors = gradientColors + + let titleSize = self.titleLabel.update( + transition: .immediate, + component: AnyComponent(Text(text: component.title, font: Font.regular(13.0), color: .white)), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let heightForIcon = availableSize.height - max(round(titleSize.height), 12) - 8.0 + self.iconView.frame = .init(x: availableSize.width * 0.5 - heightForIcon / 2, y: 0, width: heightForIcon, height: heightForIcon) + self.gradientLayer.masksToBounds = true + self.gradientLayer.cornerRadius = min(self.iconView.frame.width, self.iconView.frame.height) / 2 + self.gradientLayer.frame = self.iconView.frame + + if let titleView = self.titleLabel.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: availableSize.height - titleSize.height), size: titleSize) + } + + return availableSize } } @@ -2030,17 +2054,7 @@ final class RoundGradientButtonComponent: Component { } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - view.iconView.image = image ?? icon.flatMap { UIImage(bundleImageName: $0) } - let gradientColors: [CGColor] - if self.gradientColors.count == 1 { - gradientColors = [self.gradientColors[0], self.gradientColors[0]] - } else { - gradientColors = self.gradientColors - } - view.gradientLayer.colors = gradientColors - view.titleLabel.text = title - view.setNeedsLayout() - return availableSize + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 383700b4ad3..591844aac67 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -758,12 +758,18 @@ public final class PresentationCallManagerImpl: PresentationCallManager { public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult { let begin: () -> Void = { [weak self] in - if let requestJoinAsPeerId = requestJoinAsPeerId { + if let requestJoinAsPeerId = requestJoinAsPeerId, (initialCall.isStream == nil || initialCall.isStream == false) { requestJoinAsPeerId({ joinAsPeerId in - let _ = self?.startGroupCall(accountContext: context, peerId: peerId, invite: invite, joinAsPeerId: joinAsPeerId, initialCall: initialCall).start() + guard let self else { + return + } + self.startCallDisposable.set(self.startGroupCall(accountContext: context, peerId: peerId, invite: invite, joinAsPeerId: joinAsPeerId, initialCall: initialCall).startStrict()) }) } else { - let _ = self?.startGroupCall(accountContext: context, peerId: peerId, invite: invite, joinAsPeerId: nil, initialCall: initialCall).start() + guard let self else { + return + } + self.startCallDisposable.set(self.startGroupCall(accountContext: context, peerId: peerId, invite: invite, joinAsPeerId: nil, initialCall: initialCall).startStrict()) } } @@ -810,6 +816,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let accessEnabledSignal: Signal = Signal { subscriber in if let isStream = initialCall.isStream, isStream { subscriber.putNext(true) + subscriber.putCompletion() + return EmptyDisposable } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index ec1abfe31d2..298f3d103f9 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1263,7 +1263,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController dismissController?() if let strongSelf = self { - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self { strongSelf.presentUndoOverlay(content: .forward(savedMessages: false, text: strongSelf.presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return true }) @@ -1572,7 +1572,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController }).start() } - strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess, timeout: nil), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) } }) self?.controller?.present(controller, in: .window(.root)) @@ -1593,7 +1593,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController if let strongSelf = self, let (firstName, lastName) = firstAndLastName { let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start() - strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess, timeout: nil), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false }) } }) self?.controller?.present(controller, in: .window(.root)) @@ -2517,7 +2517,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuDisplayAsItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuDisplayAsItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) items.append(.separator) break @@ -2550,7 +2550,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuAudioItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuAudioItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) } @@ -2587,7 +2587,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuPermissionItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuPermissionItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) } } @@ -2865,7 +2865,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) return .single(items) } @@ -2960,7 +2960,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) return items } @@ -3006,7 +3006,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) }))) } return .single(items) diff --git a/submodules/TelegramCore/Package.swift b/submodules/TelegramCore/Package.swift index c98e938f00d..bd86af416d7 100644 --- a/submodules/TelegramCore/Package.swift +++ b/submodules/TelegramCore/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "TelegramCore", - platforms: [.macOS(.v10_12)], + 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/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 987ff10d191..bfdc900f1cd 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1216,38 +1216,38 @@ public class Account { let extractedExpr1: [Signal] = [ managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { inputStates in if inputStates { - print("inputStates: true") + //print("inputStates: true") } return inputStates ? AccountRunningImportantTasks.other : [] }, self.pendingMessageManager.hasPendingMessages |> map { hasPendingMessages in if !hasPendingMessages.isEmpty { - print("hasPendingMessages: true") + //print("hasPendingMessages: true") } return !hasPendingMessages.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, (self.pendingStoryManager?.hasPending ?? .single(false)) |> map { hasPending in if hasPending { - print("hasPending: true") + //print("hasPending: true") } return hasPending ? AccountRunningImportantTasks.pendingMessages : [] }, self.pendingUpdateMessageManager.updatingMessageMedia |> map { updatingMessageMedia in if !updatingMessageMedia.isEmpty { - print("updatingMessageMedia: true") + //print("updatingMessageMedia: true") } return !updatingMessageMedia.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, self.pendingPeerMediaUploadManager.uploadingPeerMedia |> map { uploadingPeerMedia in if !uploadingPeerMedia.isEmpty { - print("uploadingPeerMedia: true") + //print("uploadingPeerMedia: true") } return !uploadingPeerMedia.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, self.accountPresenceManager.isPerformingUpdate() |> map { presenceUpdate in if presenceUpdate { - print("accountPresenceManager isPerformingUpdate: true") - return [] + //print("accountPresenceManager isPerformingUpdate: true") + //return [] } return presenceUpdate ? AccountRunningImportantTasks.other : [] }, diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 670595df4bb..207923e6111 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -468,7 +468,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 @@ -528,7 +528,7 @@ struct AccountMutableState { var presences: [PeerId: Api.UserStatus] = [:] for user in users { switch user { - case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _, _): + case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _, _, _, _): if let status = status { presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))] = status } diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index d269a6ac749..0d9c2dc32cf 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -177,6 +177,7 @@ private var declaredEncodables: Void = { declareEncodable(InlineBotMessageAttribute.self, f: { InlineBotMessageAttribute(decoder: $0) }) declareEncodable(TextEntitiesMessageAttribute.self, f: { TextEntitiesMessageAttribute(decoder: $0) }) declareEncodable(ReplyMessageAttribute.self, f: { ReplyMessageAttribute(decoder: $0) }) + declareEncodable(QuotedReplyMessageAttribute.self, f: { QuotedReplyMessageAttribute(decoder: $0) }) declareEncodable(ReplyStoryAttribute.self, f: { ReplyStoryAttribute(decoder: $0) }) declareEncodable(ReplyThreadMessageAttribute.self, f: { ReplyThreadMessageAttribute(decoder: $0) }) declareEncodable(ReactionsMessageAttribute.self, f: { ReactionsMessageAttribute(decoder: $0) }) @@ -285,6 +286,8 @@ private var declaredEncodables: Void = { declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) }) declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) }) declareEncodable(MapVenue.self, f: { MapVenue(decoder: $0) }) + declareEncodable(TelegramMediaGiveaway.self, f: { TelegramMediaGiveaway(decoder: $0) }) + declareEncodable(WebpagePreviewMessageAttribute.self, f: { WebpagePreviewMessageAttribute(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/AccountManager/AccountManagerImpl.swift b/submodules/TelegramCore/Sources/AccountManager/AccountManagerImpl.swift index 51f2ac029c2..cd1b7d546f6 100644 --- a/submodules/TelegramCore/Sources/AccountManager/AccountManagerImpl.swift +++ b/submodules/TelegramCore/Sources/AccountManager/AccountManagerImpl.swift @@ -75,6 +75,7 @@ final class AccountManagerImpl { return (atomicState.records.sorted(by: { $0.key.int64 < $1.key.int64 }).map({ $1 }), atomicState.currentRecordId) } catch let e { postboxLog("decode atomic state error: \(e)") + postboxLogSync() preconditionFailure() } } @@ -91,10 +92,16 @@ final class AccountManagerImpl { self.temporarySessionId = temporarySessionId let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else { + postboxLog("Could not open guard value box at \(basePath + "/guard_db")") + postboxLogSync() + preconditionFailure() return nil } self.guardValueBox = guardValueBox guard let valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else { + postboxLog("Could not open value box at \(basePath + "/db")") + postboxLogSync() + preconditionFailure() return nil } self.valueBox = valueBox @@ -112,6 +119,7 @@ final class AccountManagerImpl { } catch let e { postboxLog("decode atomic state error: \(e)") let _ = try? FileManager.default.removeItem(atPath: self.atomicStatePath) + postboxLogSync() preconditionFailure() } } catch let e { @@ -274,9 +282,11 @@ final class AccountManagerImpl { if let data = try? JSONEncoder().encode(self.currentAtomicState) { if let _ = try? data.write(to: URL(fileURLWithPath: self.atomicStatePath), options: [.atomic]) { } else { + postboxLogSync() preconditionFailure() } } else { + postboxLogSync() preconditionFailure() } } @@ -555,6 +565,7 @@ public final class AccountManager { if let value = AccountManagerImpl(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, temporarySessionId: temporarySessionId, hiddenAccountManager: hiddenAccountManager) { return value } else { + postboxLogSync() preconditionFailure() } }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 9b065806556..bc0ab4d0489 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, _): + case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, nameColor, backgroundEmojiId): let isMin = (flags & (1 << 12)) != 0 let participationStatus: TelegramChannelParticipationStatus @@ -153,7 +153,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { } } - 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) + 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) case let .channelForbidden(flags, id, accessHash, title, untilDate): let info: TelegramChannelInfo if (flags & Int32(1 << 8)) != 0 { @@ -162,7 +162,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) + 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) } } @@ -170,7 +170,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, _): + case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, nameColor, backgroundEmojiId): let isMin = (flags & (1 << 12)) != 0 if accessHash != nil && !isMin { return parseTelegramGroupOrChannel(chat: rhs) @@ -212,7 +212,7 @@ 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) + 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: nameColor.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId) } else { return parseTelegramGroupOrChannel(chat: rhs) } @@ -266,6 +266,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) + 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) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 2a4baa3a60f..65fa28f5136 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -15,81 +15,108 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { } case auto(caption: String, entities: TextEntitiesMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?) - case text(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, replyMarkup: ReplyMarkupMessageAttribute?) + case text(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, previewParameters: WebpagePreviewMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?) case mapLocation(media: TelegramMediaMap, replyMarkup: ReplyMarkupMessageAttribute?) case contact(media: TelegramMediaContact, replyMarkup: ReplyMarkupMessageAttribute?) case invoice(media: TelegramMediaInvoice, replyMarkup: ReplyMarkupMessageAttribute?) + case webpage(text: String, entities: TextEntitiesMessageAttribute?, url: String, previewParameters: WebpagePreviewMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("_v", orElse: 0) { - case 0: - self = .auto(caption: decoder.decodeStringForKey("c", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) - case 1: - self = .text(text: decoder.decodeStringForKey("t", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, disableUrlPreview: decoder.decodeInt32ForKey("du", orElse: 0) != 0, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) - case 2: - self = .mapLocation(media: decoder.decodeObjectForKey("l") as! TelegramMediaMap, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) - case 3: - self = .contact(media: decoder.decodeObjectForKey("c") as! TelegramMediaContact, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) - case 4: - self = .invoice(media: decoder.decodeObjectForKey("i") as! TelegramMediaInvoice, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) - default: - self = .auto(caption: "", entities: nil, replyMarkup: nil) + case 0: + self = .auto(caption: decoder.decodeStringForKey("c", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + case 1: + self = .text(text: decoder.decodeStringForKey("t", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, disableUrlPreview: decoder.decodeInt32ForKey("du", orElse: 0) != 0, previewParameters: decoder.decodeObjectForKey("prp") as? WebpagePreviewMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + case 2: + self = .mapLocation(media: decoder.decodeObjectForKey("l") as! TelegramMediaMap, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + case 3: + self = .contact(media: decoder.decodeObjectForKey("c") as! TelegramMediaContact, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + case 4: + self = .invoice(media: decoder.decodeObjectForKey("i") as! TelegramMediaInvoice, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + case 5: + self = .webpage(text: decoder.decodeStringForKey("t", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, url: decoder.decodeStringForKey("url", orElse: ""), previewParameters: decoder.decodeObjectForKey("prp") as? WebpagePreviewMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + default: + self = .auto(caption: "", entities: nil, replyMarkup: nil) } } public func encode(_ encoder: PostboxEncoder) { switch self { - case let .auto(caption, entities, replyMarkup): - encoder.encodeInt32(0, forKey: "_v") - encoder.encodeString(caption, forKey: "c") - if let entities = entities { - encoder.encodeObject(entities, forKey: "e") - } else { - encoder.encodeNil(forKey: "e") - } - if let replyMarkup = replyMarkup { - encoder.encodeObject(replyMarkup, forKey: "m") - } else { - encoder.encodeNil(forKey: "m") - } - case let .text(text, entities, disableUrlPreview, replyMarkup): - encoder.encodeInt32(1, forKey: "_v") - encoder.encodeString(text, forKey: "t") - if let entities = entities { - encoder.encodeObject(entities, forKey: "e") - } else { - encoder.encodeNil(forKey: "e") - } - encoder.encodeInt32(disableUrlPreview ? 1 : 0, forKey: "du") - if let replyMarkup = replyMarkup { - encoder.encodeObject(replyMarkup, forKey: "m") - } else { - encoder.encodeNil(forKey: "m") - } - case let .mapLocation(media, replyMarkup): - encoder.encodeInt32(2, forKey: "_v") - encoder.encodeObject(media, forKey: "l") - if let replyMarkup = replyMarkup { - encoder.encodeObject(replyMarkup, forKey: "m") - } else { - encoder.encodeNil(forKey: "m") - } - case let .contact(media, replyMarkup): - encoder.encodeInt32(3, forKey: "_v") - encoder.encodeObject(media, forKey: "c") - if let replyMarkup = replyMarkup { - encoder.encodeObject(replyMarkup, forKey: "m") - } else { - encoder.encodeNil(forKey: "m") - } - case let .invoice(media: media, replyMarkup): - encoder.encodeInt32(4, forKey: "_v") - encoder.encodeObject(media, forKey: "i") - if let replyMarkup = replyMarkup { - encoder.encodeObject(replyMarkup, forKey: "m") - } else { - encoder.encodeNil(forKey: "m") - } + case let .auto(caption, entities, replyMarkup): + encoder.encodeInt32(0, forKey: "_v") + encoder.encodeString(caption, forKey: "c") + if let entities = entities { + encoder.encodeObject(entities, forKey: "e") + } else { + encoder.encodeNil(forKey: "e") + } + if let replyMarkup = replyMarkup { + encoder.encodeObject(replyMarkup, forKey: "m") + } else { + encoder.encodeNil(forKey: "m") + } + case let .text(text, entities, disableUrlPreview, previewParameters, replyMarkup): + encoder.encodeInt32(1, forKey: "_v") + encoder.encodeString(text, forKey: "t") + if let entities = entities { + encoder.encodeObject(entities, forKey: "e") + } else { + encoder.encodeNil(forKey: "e") + } + encoder.encodeInt32(disableUrlPreview ? 1 : 0, forKey: "du") + if let previewParameters = previewParameters { + encoder.encodeObject(previewParameters, forKey: "prp") + } else { + encoder.encodeNil(forKey: "prp") + } + if let replyMarkup = replyMarkup { + encoder.encodeObject(replyMarkup, forKey: "m") + } else { + encoder.encodeNil(forKey: "m") + } + case let .mapLocation(media, replyMarkup): + encoder.encodeInt32(2, forKey: "_v") + encoder.encodeObject(media, forKey: "l") + if let replyMarkup = replyMarkup { + encoder.encodeObject(replyMarkup, forKey: "m") + } else { + encoder.encodeNil(forKey: "m") + } + case let .contact(media, replyMarkup): + encoder.encodeInt32(3, forKey: "_v") + encoder.encodeObject(media, forKey: "c") + if let replyMarkup = replyMarkup { + encoder.encodeObject(replyMarkup, forKey: "m") + } else { + encoder.encodeNil(forKey: "m") + } + case let .invoice(media, replyMarkup): + encoder.encodeInt32(4, forKey: "_v") + encoder.encodeObject(media, forKey: "i") + if let replyMarkup = replyMarkup { + encoder.encodeObject(replyMarkup, forKey: "m") + } else { + encoder.encodeNil(forKey: "m") + } + case let .webpage(text, entities, url, previewParameters, replyMarkup): + encoder.encodeInt32(5, forKey: "_v") + encoder.encodeString(text, forKey: "t") + if let entities = entities { + encoder.encodeObject(entities, forKey: "e") + } else { + encoder.encodeNil(forKey: "e") + } + encoder.encodeString(url, forKey: "url") + if let previewParameters = previewParameters { + encoder.encodeObject(previewParameters, forKey: "prp") + } else { + encoder.encodeNil(forKey: "prp") + } + if let replyMarkup = replyMarkup { + encoder.encodeObject(replyMarkup, forKey: "m") + } else { + encoder.encodeNil(forKey: "m") + } } } @@ -110,75 +137,99 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { public static func ==(lhs: ChatContextResultMessage, rhs: ChatContextResultMessage) -> Bool { switch lhs { - case let .auto(lhsCaption, lhsEntities, lhsReplyMarkup): - if case let .auto(rhsCaption, rhsEntities, rhsReplyMarkup) = rhs { - if lhsCaption != rhsCaption { - return false - } - if lhsEntities != rhsEntities { - return false - } - if lhsReplyMarkup != rhsReplyMarkup { - return false - } - return true - } else { + case let .auto(lhsCaption, lhsEntities, lhsReplyMarkup): + if case let .auto(rhsCaption, rhsEntities, rhsReplyMarkup) = rhs { + if lhsCaption != rhsCaption { return false } - case let .text(lhsText, lhsEntities, lhsDisableUrlPreview, lhsReplyMarkup): - if case let .text(rhsText, rhsEntities, rhsDisableUrlPreview, rhsReplyMarkup) = rhs { - if lhsText != rhsText { - return false - } - if lhsEntities != rhsEntities { - return false - } - if lhsDisableUrlPreview != rhsDisableUrlPreview { - return false - } - if lhsReplyMarkup != rhsReplyMarkup { - return false - } - return true - } else { + if lhsEntities != rhsEntities { return false } - case let .mapLocation(lhsMedia, lhsReplyMarkup): - if case let .mapLocation(rhsMedia, rhsReplyMarkup) = rhs { - if !lhsMedia.isEqual(to: rhsMedia) { - return false - } - if lhsReplyMarkup != rhsReplyMarkup { - return false - } - return true - } else { + if lhsReplyMarkup != rhsReplyMarkup { return false } - case let .contact(lhsMedia, lhsReplyMarkup): - if case let .contact(rhsMedia, rhsReplyMarkup) = rhs { - if !lhsMedia.isEqual(to: rhsMedia) { - return false - } - if lhsReplyMarkup != rhsReplyMarkup { - return false - } - return true - } else { + return true + } else { + return false + } + case let .text(lhsText, lhsEntities, lhsDisableUrlPreview, lhsPreviewParameters, lhsReplyMarkup): + if case let .text(rhsText, rhsEntities, rhsDisableUrlPreview, rhsPreviewParameters, rhsReplyMarkup) = rhs { + if lhsText != rhsText { return false } - case let .invoice(lhsMedia, lhsReplyMarkup): - if case let .invoice(rhsMedia, rhsReplyMarkup) = rhs { - if !lhsMedia.isEqual(to: rhsMedia) { - return false - } - if lhsReplyMarkup != rhsReplyMarkup { - return false - } - return true - } else { + if lhsEntities != rhsEntities { return false } + if lhsDisableUrlPreview != rhsDisableUrlPreview { + return false + } + if lhsPreviewParameters != rhsPreviewParameters { + return false + } + if lhsReplyMarkup != rhsReplyMarkup { + return false + } + return true + } else { + return false + } + case let .mapLocation(lhsMedia, lhsReplyMarkup): + if case let .mapLocation(rhsMedia, rhsReplyMarkup) = rhs { + if !lhsMedia.isEqual(to: rhsMedia) { + return false + } + if lhsReplyMarkup != rhsReplyMarkup { + return false + } + return true + } else { + return false + } + case let .contact(lhsMedia, lhsReplyMarkup): + if case let .contact(rhsMedia, rhsReplyMarkup) = rhs { + if !lhsMedia.isEqual(to: rhsMedia) { + return false + } + if lhsReplyMarkup != rhsReplyMarkup { + return false + } + return true + } else { + return false + } + case let .invoice(lhsMedia, lhsReplyMarkup): + if case let .invoice(rhsMedia, rhsReplyMarkup) = rhs { + if !lhsMedia.isEqual(to: rhsMedia) { + return false + } + if lhsReplyMarkup != rhsReplyMarkup { + return false + } + return true + } else { + return false + } + case let .webpage(lhsText, lhsEntities, lhsUrl, lhsPreviewParameters, lhsReplyMarkup): + if case let .webpage(rhsText, rhsEntities, rhsUrl, rhsPreviewParameters, rhsReplyMarkup) = rhs { + if lhsText != rhsText { + return false + } + if lhsEntities != rhsEntities { + return false + } + if lhsUrl != rhsUrl { + return false + } + if lhsPreviewParameters != rhsPreviewParameters { + return false + } + if lhsReplyMarkup != rhsReplyMarkup { + return false + } + return true + } else { + return false + } } } } @@ -458,7 +509,13 @@ extension ChatContextResultMessage { if let replyMarkup = replyMarkup { parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) } - self = .text(text: message, entities: parsedEntities, disableUrlPreview: (flags & (1 << 0)) != 0, replyMarkup: parsedReplyMarkup) + let leadingPreview = (flags & (1 << 3)) != 0 + self = .text(text: message, entities: parsedEntities, disableUrlPreview: (flags & (1 << 0)) != 0, previewParameters: WebpagePreviewMessageAttribute( + leadingPreview: leadingPreview, + forceLargeMedia: nil, + isManuallyAdded: false, + isSafe: false + ), replyMarkup: parsedReplyMarkup) case let .botInlineMessageMediaGeo(_, geo, heading, period, proximityNotificationRadius, replyMarkup): let media = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) var parsedReplyMarkup: ReplyMarkupMessageAttribute? @@ -492,7 +549,40 @@ extension ChatContextResultMessage { if let replyMarkup = replyMarkup { parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) } - self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup) + self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup) + case let .botInlineMessageMediaWebPage(flags, message, entities, url, replyMarkup): + var parsedReplyMarkup: ReplyMarkupMessageAttribute? + if let replyMarkup = replyMarkup { + parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) + } + + let leadingPreview = (flags & (1 << 3)) != 0 + var forceLargeMedia: Bool? + if (flags & (1 << 4)) != 0 { + forceLargeMedia = true + } else if (flags & (1 << 5)) != 0 { + forceLargeMedia = false + } + let isManuallyAdded = (flags & (1 << 7)) != 0 + let isSafe = (flags & (1 << 8)) != 0 + + var parsedEntities: TextEntitiesMessageAttribute? + if let entities = entities, !entities.isEmpty { + parsedEntities = TextEntitiesMessageAttribute(entities: messageTextEntitiesFromApiEntities(entities)) + } + + self = .webpage( + text: message, + entities: parsedEntities, + url: url, + previewParameters: WebpagePreviewMessageAttribute( + leadingPreview: leadingPreview, + forceLargeMedia: forceLargeMedia, + isManuallyAdded: isManuallyAdded, + isSafe: isSafe + ), + replyMarkup: parsedReplyMarkup + ) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 2982656c33d..64cea4abacf 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -217,7 +217,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionSetSameChatWallPaper: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionSetSameChatWallPaper, .messageActionGiveawayLaunch: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -244,6 +244,10 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } case let .messageActionRequestedPeer(_, peer): result.append(peer.peerId) + case let .messageActionGiftCode(_, boostPeer, _, _): + if let boostPeer = boostPeer { + result.append(boostPeer.peerId) + } } return result @@ -257,11 +261,19 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere let peerId: PeerId = chatPeerId.peerId switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, _): - let targetId = MessageId(peerId: replyToPeerId?.peerId ?? peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - var replyIds = ReferencedReplyMessageIds() - replyIds.add(sourceId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) - return (replyIds, []) + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): + let _ = replyHeader + let _ = replyMedia + let _ = replyToTopId + let _ = quoteText + let _ = quoteEntities + + if let replyToMsgId = replyToMsgId { + let targetId = MessageId(peerId: replyToPeerId?.peerId ?? peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + var replyIds = ReferencedReplyMessageIds() + replyIds.add(sourceId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) + return (replyIds, []) + } case .messageReplyStoryHeader: break } @@ -271,11 +283,19 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _): if let replyHeader = replyHeader { switch replyHeader { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, _): - let targetId = MessageId(peerId: replyToPeerId?.peerId ?? chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - var replyIds = ReferencedReplyMessageIds() - replyIds.add(sourceId: MessageId(peerId: chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) - return (replyIds, []) + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): + let _ = replyHeader + let _ = replyMedia + let _ = replyToTopId + let _ = quoteText + let _ = quoteEntities + + if let replyToMsgId = replyToMsgId { + let targetId = MessageId(peerId: replyToPeerId?.peerId ?? chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + var replyIds = ReferencedReplyMessageIds() + replyIds.add(sourceId: MessageId(peerId: chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) + return (replyIds, []) + } case .messageReplyStoryHeader: break } @@ -284,48 +304,65 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere return nil } -func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (Media?, Int32?, Bool?, Bool?) { +struct ParsedMessageWebpageAttributes { + var forceLargeMedia: Bool? + var isManuallyAdded: Bool + var isSafe: Bool +} + +func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?) { if let media = media { switch media { case let .messageMediaPhoto(flags, photo, ttlSeconds): if let photo = photo { if let mediaImage = telegramMediaImageFromApiPhoto(photo) { - return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0) + return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil) } } else { - return (TelegramMediaExpiredContent(data: .image), nil, nil, nil) + return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil) } case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId): let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard) - return (mediaContact, nil, nil, nil) + return (mediaContact, nil, nil, nil, nil) case let .messageMediaGeo(geo): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil) case let .messageMediaVenue(geo, title, address, provider, venueId, venueType): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil) case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) - return (mediaMap, nil, nil, nil) + return (mediaMap, nil, nil, nil, nil) case let .messageMediaDocument(flags, document, _, ttlSeconds): if let document = document { if let mediaFile = telegramMediaFileFromApiDocument(document) { - return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0) + return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) } } else { - return (TelegramMediaExpiredContent(data: .file), nil, nil, nil) + return (TelegramMediaExpiredContent(data: .file), nil, nil, nil, nil) } - case let .messageMediaWebPage(webpage): + case let .messageMediaWebPage(flags, webpage): if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage, url: nil) { - return (mediaWebpage, nil, nil, nil) + var webpageForceLargeMedia: Bool? + if (flags & (1 << 0)) != 0 { + webpageForceLargeMedia = true + } else if (flags & (1 << 1)) != 0 { + webpageForceLargeMedia = false + } + + return (mediaWebpage, nil, nil, nil, ParsedMessageWebpageAttributes( + forceLargeMedia: webpageForceLargeMedia, + isManuallyAdded: (flags & (1 << 3)) != 0, + isSafe: (flags & (1 << 4)) != 0 + )) } case .messageMediaUnsupported: - return (TelegramMediaUnsupported(), nil, nil, nil) + return (TelegramMediaUnsupported(), nil, nil, nil, nil) case .messageMediaEmpty: break case let .messageMediaGame(game): - return (TelegramMediaGame(apiGame: game), nil, nil, nil) + return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil) case let .messageMediaInvoice(flags, title, description, photo, receiptMsgId, currency, totalAmount, startParam, apiExtendedMedia): var parsedFlags = TelegramMediaInvoiceFlags() if (flags & (1 << 3)) != 0 { @@ -349,7 +386,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } extendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) + let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) if let media = media { extendedMedia = .full(media: media) } else { @@ -360,7 +397,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI extendedMedia = nil } - return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil) + return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil) case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers, closePeriod, _): @@ -376,17 +413,23 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil) + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil) } case let .messageMediaDice(value, emoticon): - return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil) + return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil) 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) + return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil) + case let .messageMediaGiveaway(apiFlags, channels, countries, 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 (nil, nil, nil, nil) + return (nil, nil, nil, nil, nil) } func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { @@ -543,40 +586,48 @@ extension StoreMessage { if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { - case let .messageReplyHeader(flags, replyToMsgId, replyToPeerId, replyToTopId): - let isForumTopic = (flags & (1 << 3)) != 0 + case let .messageReplyHeader(innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): + let isForumTopic = (innerFlags & (1 << 3)) != 0 - let replyPeerId = replyToPeerId?.peerId ?? peerId - if let replyToTopId = replyToTopId { - if peerIsForum { - if isForumTopic { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) - threadMessageId = threadIdValue - if replyPeerId == peerId { + var quote: EngineMessageReplyQuote? + let isQuote = (innerFlags & (1 << 9)) != 0 + + if quoteText != nil || replyMedia != nil { + quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media) + } + + if let replyToMsgId = replyToMsgId { + let replyPeerId = replyToPeerId?.peerId ?? peerId + if let replyToTopId = replyToTopId { + if peerIsForum { + if isForumTopic { + let threadIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) + threadMessageId = threadIdValue threadId = makeMessageThreadId(threadIdValue) } - } - } else { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) - threadMessageId = threadIdValue - if replyPeerId == peerId { + } else { + let threadIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) + threadMessageId = threadIdValue threadId = makeMessageThreadId(threadIdValue) } - } - } else if peerId.namespace == Namespaces.Peer.CloudChannel { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - - if peerIsForum { - if isForumTopic { + } else if peerId.namespace == Namespaces.Peer.CloudChannel { + let threadIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + + if peerIsForum { + if isForumTopic { + threadMessageId = threadIdValue + threadId = makeMessageThreadId(threadIdValue) + } + } else { threadMessageId = threadIdValue threadId = makeMessageThreadId(threadIdValue) } - } else { - threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) } + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) + } + if let replyHeader = replyHeader { + attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId)) case let .messageReplyStoryHeader(userId, storyId): attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId))) } @@ -636,7 +687,7 @@ extension StoreMessage { var consumableContent: (Bool, Bool)? = nil if let media = media { - let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) @@ -652,6 +703,14 @@ extension StoreMessage { if let hasSpoiler = hasSpoiler, hasSpoiler { attributes.append(MediaSpoilerMessageAttribute()) } + + if mediaValue is TelegramMediaWebpage { + let leadingPreview = (flags & (1 << 27)) != 0 + + if let webpageAttributes = webpageAttributes { + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: leadingPreview, forceLargeMedia: webpageAttributes.forceLargeMedia, isManuallyAdded: webpageAttributes.isManuallyAdded, isSafe: webpageAttributes.isSafe)) + } + } } } @@ -808,26 +867,36 @@ extension StoreMessage { if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyToTopId): - let replyPeerId = replyToPeerId?.peerId ?? peerId - if let replyToTopId = replyToTopId { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) - threadMessageId = threadIdValue - if replyPeerId == peerId { + case let .messageReplyHeader(innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): + var quote: EngineMessageReplyQuote? + let isQuote = (innerFlags & (1 << 9)) != 0 + if quoteText != nil || replyMedia != nil { + quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media) + } + + if let replyToMsgId = replyToMsgId { + let replyPeerId = replyToPeerId?.peerId ?? peerId + if let replyToTopId = replyToTopId { + let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) + threadMessageId = threadIdValue + if replyPeerId == peerId { + threadId = makeMessageThreadId(threadIdValue) + } + } else if peerId.namespace == Namespaces.Peer.CloudChannel { + let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + threadMessageId = threadIdValue threadId = makeMessageThreadId(threadIdValue) } - } else if peerId.namespace == Namespaces.Peer.CloudChannel { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) - } - switch action { - case .messageActionTopicEdit: - threadId = Int64(replyToMsgId) - default: - break + switch action { + case .messageActionTopicEdit: + threadId = Int64(replyToMsgId) + default: + break + } + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) + } else if let replyHeader = replyHeader { + attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId)) case let .messageReplyStoryHeader(userId, storyId): attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId))) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 997719f0cdf..f4f4de886d1 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -127,6 +127,10 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper))) case let .messageActionSetSameChatWallPaper(wallpaper): return TelegramMediaAction(action: .setSameChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper))) + 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 .messageActionGiveawayLaunch: + return TelegramMediaAction(action: .giveawayLaunched) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index 66c30a87513..4cd629839fb 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -21,9 +21,10 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) -> switch webpage { case .webPageNotModified: return nil - case let .webPagePending(id, date): + case let .webPagePending(flags, id, url, date): + let _ = flags return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Pending(date, url)) - case let .webPage(_, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes): + case let .webPage(flags, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes): var embedSize: PixelDimensions? if let embedWidth = embedWidth, let embedHeight = embedHeight { embedSize = PixelDimensions(width: embedWidth, height: embedHeight) @@ -55,8 +56,62 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) -> if let cachedPage = cachedPage { instantPage = InstantPage(apiPage: cachedPage) } - return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage))) + + let isMediaLargeByDefault = (flags & (1 << 13)) != 0 + + return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, isMediaLargeByDefault: isMediaLargeByDefault, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage))) case .webPageEmpty: return nil } } + +public class WebpagePreviewMessageAttribute: MessageAttribute, Equatable { + public let associatedPeerIds: [PeerId] = [] + public let associatedMediaIds: [MediaId] = [] + + public let leadingPreview: Bool + public let forceLargeMedia: Bool? + public let isManuallyAdded: Bool + public let isSafe: Bool + + public init(leadingPreview: Bool, forceLargeMedia: Bool?, isManuallyAdded: Bool, isSafe: Bool) { + self.leadingPreview = leadingPreview + self.forceLargeMedia = forceLargeMedia + self.isManuallyAdded = isManuallyAdded + self.isSafe = isSafe + } + + required public init(decoder: PostboxDecoder) { + self.leadingPreview = decoder.decodeBoolForKey("lp", orElse: false) + self.forceLargeMedia = decoder.decodeOptionalBoolForKey("lm") + self.isManuallyAdded = decoder.decodeBoolForKey("ma", orElse: false) + self.isSafe = decoder.decodeBoolForKey("sf", orElse: false) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeBool(self.leadingPreview, forKey: "lp") + if let forceLargeMedia = self.forceLargeMedia { + encoder.encodeBool(forceLargeMedia, forKey: "lm") + } else { + encoder.encodeNil(forKey: "lm") + } + encoder.encodeBool(self.isManuallyAdded, forKey: "ma") + encoder.encodeBool(self.isSafe, forKey: "sf") + } + + public static func ==(lhs: WebpagePreviewMessageAttribute, rhs: WebpagePreviewMessageAttribute) -> Bool { + if lhs.leadingPreview != rhs.leadingPreview { + return false + } + if lhs.forceLargeMedia != rhs.forceLargeMedia { + return false + } + if lhs.isManuallyAdded != rhs.isManuallyAdded { + return false + } + if lhs.isSafe != rhs.isSafe { + return false + } + return true + } +} diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index 1daa1a3d993..ebc1d017af5 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -36,7 +36,7 @@ extension TelegramPeerUsername { extension TelegramUser { convenience init(user: Api.User) { switch user { - case let .user(flags, flags2, id, accessHash, firstName, lastName, username, phone, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, usernames, _): + case let .user(flags, flags2, id, accessHash, firstName, lastName, username, phone, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, usernames, _, nameColor, backgroundEmojiId): let representations: [TelegramMediaImageRepresentation] = photo.flatMap(parsedTelegramProfilePhoto) ?? [] let isMin = (flags & (1 << 20)) != 0 @@ -96,15 +96,15 @@ extension TelegramUser { let restrictionInfo: PeerAccessRestrictionInfo? = restrictionReason.flatMap(PeerAccessRestrictionInfo.init(apiReasons:)) - self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, firstName: firstName, lastName: lastName, username: username, phone: phone, photo: representations, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden) + self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, firstName: firstName, lastName: lastName, username: username, phone: phone, photo: representations, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColor.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId) case let .userEmpty(id): - self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), 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) } } static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? { switch rhs { - case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _, _): + case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _, _, nameColor, backgroundEmojiId): let isMin = (flags & (1 << 20)) != 0 if !isMin { return TelegramUser(user: rhs) @@ -180,7 +180,7 @@ extension TelegramUser { accessHash = lhs.accessHash ?? rhsAccessHashValue } - return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: telegramPhoto, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: lhs.usernames, storiesHidden: lhs.storiesHidden) + return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: telegramPhoto, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: lhs.usernames, storiesHidden: lhs.storiesHidden, nameColor: nameColor.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId) } else { return TelegramUser(user: rhs) } @@ -241,7 +241,7 @@ extension TelegramUser { storiesHidden = lhs.storiesHidden } - return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: photo, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus, usernames: lhs.usernames, storiesHidden: storiesHidden) + return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: photo, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus, usernames: lhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift index 9a29052a971..8cf6224660d 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift @@ -23,7 +23,7 @@ extension TelegramUserPresence { convenience init?(apiUser: Api.User) { switch apiUser { - case let .user(_, _, _, _, _, _, _, _, _, status, _, _, _, _, _, _, _): + case let .user(_, _, _, _, _, _, _, _, _, status, _, _, _, _, _, _, _, _, _): if let status = status { self.init(apiStatus: status) } else { diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 7018b6b2261..560eda65ed5 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -253,7 +253,12 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title: } } -func _internal_fetchForumChannelTopic(account: Account, peerId: PeerId, threadId: Int64) -> Signal { +public enum FetchForumChannelTopicResult { + case progress + case result(EngineMessageHistoryThread.Info?) +} + +func _internal_fetchForumChannelTopic(account: Account, peerId: PeerId, threadId: Int64) -> Signal { return account.postbox.transaction { transaction -> (info: EngineMessageHistoryThread.Info?, inputChannel: Api.InputChannel?) in if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { return (data.info, nil) @@ -261,20 +266,20 @@ func _internal_fetchForumChannelTopic(account: Account, peerId: PeerId, threadId return (nil, transaction.getPeer(peerId).flatMap(apiInputChannel)) } } - |> mapToSignal { info, _ -> Signal in + |> mapToSignal { info, _ -> Signal in if let info = info { - return .single(info) + return .single(.result(info)) } else { - return resolveForumThreads(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, ids: [MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))]) - |> mapToSignal { _ -> Signal in - return account.postbox.transaction { transaction -> EngineMessageHistoryThread.Info? in + return .single(.progress) |> then(resolveForumThreads(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, ids: [MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))]) + |> mapToSignal { _ -> Signal in + return account.postbox.transaction { transaction -> FetchForumChannelTopicResult in if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { - return data.info + return .result(data.info) } else { - return nil + return .result(nil) } } - } + }) } } } diff --git a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift index b0ae253b1aa..774b18901ca 100644 --- a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift +++ b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift @@ -14,8 +14,13 @@ public enum InternalUpdaterError { public func requestUpdatesXml(account: Account, source: String) -> Signal { return TelegramEngine(account: account).peers.resolvePeerByName(name: source) |> castError(InternalUpdaterError.self) - |> map { peer -> Peer? in - return peer?._asPeer() + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .never() + case let .result(peer): + return .single(peer?._asPeer()) + } } |> mapToSignal { peer -> Signal in if let peer = peer, let inputPeer = apiInputPeer(peer) { @@ -79,8 +84,13 @@ public enum AppUpdateDownloadResult { public func downloadAppUpdate(account: Account, source: String, messageId: Int32) -> Signal { return TelegramEngine(account: account).peers.resolvePeerByName(name: source) |> castError(InternalUpdaterError.self) - |> mapToSignal { peer -> Signal in - return .single(peer?._asPeer()) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .never() + case let .result(peer): + return .single(peer?._asPeer()) + } } |> mapToSignal { peer in if let peer = peer, let inputChannel = apiInputChannel(peer) { diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index ddaa422d097..84674c29b88 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -492,9 +492,9 @@ final class MediaReferenceRevalidationContext { } } - func webPage(postbox: Postbox, network: Network, background: Bool, webPage: WebpageReference) -> Signal { + func webPage(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, background: Bool, webPage: WebpageReference) -> Signal { return self.genericItem(key: .webPage(webPage: webPage), background: background, request: { next, error in - return (updatedRemoteWebpage(postbox: postbox, network: network, webPage: webPage) + return (updatedRemoteWebpage(postbox: postbox, network: network, accountPeerId: accountPeerId, webPage: webPage) |> mapError { _ -> RevalidateMediaReferenceError in }).start(next: { value in if let value = value { @@ -792,7 +792,7 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n return .fail(.generic) } case let .webPage(webPage, previousMedia): - return revalidationContext.webPage(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, webPage: webPage) + return revalidationContext.webPage(accountPeerId: accountPeerId, postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, webPage: webPage) |> mapToSignal { result -> Signal in if let updatedResource = findUpdatedMediaResource(media: result, previousMedia: previousMedia, resource: resource) { return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) diff --git a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift index 0bff10aa24f..95331fcf9f0 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift @@ -191,10 +191,22 @@ private final class MultipartUploadManager { } } + deinit { + let uploadingParts = self.uploadingParts + let dataDisposable = self.dataDisposable + + self.queue.async { + for (_, (_, disposable)) in uploadingParts { + disposable.dispose() + } + dataDisposable.dispose() + } + } + func start() { self.queue.async { self.dataDisposable.set((self.dataSignal - |> deliverOn(self.queue)).start(next: { [weak self] data in + |> deliverOn(self.queue)).startStrict(next: { [weak self] data in if let strongSelf = self { strongSelf.resourceData = data strongSelf.checkState() @@ -276,11 +288,11 @@ private final class MultipartUploadManager { self.headerPartState = .uploading let part = self.uploadPart(UploadPart(fileId: self.fileId, index: partIndex, data: partData, bigTotalParts: currentBigTotalParts, bigPart: self.bigParts)) |> deliverOn(self.queue) - self.uploadingParts[0] = (partSize, part.start(error: { [weak self] _ in + self.uploadingParts[0] = (partSize, part.startStrict(error: { [weak self] _ in self?.completed(nil) }, completed: { [weak self] in if let strongSelf = self { - let _ = strongSelf.uploadingParts.removeValue(forKey: 0) + strongSelf.uploadingParts.removeValue(forKey: 0)?.1.dispose() strongSelf.headerPartState = .ready strongSelf.checkState() } @@ -350,11 +362,11 @@ private final class MultipartUploadManager { break } } - self.uploadingParts[nextOffset] = (partSize, part.start(error: { [weak self] _ in + self.uploadingParts[nextOffset] = (partSize, part.startStrict(error: { [weak self] _ in self?.completed(nil) }, completed: { [weak self] in if let strongSelf = self { - let _ = strongSelf.uploadingParts.removeValue(forKey: nextOffset) + strongSelf.uploadingParts.removeValue(forKey: nextOffset)?.1.dispose() strongSelf.uploadedParts[partOffset] = partSize if partIndex == 0 { strongSelf.headerPartState = .ready diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 0ec3d5d6dfb..d82251d9df8 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -9,14 +9,86 @@ public enum EnqueueMessageGrouping { case auto } +public struct EngineMessageReplyQuote: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case text = "t" + case entities = "e" + case media = "m" + } + + public var text: String + public var entities: [MessageTextEntity] + public var media: Media? + + public init(text: String, entities: [MessageTextEntity], media: Media?) { + self.text = text + self.entities = entities + self.media = media + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.text = try container.decode(String.self, forKey: .text) + self.entities = try container.decode([MessageTextEntity].self, forKey: .entities) + + if let mediaData = try container.decodeIfPresent(Data.self, forKey: .media) { + self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media + } else { + self.media = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.text, forKey: .text) + try container.encode(self.entities, forKey: .entities) + if let media = self.media { + let mediaEncoder = PostboxEncoder() + mediaEncoder.encodeRootObject(media) + try container.encode(mediaEncoder.makeData(), forKey: .media) + } + } + + public static func ==(lhs: EngineMessageReplyQuote, rhs: EngineMessageReplyQuote) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.entities != rhs.entities { + return false + } + if let lhsMedia = lhs.media, let rhsMedia = rhs.media { + if !lhsMedia.isEqual(to: rhsMedia) { + return false + } + } else { + if (lhs.media == nil) != (rhs.media == nil) { + return false + } + } + return true + } +} + +public struct EngineMessageReplySubject: Codable, Equatable { + public var messageId: EngineMessage.Id + public var quote: EngineMessageReplyQuote? + + public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?) { + self.messageId = messageId + self.quote = quote + } +} + public enum EnqueueMessage { - case message(text: String, attributes: [MessageAttribute], inlineStickers: [MediaId: Media], mediaReference: AnyMediaReference?, replyToMessageId: MessageId?, replyToStoryId: StoryId?, localGroupingKey: Int64?, correlationId: Int64?, bubbleUpEmojiOrStickersets: [ItemCollectionId]) + case message(text: String, attributes: [MessageAttribute], inlineStickers: [MediaId: Media], mediaReference: AnyMediaReference?, threadId: Int64?, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, localGroupingKey: Int64?, correlationId: Int64?, bubbleUpEmojiOrStickersets: [ItemCollectionId]) case forward(source: MessageId, threadId: Int64?, grouping: EnqueueMessageGrouping, attributes: [MessageAttribute], correlationId: Int64?, asCopy: Bool = false) - public func withUpdatedReplyToMessageId(_ replyToMessageId: MessageId?) -> EnqueueMessage { + public func withUpdatedReplyToMessageId(_ replyToMessageId: EngineMessageReplySubject?) -> EnqueueMessage { switch self { - case let .message(text, attributes, inlineStickers, mediaReference, _, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): - return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + case let .message(text, attributes, inlineStickers, mediaReference, threadId, _, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) case .forward: return self } @@ -24,8 +96,8 @@ public enum EnqueueMessage { public func withUpdatedReplyToStoryId(_ replyToStoryId: StoryId?) -> EnqueueMessage { switch self { - case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, _, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): - return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, _, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) case .forward: return self } @@ -33,8 +105,8 @@ public enum EnqueueMessage { public func withUpdatedAttributes(_ f: ([MessageAttribute]) -> [MessageAttribute]) -> EnqueueMessage { switch self { - case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): - return .message(text: text, attributes: f(attributes), inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + case let .message(text, attributes, inlineStickers, mediaReference, threadId: threadId, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + return .message(text: text, attributes: f(attributes), inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) case let .forward(source, threadId, grouping, attributes, correlationId, asCopy): return .forward(source: source, threadId: threadId, grouping: grouping, attributes: f(attributes), correlationId: correlationId, asCopy: asCopy) } @@ -42,8 +114,8 @@ public enum EnqueueMessage { public func withUpdatedGroupingKey(_ f: (Int64?) -> Int64?) -> EnqueueMessage { switch self { - case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): - return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: f(localGroupingKey), correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: f(localGroupingKey), correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) case .forward: return self } @@ -51,15 +123,15 @@ public enum EnqueueMessage { public func withUpdatedCorrelationId(_ value: Int64?) -> EnqueueMessage { switch self { - case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, replyToStoryId, localGroupingKey, _, bubbleUpEmojiOrStickersets): - return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: value, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, replyToStoryId, localGroupingKey, _, bubbleUpEmojiOrStickersets): + return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: value, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) case let .forward(source, threadId, grouping, attributes, _, asCopy): return .forward(source: source, threadId: threadId, grouping: grouping, attributes: attributes, correlationId: value, asCopy: asCopy) } } public var groupingKey: Int64? { - if case let .message(_, _, _, _, _, _, localGroupingKey, _, _) = self { + if case let .message(_, _, _, _, _, _, _, localGroupingKey, _, _) = self { return localGroupingKey } else { return nil @@ -70,7 +142,7 @@ public enum EnqueueMessage { private extension EnqueueMessage { var correlationId: Int64? { switch self { - case let .message(_, _, _, _, _, _, _, correlationId, _): + case let .message(_, _, _, _, _, _, _, _, correlationId, _): return correlationId case let .forward(_, _, _, _, correlationId, _): return correlationId @@ -79,7 +151,7 @@ private extension EnqueueMessage { var bubbleUpEmojiOrStickersets: [ItemCollectionId] { switch self { - case let .message(_, _, _, _, _, _, _, _, bubbleUpEmojiOrStickersets): + case let .message(_, _, _, _, _, _, _, _, _, bubbleUpEmojiOrStickersets): return bubbleUpEmojiOrStickersets case .forward: return [] @@ -118,36 +190,38 @@ private func convertForwardedMediaForSecretChat(_ media: Media) -> Media { private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAttribute]) -> [MessageAttribute] { return attributes.filter { attribute in switch attribute { - case _ as TextEntitiesMessageAttribute: - return true - case _ as InlineBotMessageAttribute: - return true - case _ as OutgoingMessageInfoAttribute: - return false - case _ as OutgoingContentInfoMessageAttribute: - return true - case _ as ReplyMarkupMessageAttribute: - return true - case _ as OutgoingChatContextResultMessageAttribute: - return true - case _ as AutoremoveTimeoutMessageAttribute: - return true - case _ as NotificationInfoMessageAttribute: - return true - case _ as OutgoingScheduleInfoMessageAttribute: - return true - case _ as EmbeddedMediaStickersMessageAttribute: - return true - case _ as EmojiSearchQueryMessageAttribute: - return true - case _ as ForwardOptionsMessageAttribute: - return true - case _ as SendAsMessageAttribute: - return true - case _ as MediaSpoilerMessageAttribute: - return true - default: - return false + case _ as TextEntitiesMessageAttribute: + return true + case _ as InlineBotMessageAttribute: + return true + case _ as OutgoingMessageInfoAttribute: + return false + case _ as OutgoingContentInfoMessageAttribute: + return true + case _ as ReplyMarkupMessageAttribute: + return true + case _ as OutgoingChatContextResultMessageAttribute: + return true + case _ as AutoremoveTimeoutMessageAttribute: + return true + case _ as NotificationInfoMessageAttribute: + return true + case _ as OutgoingScheduleInfoMessageAttribute: + return true + case _ as EmbeddedMediaStickersMessageAttribute: + return true + case _ as EmojiSearchQueryMessageAttribute: + return true + case _ as ForwardOptionsMessageAttribute: + return true + case _ as SendAsMessageAttribute: + return true + case _ as MediaSpoilerMessageAttribute: + return true + case _ as WebpagePreviewMessageAttribute: + return true + default: + return false } } } @@ -170,6 +244,9 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt case _ as MediaSpoilerMessageAttribute: return true case let attribute as ReplyMessageAttribute: + if attribute.quote != nil { + return true + } if let forwardedMessageIds = forwardedMessageIds { return forwardedMessageIds.contains(attribute.messageId) } else { @@ -204,7 +281,7 @@ private func opportunisticallyTransformOutgoingMedia(network: Network, postbox: var hasMedia = false loop: for message in messages { switch message { - case let .message(_, _, _, mediaReference, _, _, _, _, _): + case let .message(_, _, _, mediaReference, _, _, _, _, _, _): if mediaReference != nil { hasMedia = true break loop @@ -221,14 +298,14 @@ private func opportunisticallyTransformOutgoingMedia(network: Network, postbox: var signals: [Signal<(Bool, EnqueueMessage), NoError>] = [] for message in messages { switch message { - case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): if let mediaReference = mediaReference { signals.append(opportunisticallyTransformMessageWithMedia(network: network, postbox: postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, mediaReference: mediaReference, userInteractive: userInteractive) |> map { result -> (Bool, EnqueueMessage) in if let result = result { - return (true, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: .standalone(media: result.media), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) + return (true, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: .standalone(media: result.media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) } else { - return (false, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) + return (false, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) } }) } else { @@ -268,9 +345,9 @@ public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], return account.postbox.transaction { transaction -> [MessageId] in var messageIds: [MessageId] = [] for peerId in peerIds { - var replyToMessageId: MessageId? + var replyToMessageId: EngineMessageReplySubject? if let threadIds = threadIds[peerId] { - replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadIds)) + replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadIds)), quote: nil) } var messages = messages if let replyToMessageId = replyToMessageId { @@ -297,13 +374,13 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal< removeMessageIds.append(id) var filteredAttributes: [MessageAttribute] = [] - var replyToMessageId: MessageId? + var replyToMessageId: EngineMessageReplySubject? var replyToStoryId: StoryId? var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] var forwardSource: MessageId? inner: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - replyToMessageId = attribute.messageId + replyToMessageId = EngineMessageReplySubject(messageId: attribute.messageId, quote: attribute.quote) } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let attribute = attribute as? OutgoingMessageInfoAttribute { @@ -319,7 +396,7 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal< if let forwardSource = forwardSource { messages.append(.forward(source: forwardSource, threadId: nil, grouping: .auto, attributes: filteredAttributes, correlationId: nil)) } else { - messages.append(.message(text: message.text, attributes: filteredAttributes, inlineStickers: [:], mediaReference: message.media.first.flatMap(AnyMediaReference.standalone), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: message.groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) + messages.append(.message(text: message.text, attributes: filteredAttributes, inlineStickers: [:], mediaReference: message.media.first.flatMap(AnyMediaReference.standalone), threadId: message.threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: message.groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) } } } @@ -350,8 +427,8 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } switch message { - case let .message(_, attributes, _, _, replyToMessageId, _, _, _, _): - if let replyToMessageId = replyToMessageId, replyToMessageId.peerId != peerId, let replyMessage = transaction.getMessage(replyToMessageId) { + case let .message(_, attributes, _, _, threadId, replyToMessageId, _, _, _, _): + if let replyToMessageId = replyToMessageId, (replyToMessageId.messageId.peerId != peerId && peerId.namespace == Namespaces.Peer.SecretChat), let replyMessage = transaction.getMessage(replyToMessageId.messageId) { var canBeForwarded = true if replyMessage.id.namespace != Namespaces.Message.Cloud { canBeForwarded = false @@ -363,7 +440,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } if canBeForwarded { - updatedMessages.append((true, .forward(source: replyToMessageId, threadId: nil, grouping: .none, attributes: attributes, correlationId: nil))) + updatedMessages.append((true, .forward(source: replyToMessageId.messageId, threadId: threadId, grouping: .none, attributes: attributes, correlationId: nil))) } } // MARK: Nicegram (asCopy) @@ -378,7 +455,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } // MARK: Nicegram let localGroupingKey: Int64? = asCopy ? sourceMessage.groupingKey : nil - updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: threadId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: $0)) }, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []))) + updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: threadId.flatMap { EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: $0)), quote: nil) }, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []))) continue outer } } @@ -420,7 +497,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, globallyUniqueIds.append(randomId) switch message { - case let .message(text, requestedAttributes, inlineStickers, mediaReference, replyToMessageId, replyToStoryId, localGroupingKey, _, _): + case let .message(text, requestedAttributes, inlineStickers, mediaReference, threadId, replyToMessageId, replyToStoryId, localGroupingKey, _, _): for (_, file) in inlineStickers { transaction.storeMediaIfNotPresent(media: file) } @@ -489,12 +566,27 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, attributes.append(AutoremoveTimeoutMessageAttribute(timeout: peerAutoremoveTimeout, countdownBeginTime: nil)) } - if let replyToMessageId = replyToMessageId, replyToMessageId.peerId == peerId { + if let replyToMessageId = replyToMessageId { var threadMessageId: MessageId? - if let replyMessage = transaction.getMessage(replyToMessageId) { + var quote = replyToMessageId.quote + let isQuote = quote != nil + if let replyMessage = transaction.getMessage(replyToMessageId.messageId) { threadMessageId = replyMessage.effectiveReplyThreadMessageId + if quote == nil, replyToMessageId.messageId.peerId != peerId { + let nsText = replyMessage.text as NSString + var replyMedia: Media? + for m in replyMessage.media { + switch m { + case _ as TelegramMediaImage, _ as TelegramMediaFile: + replyMedia = m + default: + break + } + } + quote = EngineMessageReplyQuote(text: replyMessage.text, entities: messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: NSRange(location: 0, length: nsText.length), onlyQuoteable: true), media: replyMedia) + } } - attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: threadMessageId)) + attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) } if let replyToStoryId = replyToStoryId { attributes.append(ReplyStoryAttribute(storyId: replyToStoryId)) @@ -608,23 +700,25 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } - var threadId: Int64? - if let replyToMessageId = replyToMessageId { - if let message = transaction.getMessage(replyToMessageId) { - if let threadIdValue = message.threadId { - if threadIdValue == 1 { - if let channel = transaction.getPeer(message.id.peerId) as? TelegramChannel, channel.flags.contains(.isForum) { - threadId = threadIdValue - } else { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - threadId = makeMessageThreadId(replyToMessageId) + var threadId: Int64? = threadId + if threadId == nil { + if let replyToMessageId = replyToMessageId { + if let message = transaction.getMessage(replyToMessageId.messageId) { + if let threadIdValue = message.threadId { + if threadIdValue == 1 { + if let channel = transaction.getPeer(message.id.peerId) as? TelegramChannel, channel.flags.contains(.isForum) { + threadId = threadIdValue + } else { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + threadId = makeMessageThreadId(replyToMessageId.messageId) + } } + } else { + threadId = threadIdValue } - } else { - threadId = threadIdValue + } else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + threadId = makeMessageThreadId(replyToMessageId.messageId) } - } else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - threadId = makeMessageThreadId(replyToMessageId) } } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 91baa49b1ae..0e81fdcf7f9 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -95,6 +95,21 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(peer: inputPeer, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil)) } |> castError(PendingMessageUploadError.self), .text) + } else if let media = media.first as? TelegramMediaWebpage, case let .Loaded(content) = media.content { + return .signal(postbox.transaction { transaction -> PendingMessageUploadedContentResult in + var flags: Int32 = 0 + if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { + if let forceLargeMedia = attribute.forceLargeMedia { + if forceLargeMedia { + flags |= 1 << 0 + } else { + flags |= 1 << 1 + } + } + } + return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaWebPage(flags: flags, url: content.url), text), reuploadInfo: nil, cacheReferenceKey: nil)) + } + |> castError(PendingMessageUploadError.self), .text) } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { return .signal(mediaResult, .media) } else { @@ -222,6 +237,18 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } else if let media = media as? TelegramMediaDice { let inputDice = Api.InputMedia.inputMediaDice(emoticon: media.emoji) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) + } else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content { + var flags: Int32 = 0 + if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { + if let forceLargeMedia = attribute.forceLargeMedia { + if forceLargeMedia { + flags |= 1 << 0 + } else { + flags |= 1 << 1 + } + } + } + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaWebPage(flags: flags, url: content.url), text), reuploadInfo: nil, cacheReferenceKey: nil))) } else { return nil } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift index 5c53c72480a..ee1dc96a755 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift @@ -64,7 +64,7 @@ private final class PendingUpdateMessageManagerImpl { } } - func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool) { + func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { if let context = self.contexts[messageId] { self.contexts.removeValue(forKey: messageId) context.disposable.dispose() @@ -75,7 +75,7 @@ private final class PendingUpdateMessageManagerImpl { self.contexts[messageId] = context let queue = self.queue - disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: nil) + disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: nil) |> deliverOn(self.queue)).start(next: { [weak self, weak context] value in queue.async { guard let strongSelf = self, let initialContext = context else { @@ -163,9 +163,9 @@ public final class PendingUpdateMessageManager { }) } - public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false) { + public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, disableUrlPreview: Bool = false) { self.impl.with { impl in - impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview) + impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 3d3fd6be4af..f58525cf859 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -27,15 +27,15 @@ public enum RequestEditMessageError { case invalidGrouping } -func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { - return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) +func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal { + return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) } -func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal { - return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false) +func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal { + return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false) |> `catch` { error -> Signal in if case .invalidReference = error { - return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true) + return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true) } else { return .fail(error) } @@ -50,7 +50,7 @@ func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Networ } } -private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal { +private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal { let uploadedMedia: Signal switch media { case .keep: @@ -59,7 +59,11 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, case let .update(media): let generateUploadSignal: (Bool) -> Signal? = { forceReupload in let augmentedMedia = augmentMediaWithReference(media) - return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: []) + var attributes: [MessageAttribute] = [] + if let webpagePreviewAttribute = webpagePreviewAttribute { + attributes.append(webpagePreviewAttribute) + } + return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes) } if let uploadSignal = generateUploadSignal(forceReupload) { uploadedMedia = .single(.progress(0.027)) @@ -164,6 +168,12 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, flags |= Int32(1 << 15) } + if let webpagePreviewAttribute = webpagePreviewAttribute { + if webpagePreviewAttribute.leadingPreview { + flags |= Int32(1 << 16) + } + } + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime)) |> map { result -> Api.Updates? in return result diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index fb92377aca9..7e8f5e74404 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -164,7 +164,7 @@ public func standaloneSendEnqueueMessages( } if let replyToMessageId = message.replyToMessageId { - attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } if let forwardOptions = message.forwardOptions { attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions.hideNames, hideCaptions: forwardOptions.hideCaptions)) @@ -334,7 +334,7 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -356,7 +356,7 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -392,7 +392,7 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -407,18 +407,18 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if let replyMessageId = replyMessageId { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 replyTo = .inputReplyToStory(userId: inputUser, storyId: replyToStoryId.id) } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } sendMessageRequest = network.request(Api.functions.messages.sendScreenshotNotification(peer: inputPeer, replyTo: replyTo, randomId: uniqueId)) @@ -557,7 +557,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -575,7 +575,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/SecretChats/SetSecretChatMessageAutoremoveTimeoutInteractively.swift b/submodules/TelegramCore/Sources/SecretChats/SetSecretChatMessageAutoremoveTimeoutInteractively.swift index efd313df909..1474df556a7 100644 --- a/submodules/TelegramCore/Sources/SecretChats/SetSecretChatMessageAutoremoveTimeoutInteractively.swift +++ b/submodules/TelegramCore/Sources/SecretChats/SetSecretChatMessageAutoremoveTimeoutInteractively.swift @@ -20,7 +20,7 @@ func _internal_setSecretChatMessageAutoremoveTimeoutInteractively(transaction: T transaction.setPeerChatState(peerId, state: updatedState) } - let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.messageAutoremoveTimeoutUpdated(period: timeout == nil ? 0 : timeout!, autoSettingSource: nil))), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))]) + let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.messageAutoremoveTimeoutUpdated(period: timeout == nil ? 0 : timeout!, autoSettingSource: nil))), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))]) } } } @@ -34,7 +34,7 @@ func _internal_addSecretChatMessageScreenshot(account: Account, peerId: PeerId) default: break } - let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))]) + let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))]) } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index e1edecbb141..f195353990a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -989,7 +989,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if previousState.pts >= pts { } else if previousState.pts + ptsCount == pts { switch apiWebpage { - case let .webPageEmpty(id): + case let .webPageEmpty(flags, id, url): + let _ = flags + let _ = url updatedState.updateMedia(MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), media: nil) default: if let webpage = telegramMediaWebpageFromApiWebpage(apiWebpage, url: nil) { @@ -1119,7 +1121,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if updatedState.peers[peerId] == nil { updatedState.updatePeer(peerId, { peer in if peer == nil { - return TelegramUser(id: peerId, accessHash: nil, firstName: "Telegram Notifications", lastName: nil, username: nil, phone: nil, photo: [], botInfo: BotUserInfo(flags: [], inlinePlaceholder: nil), restrictionInfo: nil, flags: [.isVerified], emojiStatus: nil, usernames: [], storiesHidden: nil) + return TelegramUser(id: peerId, accessHash: nil, firstName: "Telegram Notifications", lastName: nil, username: nil, phone: nil, photo: [], botInfo: BotUserInfo(flags: [], inlinePlaceholder: nil), restrictionInfo: nil, flags: [.isVerified], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil) } else { return peer } @@ -1146,9 +1148,15 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: let messageText = text var medias: [Media] = [] - let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) + + if mediaValue is TelegramMediaWebpage { + if let webpageAttributes = webpageAttributes { + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: webpageAttributes.forceLargeMedia, isManuallyAdded: webpageAttributes.isManuallyAdded, isSafe: webpageAttributes.isSafe)) + } + } } if let expirationTimer = expirationTimer { attributes.append(AutoclearTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil)) @@ -1204,7 +1212,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } case let .updateWebPage(apiWebpage, _, _): switch apiWebpage { - case let .webPageEmpty(id): + case let .webPageEmpty(flags, id, url): + let _ = flags + let _ = url updatedState.updateMedia(MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), media: nil) default: if let webpage = telegramMediaWebpageFromApiWebpage(apiWebpage, url: nil) { @@ -1523,12 +1533,52 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: switch draft { case .draftMessageEmpty: inputState = nil - case let .draftMessage(_, replyToMsgId, message, entities, date): - var replyToMessageId: MessageId? - if let replyToMsgId = replyToMsgId { - replyToMessageId = MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + case let .draftMessage(_, replyToMsgHeader, message, entities, media, date): + let _ = media + var replySubject: EngineMessageReplySubject? + if let replyToMsgHeader = replyToMsgHeader { + switch replyToMsgHeader { + case let .inputReplyToMessage(_, replyToMsgId, topMsgId, replyToPeerId, quoteText, quoteEntities): + let _ = topMsgId + + var quote: EngineMessageReplyQuote? + if let quoteText = quoteText { + quote = EngineMessageReplyQuote( + text: quoteText, + entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), + media: nil + ) + } + + var parsedReplyToPeerId: PeerId? + switch replyToPeerId { + case let .inputPeerChannel(channelId, _): + parsedReplyToPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + case let .inputPeerChannelFromMessage(_, _, channelId): + parsedReplyToPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + case let .inputPeerChat(chatId): + parsedReplyToPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case .inputPeerEmpty: + break + case .inputPeerSelf: + parsedReplyToPeerId = accountPeerId + case let .inputPeerUser(userId, _): + parsedReplyToPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerUserFromMessage(_, _, userId): + parsedReplyToPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case .none: + break + } + + replySubject = EngineMessageReplySubject( + messageId: MessageId(peerId: parsedReplyToPeerId ?? peer.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), + quote: quote + ) + case .inputReplyToStory: + break + } } - inputState = SynchronizeableChatInputState(replyToMessageId: replyToMessageId, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil) + inputState = SynchronizeableChatInputState(replySubject: replySubject, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil) } var threadId: Int64? if let topMsgId = topMsgId { @@ -1757,7 +1807,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: |> mapToSignal { finalState in return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in - return resolveAssociatedStories(postbox: postbox, network: network, accountPeerId: accountPeerId, state: finalState) + return resolveAssociatedStories(postbox: postbox, network: network, accountPeerId: accountPeerId, state: resultingState) |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, resolveError -> AccountFinalState in @@ -2938,7 +2988,9 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views) case let .updateChannelWebPage(_, apiWebpage, _, _): switch apiWebpage { - case let .webPageEmpty(id): + case let .webPageEmpty(flags, id, url): + let _ = flags + let _ = url updatedState.updateMedia(MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), media: nil) default: if let webpage = telegramMediaWebpageFromApiWebpage(apiWebpage, url: nil) { @@ -4464,7 +4516,7 @@ func replayFinalState( } updatedExtendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let media = media { updatedExtendedMedia = .full(media: media) } else { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 9faf4f49f74..1601b20c240 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -311,11 +311,12 @@ public final class AccountStateManager { |> distinctUntilChanged |> mapToSignal { value -> Signal in if isMaxMessageId { - return network.request(Api.functions.messages.receivedMessages(maxId: value)) + return .complete() + /*return network.request(Api.functions.messages.receivedMessages(maxId: value)) |> ignoreValues |> `catch` { _ -> Signal in return .complete() - } + }*/ } else { if value == 0 { return .complete() diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index 7f165b1b8f5..726ccbef8ed 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -104,6 +104,7 @@ final class AccountTaskManager { tasks.add(managedFeaturedStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedProfilePhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedGroupPhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) + tasks.add(managedBackgroundIconEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedRecentReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconStatusEmoji, forceActualized: true).start()) tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconTopicEmoji, forceActualized: true).start()) diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index f934c9b6b2d..9e17bb41697 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -408,7 +408,10 @@ public final class AccountViewTracker { for messageId in addedMessageIds { if self.webpageDisposables[messageId] == nil { if let (_, url) = localWebpages[messageId] { - self.webpageDisposables[messageId] = (webpagePreview(account: account, url: url) |> mapToSignal { webpage -> Signal in + self.webpageDisposables[messageId] = (webpagePreview(account: account, url: url) |> mapToSignal { result -> Signal in + guard case let .result(webpage) = result else { + return .complete() + } return account.postbox.transaction { transaction -> Void in if let webpage = webpage { transaction.updateMessage(messageId, update: { currentMessage in diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 415773973d6..8407eaf2320 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -157,7 +157,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes text = updatedMessage.text forwardInfo = updatedMessage.forwardInfo } else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result { - let (mediaValue, _, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let mediaValue = mediaValue { media = [mediaValue] } else { diff --git a/submodules/TelegramCore/Sources/State/ChannelBoost.swift b/submodules/TelegramCore/Sources/State/ChannelBoost.swift index b09f8f4f2df..e6853b7f0f6 100644 --- a/submodules/TelegramCore/Sources/State/ChannelBoost.swift +++ b/submodules/TelegramCore/Sources/State/ChannelBoost.swift @@ -3,19 +3,47 @@ import TelegramApi import Postbox import SwiftSignalKit -public final class ChannelBoostStatus: Equatable { +public struct MyBoostStatus: Equatable { + public struct Boost: Equatable { + public let slot: Int32 + public let peer: EnginePeer? + public let date: Int32 + public let expires: Int32 + public let cooldownUntil: Int32? + + public init(slot: Int32, peer: EnginePeer?, date: Int32, expires: Int32, cooldownUntil: Int32?) { + self.slot = slot + self.peer = peer + self.date = date + self.expires = expires + self.cooldownUntil = cooldownUntil + } + } + + public let boosts: [Boost] +} + +public struct ChannelBoostStatus: Equatable { public let level: Int public let boosts: Int + public let giftBoosts: Int? public let currentLevelBoosts: Int public let nextLevelBoosts: Int? public let premiumAudience: StatsPercentValue? + public let url: String + public let prepaidGiveaways: [PrepaidGiveaway] + public let boostedByMe: Bool - public init(level: Int, boosts: Int, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?) { + public init(level: Int, boosts: Int, giftBoosts: Int?, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?, url: String, prepaidGiveaways: [PrepaidGiveaway], boostedByMe: Bool) { self.level = level self.boosts = boosts + self.giftBoosts = giftBoosts self.currentLevelBoosts = currentLevelBoosts self.nextLevelBoosts = nextLevelBoosts self.premiumAudience = premiumAudience + self.url = url + self.prepaidGiveaways = prepaidGiveaways + self.boostedByMe = boostedByMe } public static func ==(lhs: ChannelBoostStatus, rhs: ChannelBoostStatus) -> Bool { @@ -25,6 +53,9 @@ public final class ChannelBoostStatus: Equatable { if lhs.boosts != rhs.boosts { return false } + if lhs.giftBoosts != rhs.giftBoosts { + return false + } if lhs.currentLevelBoosts != rhs.currentLevelBoosts { return false } @@ -34,6 +65,15 @@ public final class ChannelBoostStatus: Equatable { if lhs.premiumAudience != rhs.premiumAudience { return false } + if lhs.url != rhs.url { + return false + } + if lhs.prepaidGiveaways != rhs.prepaidGiveaways { + return false + } + if lhs.boostedByMe != rhs.boostedByMe { + return false + } return true } } @@ -46,116 +86,66 @@ func _internal_getChannelBoostStatus(account: Account, peerId: PeerId) -> Signal guard let inputPeer = inputPeer else { return .single(nil) } - return account.network.request(Api.functions.stories.getBoostsStatus(peer: inputPeer)) + return account.network.request(Api.functions.premium.getBoostsStatus(peer: inputPeer)) |> map(Optional.init) - |> `catch` { _ -> Signal in + |> `catch` { _ -> Signal in return .single(nil) } |> map { result -> ChannelBoostStatus? in guard let result = result else { return nil } - switch result { - case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience): - return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) })) + case let .boostsStatus(flags, level, currentLevelBoosts, boosts, giftBoosts, nextLevelBoosts, premiumAudience, boostUrl, prepaidGiveaways, myBoostSlots): + let _ = myBoostSlots + return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), giftBoosts: giftBoosts.flatMap(Int.init), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: boostUrl, prepaidGiveaways: prepaidGiveaways?.map({ PrepaidGiveaway(apiPrepaidGiveaway: $0) }) ?? [], boostedByMe: (flags & (1 << 2)) != 0) } } } } -public enum CanApplyBoostStatus { - public enum ErrorReason { - case generic - case premiumRequired - case floodWait(Int32) - case peerBoostAlreadyActive - case giftedPremiumNotAllowed - } - - case ok - case replace(currentBoost: EnginePeer) - case error(ErrorReason) -} - -func _internal_canApplyChannelBoost(account: Account, peerId: PeerId) -> Signal { +func _internal_applyChannelBoost(account: Account, peerId: PeerId, slots: [Int32]) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputPeer -> Signal in + |> mapToSignal { inputPeer -> Signal in guard let inputPeer = inputPeer else { - return .single(.error(.generic)) - } - return account.network.request(Api.functions.stories.canApplyBoost(peer: inputPeer), automaticFloodWait: false) - |> map { result -> (Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?) in - return (result, nil) - } - |> `catch` { error -> Signal<(Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?), NoError> in - let reason: CanApplyBoostStatus.ErrorReason - if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" { - reason = .premiumRequired - } else if error.errorDescription.hasPrefix("FLOOD_WAIT_") { - let errorText = error.errorDescription ?? "" - if let underscoreIndex = errorText.lastIndex(of: "_") { - let timeoutText = errorText[errorText.index(after: underscoreIndex)...] - if let timeoutValue = Int32(String(timeoutText)) { - reason = .floodWait(timeoutValue) - } else { - reason = .generic - } - } else { - reason = .generic - } - } else if error.errorDescription == "SAME_BOOST_ALREADY_ACTIVE" || error.errorDescription == "BOOST_NOT_MODIFIED" { - reason = .peerBoostAlreadyActive - } else if error.errorDescription == "PREMIUM_GIFTED_NOT_ALLOWED" { - reason = .giftedPremiumNotAllowed - } else { - reason = .generic - } - - return .single((nil, reason)) + return .complete() } - |> mapToSignal { result, errorReason -> Signal in - guard let result = result else { - return .single(.error(errorReason ?? .generic)) - } - - return account.postbox.transaction { transaction -> CanApplyBoostStatus in - switch result { - case .canApplyBoostOk: - return .ok - case let .canApplyBoostReplace(currentBoost, chats): - updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(transaction: transaction, chats: chats, users: [])) - - if let peer = transaction.getPeer(currentBoost.peerId) { - return .replace(currentBoost: EnginePeer(peer)) - } else { - return .error(.generic) - } + var flags: Int32 = 0 + if !slots.isEmpty { + flags |= (1 << 0) + } + + return account.network.request(Api.functions.premium.applyBoost(flags: flags, slots: !slots.isEmpty ? slots : nil, peer: inputPeer)) + |> map (Optional.init) + |> `catch` { error -> Signal in + return .complete() + } + |> mapToSignal { result -> Signal in + if let result = result { + return account.postbox.transaction { transaction -> MyBoostStatus? in + return MyBoostStatus(apiMyBoostStatus: result, accountPeerId: account.peerId, transaction: transaction) } + } else { + return .single(nil) } } } } -func _internal_applyChannelBoost(account: Account, peerId: PeerId) -> Signal { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) +func _internal_getMyBoostStatus(account: Account) -> Signal { + return account.network.request(Api.functions.premium.getMyBoosts()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .single(false) + |> mapToSignal { result -> Signal in + guard let result = result else { + return .single(nil) } - return account.network.request(Api.functions.stories.applyBoost(peer: inputPeer)) - |> `catch` { error -> Signal in - return .single(.boolFalse) - } - |> map { result -> Bool in - if case .boolTrue = result { - return true - } - return false + return account.postbox.transaction { transaction -> MyBoostStatus? in + return MyBoostStatus(apiMyBoostStatus: result, accountPeerId: account.peerId, transaction: transaction) } } } @@ -164,37 +154,36 @@ private final class ChannelBoostersContextImpl { private let queue: Queue private let account: Account private let peerId: PeerId + private let gift: Bool private let disposable = MetaDisposable() private let updateDisposables = DisposableSet() private var isLoadingMore: Bool = false private var hasLoadedOnce: Bool = false private var canLoadMore: Bool = true private var loadedFromCache = false - private var results: [ChannelBoostersContext.State.Booster] = [] + private var results: [ChannelBoostersContext.State.Boost] = [] private var count: Int32 private var lastOffset: String? private var populateCache: Bool = true let state = Promise() - init(queue: Queue, account: Account, peerId: PeerId) { + init(queue: Queue, account: Account, peerId: PeerId, gift: Bool) { self.queue = queue self.account = account self.peerId = peerId + self.gift = gift self.count = 0 self.isLoadingMore = true - self.disposable.set((account.postbox.transaction { transaction -> (peers: [ChannelBoostersContext.State.Booster], count: Int32, canLoadMore: Bool)? in - let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)))?.get(CachedChannelBoosters.self) - if let cachedResult = cachedResult { - var result: [ChannelBoostersContext.State.Booster] = [] - for peerId in cachedResult.peerIds { - if let peer = transaction.getPeer(peerId), let expires = cachedResult.dates[peerId] { - result.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) - } else { - return nil - } + self.disposable.set((account.postbox.transaction { transaction -> (peers: [ChannelBoostersContext.State.Boost], count: Int32, canLoadMore: Bool)? in + let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)))?.get(CachedChannelBoosters.self) + if let cachedResult = cachedResult, !gift { + var result: [ChannelBoostersContext.State.Boost] = [] + for boost in cachedResult.boosts { + let peer = boost.peerId.flatMap { transaction.getPeer($0) } + result.append(ChannelBoostersContext.State.Boost(flags: ChannelBoostersContext.State.Boost.Flags(rawValue: boost.flags), id: boost.id, peer: peer.flatMap { EnginePeer($0) }, date: boost.date, expires: boost.expires, multiplier: boost.multiplier, slug: boost.slug)) } return (result, cachedResult.count, true) } else { @@ -230,13 +219,14 @@ private final class ChannelBoostersContextImpl { } func loadMore() { - if self.isLoadingMore { + if self.isLoadingMore || !self.canLoadMore { return } self.isLoadingMore = true let account = self.account let accountPeerId = account.peerId let peerId = self.peerId + let gift = self.gift let populateCache = self.populateCache if self.loadedFromCache { @@ -247,43 +237,58 @@ private final class ChannelBoostersContextImpl { self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in + |> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Boost], Int32, String?), NoError> in if let inputPeer = inputPeer { let offset = lastOffset ?? "" let limit: Int32 = lastOffset == nil ? 25 : 50 - let signal = account.network.request(Api.functions.stories.getBoostersList(peer: inputPeer, offset: offset, limit: limit)) + var flags: Int32 = 0 + if gift { + flags |= (1 << 0) + } + let signal = account.network.request(Api.functions.premium.getBoostsList(flags: flags, peer: inputPeer, offset: offset, limit: limit)) |> map(Optional.init) - |> `catch` { _ -> Signal in + |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in - return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Booster], Int32, String?) in + |> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Boost], Int32, String?), NoError> in + return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Boost], Int32, String?) in guard let result = result else { return ([], 0, nil) } switch result { - case let .boostersList(_, count, boosters, nextOffset, users): + case let .boostsList(_, count, boosts, nextOffset, users): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) - var resultBoosters: [ChannelBoostersContext.State.Booster] = [] - for booster in boosters { - let peerId: EnginePeer.Id - let expires: Int32 - switch booster { - case let .booster(userId, expiresValue): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - expires = expiresValue - } - if let peer = transaction.getPeer(peerId) { - resultBoosters.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) + var resultBoosts: [ChannelBoostersContext.State.Boost] = [] + for boost in boosts { + switch boost { + case let .boost(flags, id, userId, giveawayMessageId, date, expires, usedGiftSlug, multiplier): + var boostFlags: ChannelBoostersContext.State.Boost.Flags = [] + var boostPeer: EnginePeer? + if let userId = userId { + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + if let peer = transaction.getPeer(peerId) { + boostPeer = EnginePeer(peer) + } + } + if (flags & (1 << 1)) != 0 { + boostFlags.insert(.isGift) + } + if (flags & (1 << 2)) != 0 { + boostFlags.insert(.isGiveaway) + } + if (flags & (1 << 3)) != 0 { + boostFlags.insert(.isUnclaimed) + } + resultBoosts.append(ChannelBoostersContext.State.Boost(flags: boostFlags, id: id, peer: boostPeer, date: date, expires: expires, multiplier: multiplier ?? 1, slug: usedGiftSlug, giveawayMessageId: giveawayMessageId.flatMap { EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) })) } } if populateCache { - if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) + if let entry = CodableEntry(CachedChannelBoosters(channelPeerId: peerId, boosts: resultBoosts, count: count)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) } } - return (resultBoosters, count, nextOffset) + return (resultBoosts, count, nextOffset) } } } @@ -301,20 +306,24 @@ private final class ChannelBoostersContextImpl { strongSelf.populateCache = false strongSelf.results.removeAll() } - var existingIds = Set(strongSelf.results.map { $0.peer.id }) for booster in boosters { - if !existingIds.contains(booster.peer.id) { - strongSelf.results.append(booster) - existingIds.insert(booster.peer.id) - } + strongSelf.results.append(booster) } strongSelf.isLoadingMore = false strongSelf.hasLoadedOnce = true - strongSelf.canLoadMore = !boosters.isEmpty + strongSelf.canLoadMore = !boosters.isEmpty && nextOffset != nil if strongSelf.canLoadMore { - strongSelf.count = max(updatedCount, Int32(strongSelf.results.count)) + var resultsCount: Int32 = 0 + for result in strongSelf.results { + resultsCount += result.multiplier + } + strongSelf.count = max(updatedCount, resultsCount) } else { - strongSelf.count = Int32(strongSelf.results.count) + var resultsCount: Int32 = 0 + for result in strongSelf.results { + resultsCount += result.multiplier + } + strongSelf.count = resultsCount } strongSelf.updateState() })) @@ -327,34 +336,52 @@ private final class ChannelBoostersContextImpl { } let peerId = self.peerId - let resultBoosters = Array(self.results.prefix(50)) + let resultBoosts = Array(self.results.prefix(50)) let count = self.count self.updateDisposables.add(self.account.postbox.transaction({ transaction in - if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) + if let entry = CodableEntry(CachedChannelBoosters(channelPeerId: peerId, boosts: resultBoosts, count: count)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) } }).start()) } private func updateState() { - self.state.set(.single(ChannelBoostersContext.State(boosters: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count))) + self.state.set(.single(ChannelBoostersContext.State(boosts: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count))) } } public final class ChannelBoostersContext { public struct State: Equatable { - public struct Booster: Equatable { - public var peer: EnginePeer + public struct Boost: Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isGift = Flags(rawValue: 1 << 0) + public static let isGiveaway = Flags(rawValue: 1 << 1) + public static let isUnclaimed = Flags(rawValue: 1 << 2) + } + + public var flags: Flags + public var id: String + public var peer: EnginePeer? + public var date: Int32 public var expires: Int32 + public var multiplier: Int32 + public var slug: String? + public var giveawayMessageId: EngineMessage.Id? } - public var boosters: [Booster] + public var boosts: [Boost] public var isLoadingMore: Bool public var hasLoadedOnce: Bool public var canLoadMore: Bool public var count: Int32 - public static var Empty = State(boosters: [], isLoadingMore: false, hasLoadedOnce: true, canLoadMore: false, count: 0) - public static var Loading = State(boosters: [], isLoadingMore: false, hasLoadedOnce: false, canLoadMore: false, count: 0) + public static var Empty = State(boosts: [], isLoadingMore: false, hasLoadedOnce: true, canLoadMore: false, count: 0) + public static var Loading = State(boosts: [], isLoadingMore: false, hasLoadedOnce: false, canLoadMore: false, count: 0) } @@ -373,10 +400,10 @@ public final class ChannelBoostersContext { } } - public init(account: Account, peerId: PeerId) { + public init(account: Account, peerId: PeerId, gift: Bool) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return ChannelBoostersContextImpl(queue: queue, account: account, peerId: peerId) + return ChannelBoostersContextImpl(queue: queue, account: account, peerId: peerId, gift: gift) }) } @@ -395,38 +422,76 @@ public final class ChannelBoostersContext { private final class CachedChannelBoosters: Codable { private enum CodingKeys: String, CodingKey { - case peerIds - case expires + case boosts case count } - private struct DictionaryPair: Codable, Hashable { - var key: Int64 - var value: String + fileprivate struct CachedBoost: Codable, Hashable { + private enum CodingKeys: String, CodingKey { + case flags + case id + case peerId + case date + case expires + case multiplier + case slug + case channelPeerId + case giveawayMessageId + } + + var flags: Int32 + var id: String + var peerId: EnginePeer.Id? + var date: Int32 + var expires: Int32 + var multiplier: Int32 + var slug: String? + var channelPeerId: EnginePeer.Id + var giveawayMessageId: EngineMessage.Id? - init(_ key: Int64, value: String) { - self.key = key - self.value = value + init(flags: Int32, id: String, peerId: EnginePeer.Id?, date: Int32, expires: Int32, multiplier: Int32, slug: String?, channelPeerId: EnginePeer.Id, giveawayMessageId: EngineMessage.Id?) { + self.flags = flags + self.id = id + self.peerId = peerId + self.date = date + self.expires = expires + self.multiplier = multiplier + self.slug = slug + self.channelPeerId = channelPeerId + self.giveawayMessageId = giveawayMessageId } init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: StringCodingKey.self) + let container = try decoder.container(keyedBy: CodingKeys.self) - self.key = try container.decode(Int64.self, forKey: "k") - self.value = try container.decode(String.self, forKey: "v") + self.flags = try container.decode(Int32.self, forKey: .flags) + self.id = try container.decode(String.self, forKey: .id) + self.peerId = try container.decodeIfPresent(Int64.self, forKey: .peerId).flatMap { EnginePeer.Id($0) } + self.date = try container.decode(Int32.self, forKey: .date) + self.expires = try container.decode(Int32.self, forKey: .expires) + self.multiplier = try container.decode(Int32.self, forKey: .multiplier) + self.slug = try container.decodeIfPresent(String.self, forKey: .slug) + self.channelPeerId = EnginePeer.Id(try container.decode(Int64.self, forKey: .channelPeerId)) + self.giveawayMessageId = try container.decodeIfPresent(Int32.self, forKey: .giveawayMessageId).flatMap { EngineMessage.Id(peerId: self.channelPeerId, namespace: Namespaces.Message.Cloud, id: $0) } } func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: StringCodingKey.self) + var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.key, forKey: "k") - try container.encode(self.value, forKey: "v") + try container.encode(self.flags, forKey: .flags) + try container.encode(self.id, forKey: .id) + try container.encodeIfPresent(self.peerId?.toInt64(), forKey: .peerId) + try container.encode(self.date, forKey: .date) + try container.encode(self.expires, forKey: .expires) + try container.encode(self.multiplier, forKey: .multiplier) + try container.encodeIfPresent(self.slug, forKey: .slug) + try container.encode(self.channelPeerId.toInt64(), forKey: .channelPeerId) + try container.encodeIfPresent(self.giveawayMessageId?.id, forKey: .giveawayMessageId) } } - let peerIds: [EnginePeer.Id] - let dates: [EnginePeer.Id: Int32] - let count: Int32 + fileprivate let boosts: [CachedBoost] + fileprivate let count: Int32 static func key(peerId: EnginePeer.Id) -> ValueBoxKey { let key = ValueBoxKey(length: 8) @@ -434,50 +499,44 @@ private final class CachedChannelBoosters: Codable { return key } - init(boosters: [ChannelBoostersContext.State.Booster], count: Int32) { - self.peerIds = boosters.map { $0.peer.id } - self.dates = boosters.reduce(into: [EnginePeer.Id: Int32]()) { - $0[$1.peer.id] = $1.expires - } - self.count = count - } - - init(peerIds: [PeerId], dates: [PeerId: Int32], count: Int32) { - self.peerIds = peerIds - self.dates = dates + init(channelPeerId: EnginePeer.Id, boosts: [ChannelBoostersContext.State.Boost], count: Int32) { + self.boosts = boosts.map { CachedBoost(flags: $0.flags.rawValue, id: $0.id, peerId: $0.peer?.id, date: $0.date, expires: $0.expires, multiplier: $0.multiplier, slug: $0.slug, channelPeerId: channelPeerId, giveawayMessageId: $0.giveawayMessageId) } self.count = count } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.peerIds = (try container.decode([Int64].self, forKey: .peerIds)).map(EnginePeer.Id.init) - - var dates: [EnginePeer.Id: Int32] = [:] - let datesArray = try container.decode([Int64].self, forKey: .expires) - for index in stride(from: 0, to: datesArray.endIndex, by: 2) { - let userId = datesArray[index] - let date = datesArray[index + 1] - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - dates[peerId] = Int32(clamping: date) - } - self.dates = dates - + self.boosts = (try container.decode([CachedBoost].self, forKey: .boosts)) self.count = try container.decode(Int32.self, forKey: .count) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds) - - var dates: [Int64] = [] - for (peerId, date) in self.dates { - dates.append(peerId.id._internalGetInt64Value()) - dates.append(Int64(date)) - } - - try container.encode(dates, forKey: .expires) + try container.encode(self.boosts, forKey: .boosts) try container.encode(self.count, forKey: .count) } } + +extension MyBoostStatus { + init(apiMyBoostStatus: Api.premium.MyBoosts, accountPeerId: PeerId, transaction: Transaction) { + var boostsResult: [MyBoostStatus.Boost] = [] + switch apiMyBoostStatus { + case let .myBoosts(myBoosts, chats, users): + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + for boost in myBoosts { + switch boost { + case let .myBoost(_, slot, peer, date, expires, cooldownUntilDate): + var boostPeer: EnginePeer? + if let peerId = peer?.peerId, let peer = transaction.getPeer(peerId) { + boostPeer = EnginePeer(peer) + } + boostsResult.append(MyBoostStatus.Boost(slot: slot, peer: boostPeer, date: date, expires: expires, cooldownUntil: cooldownUntilDate)) + } + } + } + self.boosts = boostsResult + } +} diff --git a/submodules/TelegramCore/Sources/State/ManagedAccountPresence.swift b/submodules/TelegramCore/Sources/State/ManagedAccountPresence.swift index b14f0fbc95a..8c8a4fa31d0 100644 --- a/submodules/TelegramCore/Sources/State/ManagedAccountPresence.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAccountPresence.swift @@ -16,6 +16,8 @@ private final class AccountPresenceManagerImpl { private let currentRequestDisposable = MetaDisposable() private var onlineTimer: SignalKitTimer? + private var wasOnline: Bool = false + init(queue: Queue, shouldKeepOnlinePresence: Signal, network: Network) { self.queue = queue self.network = network @@ -23,7 +25,13 @@ private final class AccountPresenceManagerImpl { self.shouldKeepOnlinePresenceDisposable = (shouldKeepOnlinePresence |> distinctUntilChanged |> deliverOn(self.queue)).start(next: { [weak self] value in - self?.updatePresence(value) + guard let `self` = self else { + return + } + if self.wasOnline != value { + self.wasOnline = value + self.updatePresence(value) + } }) } diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 48385da8c6e..7bc4fd6b4d9 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -318,6 +318,34 @@ func managedGroupPhotoEmoji(postbox: Postbox, network: Network) -> Signal then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } +func managedBackgroundIconEmoji(postbox: Postbox, network: Network) -> Signal { + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedBackgroundIconEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.account.getDefaultBackgroundEmojis(hash: hash)) + |> retryRequest + |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in + switch result { + case .emojiListNotModified: + return .single(nil) + case let .emojiList(_, documentIds): + return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: documentIds) + |> map { files -> [OrderedItemListEntry] in + var items: [OrderedItemListEntry] = [] + for fileId in documentIds { + guard let file = files[fileId] else { + continue + } + if let entry = CodableEntry(RecentMediaItem(file)) { + items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry)) + } + } + return items + } + } + } + }) + return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} + func managedRecentReactions(postbox: Postbox, network: Network) -> Signal { let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentReactions, extractItemId: { rawId in switch RecentReactionItemId(rawId).id { diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift index 47dfeebad79..f4aed3660be 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift @@ -141,19 +141,72 @@ private func synchronizeChatInputState(transaction: Transaction, postbox: Postbo if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var flags: Int32 = 0 if let inputState = inputState { - if inputState.replyToMessageId != nil { - flags |= (1 << 0) - } if !inputState.entities.isEmpty { flags |= (1 << 3) } } var topMsgId: Int32? if let threadId = threadId { - flags |= (1 << 2) topMsgId = Int32(clamping: threadId) } - return network.request(Api.functions.messages.saveDraft(flags: flags, replyToMsgId: inputState?.replyToMessageId?.id, topMsgId: topMsgId, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()))) + + var replyTo: Api.InputReplyTo? + if let replySubject = inputState?.replySubject { + flags |= 1 << 0 + + var innerFlags: Int32 = 0 + //inputReplyToMessage#73ec805 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector = InputReplyTo; + var replyToPeer: Api.InputPeer? + var discard = false + if replySubject.messageId.peerId != peerId { + replyToPeer = transaction.getPeer(replySubject.messageId.peerId).flatMap(apiInputPeer) + if replyToPeer == nil { + discard = true + } + } + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replySubject.quote { + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + if replyToPeer != nil { + innerFlags |= 1 << 1 + } + if quoteText != nil { + innerFlags |= 1 << 2 + } + if quoteEntities != nil { + innerFlags |= 1 << 3 + } + + if !discard { + replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: replySubject.messageId.id, topMsgId: topMsgId, replyToPeerId: replyToPeer, quoteText: quoteText, quoteEntities: quoteEntities) + } + } else if let topMsgId = topMsgId { + flags |= 1 << 0 + + var innerFlags: Int32 = 0 + innerFlags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: topMsgId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + } + + return network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()), media: nil)) |> delay(2.0, queue: Queue.concurrentDefaultQueue()) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index bc3ec25459b..3c3bd5ae1a4 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -779,6 +779,8 @@ public final class PendingMessageManager { var hideSendersNames = false var hideCaptions = false var replyMessageId: Int32? + var replyPeerId: PeerId? + var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var scheduleTime: Int32? var sendAsPeerId: PeerId? @@ -788,6 +790,12 @@ public final class PendingMessageManager { for attribute in messages[0].0.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute { replyMessageId = replyAttribute.messageId.id + if peerId != replyAttribute.messageId.peerId { + replyPeerId = replyAttribute.messageId.peerId + } + if replyAttribute.isQuote { + replyQuote = replyAttribute.quote + } } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let _ = attribute as? ForwardSourceInfoAttribute { @@ -931,7 +939,38 @@ public final class PendingMessageManager { if topMsgId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId) + + var replyToPeerId: Api.InputPeer? + if let replyPeerId = replyPeerId { + replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer) + } + if replyToPeerId != nil { + replyFlags |= 1 << 1 + } + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -1108,6 +1147,8 @@ public final class PendingMessageManager { var forwardSourceInfoAttribute: ForwardSourceInfoAttribute? var messageEntities: [Api.MessageEntity]? var replyMessageId: Int32? + var replyPeerId: PeerId? + var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var scheduleTime: Int32? var sendAsPeerId: PeerId? @@ -1118,6 +1159,12 @@ public final class PendingMessageManager { for attribute in message.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute { replyMessageId = replyAttribute.messageId.id + if peer.id != replyAttribute.messageId.peerId { + replyPeerId = replyAttribute.messageId.peerId + } + if replyAttribute.isQuote { + replyQuote = replyAttribute.quote + } } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let outgoingInfo = attribute as? OutgoingMessageInfoAttribute { @@ -1178,7 +1225,38 @@ public final class PendingMessageManager { if message.threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:))) + + var replyToPeerId: Api.InputPeer? + if let replyPeerId = replyPeerId { + replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer) + } + if replyToPeerId != nil { + replyFlags |= 1 << 1 + } + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -1186,6 +1264,12 @@ public final class PendingMessageManager { } } + if let attribute = message.webpagePreviewAttribute { + if attribute.leadingPreview { + flags |= 1 << 16 + } + } + sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), info: .acknowledgement, tag: dependencyTag) case let .media(inputMedia, text): if bubbleUpEmojiOrStickersets { @@ -1200,13 +1284,50 @@ public final class PendingMessageManager { if message.threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:))) + + var replyToPeerId: Api.InputPeer? + if let replyPeerId = replyPeerId { + replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer) + } + if replyToPeerId != nil { + replyFlags |= 1 << 1 + } + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 replyTo = .inputReplyToStory(userId: inputUser, storyId: replyToStoryId.id) } } + + if let attribute = message.webpagePreviewAttribute { + if attribute.leadingPreview { + flags |= 1 << 16 + } + } sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag) |> map(NetworkRequestResult.result) @@ -1236,7 +1357,38 @@ public final class PendingMessageManager { if message.threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:))) + + var replyToPeerId: Api.InputPeer? + if let replyPeerId = replyPeerId { + replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer) + } + if replyToPeerId != nil { + replyFlags |= 1 << 1 + } + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -1251,18 +1403,18 @@ public final class PendingMessageManager { if let replyMessageId = replyMessageId { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 replyTo = .inputReplyToStory(userId: inputUser, storyId: replyToStoryId.id) } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } sendMessageRequest = network.request(Api.functions.messages.sendScreenshotNotification(peer: inputPeer, replyTo: replyTo, randomId: uniqueId)) diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 1151ec65f25..7724ed9162d 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -881,7 +881,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1113,7 +1113,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1392,7 +1392,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1593,7 +1593,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 9ddebaba24c..ec47cea9fef 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 164 + return 166 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 0974d8e646d..fbd58bf03b5 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -57,7 +57,8 @@ extension Api.MessageMedia { return collectPreCachedResources(for: document) } return nil - case let .messageMediaWebPage(webPage): + case let .messageMediaWebPage(flags, webPage): + let _ = flags var result: [(MediaResource, Data)]? switch webPage { case let .webPage(_, _, _, _, _, _, _, _, _, photo, _, _, _, _, _, _, document, _, _): @@ -181,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)) @@ -192,7 +193,7 @@ extension Api.Chat { extension Api.User { var peerId: PeerId { switch self { - case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) case let .userEmpty(id): return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index bf24347c6f2..b5f99b73926 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -22,6 +22,10 @@ public struct UserLimitsConfiguration: Equatable { public let maxStoriesWeeklyCount: Int32 public let maxStoriesMonthlyCount: Int32 public let maxStoriesSuggestedReactions: Int32 + public let maxGiveawayChannelsCount: Int32 + public let maxGiveawayCountriesCount: Int32 + public let maxGiveawayPeriodSeconds: Int32 + public let minChannelNameColorLevel: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -44,7 +48,11 @@ public struct UserLimitsConfiguration: Equatable { maxExpiringStoriesCount: 3, maxStoriesWeeklyCount: 7, maxStoriesMonthlyCount: 30, - maxStoriesSuggestedReactions: 1 + maxStoriesSuggestedReactions: 1, + maxGiveawayChannelsCount: 10, + maxGiveawayCountriesCount: 10, + maxGiveawayPeriodSeconds: 86400 * 7, + minChannelNameColorLevel: 10 ) } @@ -68,7 +76,11 @@ public struct UserLimitsConfiguration: Equatable { maxExpiringStoriesCount: Int32, maxStoriesWeeklyCount: Int32, maxStoriesMonthlyCount: Int32, - maxStoriesSuggestedReactions: Int32 + maxStoriesSuggestedReactions: Int32, + maxGiveawayChannelsCount: Int32, + maxGiveawayCountriesCount: Int32, + maxGiveawayPeriodSeconds: Int32, + minChannelNameColorLevel: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -90,6 +102,10 @@ public struct UserLimitsConfiguration: Equatable { self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions + self.maxGiveawayChannelsCount = maxGiveawayChannelsCount + self.maxGiveawayCountriesCount = maxGiveawayCountriesCount + self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds + self.minChannelNameColorLevel = minChannelNameColorLevel } } @@ -134,5 +150,9 @@ extension UserLimitsConfiguration { self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount) self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount) self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount) + self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount) + self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount) + self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds) + self.minChannelNameColorLevel = getGeneralValue("channel_color_level_min", orElse: defaultValue.minChannelNameColorLevel) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 4e3e4144273..b693a9c9336 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -114,6 +114,59 @@ public struct CachedPremiumGiftOption: Equatable, PostboxCoding { } } +public enum PeerNameColor: Equatable { + case red + case orange + case violet + case green + case cyan + case blue + case pink + case other(Int32) + + public init(rawValue: Int32) { + switch rawValue { + case 0: + self = .red + case 1: + self = .orange + case 2: + self = .violet + case 3: + self = .green + case 4: + self = .cyan + case 5: + self = .blue + case 6: + self = .pink + default: + self = .other(rawValue) + } + } + + public var rawValue: Int32 { + switch self { + case .red: + return 0 + case .orange: + return 1 + case .violet: + return 2 + case .green: + return 3 + case .cyan: + return 4 + case .blue: + return 5 + case .pink: + return 6 + case let .other(value): + return value + } + } +} + public struct PeerEmojiStatus: Equatable, Codable { public var fileId: Int64 public var expirationDate: Int32? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_LoggingSettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LoggingSettings.swift index 7b26a6324cf..2516e3140ad 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_LoggingSettings.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LoggingSettings.swift @@ -6,7 +6,7 @@ public final class LoggingSettings: Codable { public let redactSensitiveData: Bool #if DEBUG - public static var defaultSettings = LoggingSettings(logToFile: true, logToConsole: true, redactSensitiveData: true) + public static var defaultSettings = LoggingSettings(logToFile: false, logToConsole: false, redactSensitiveData: true) #else public static var defaultSettings = LoggingSettings(logToFile: false, logToConsole: false, redactSensitiveData: true) #endif diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index e87c9baea4d..3c308396b0a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -80,6 +80,7 @@ public struct Namespaces { public static let CloudFeaturedProfilePhotoEmoji: Int32 = 23 public static let CloudFeaturedGroupPhotoEmoji: Int32 = 24 public static let NewSessionReviews: Int32 = 25 + public static let CloudFeaturedBackgroundIconEmoji: Int32 = 26 } public struct CachedItemCollection { @@ -110,7 +111,8 @@ public struct Namespaces { public static let cachedPeerStoryListHeads: Int8 = 27 public static let displayedStoryNotifications: Int8 = 28 public static let storySendAsPeerIds: Int8 = 29 - public static let cachedChannelBoosters: Int8 = 30 + public static let cachedChannelBoosts: Int8 = 31 + public static let displayedMessageNotifications: Int8 = 32 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingChatContextResultMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingChatContextResultMessageAttribute.swift index b6117cd9599..aa9d7975513 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingChatContextResultMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingChatContextResultMessageAttribute.swift @@ -5,22 +5,30 @@ public class OutgoingChatContextResultMessageAttribute: MessageAttribute { public let queryId: Int64 public let id: String public let hideVia: Bool + public let webpageUrl: String? - public init(queryId: Int64, id: String, hideVia: Bool) { + public init(queryId: Int64, id: String, hideVia: Bool, webpageUrl: String?) { self.queryId = queryId self.id = id self.hideVia = hideVia + self.webpageUrl = webpageUrl } required public init(decoder: PostboxDecoder) { self.queryId = decoder.decodeInt64ForKey("q", orElse: 0) self.id = decoder.decodeStringForKey("i", orElse: "") self.hideVia = decoder.decodeBoolForKey("v", orElse: false) + self.webpageUrl = decoder.decodeOptionalStringForKey("wurl") } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.queryId, forKey: "q") encoder.encodeString(self.id, forKey: "i") encoder.encodeBool(self.hideVia, forKey: "v") + if let webpageUrl = self.webpageUrl { + encoder.encodeString(webpageUrl, forKey: "wurl") + } else { + encoder.encodeNil(forKey: "wurl") + } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift index c3b12f7f435..b5918615dd1 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift @@ -1,17 +1,22 @@ import Foundation import Postbox +import TelegramApi public class ReplyMessageAttribute: MessageAttribute { public let messageId: MessageId public let threadMessageId: MessageId? + public let quote: EngineMessageReplyQuote? + public let isQuote: Bool public var associatedMessageIds: [MessageId] { return [self.messageId] } - public init(messageId: MessageId, threadMessageId: MessageId?) { + public init(messageId: MessageId, threadMessageId: MessageId?, quote: EngineMessageReplyQuote?, isQuote: Bool) { self.messageId = messageId self.threadMessageId = threadMessageId + self.quote = quote + self.isQuote = isQuote } required public init(decoder: PostboxDecoder) { @@ -23,6 +28,9 @@ public class ReplyMessageAttribute: MessageAttribute { } else { self.threadMessageId = nil } + + self.quote = decoder.decodeCodable(EngineMessageReplyQuote.self, forKey: "qu") + self.isQuote = decoder.decodeBoolForKey("iq", orElse: self.quote != nil) } public func encode(_ encoder: PostboxEncoder) { @@ -34,6 +42,76 @@ public class ReplyMessageAttribute: MessageAttribute { encoder.encodeInt64(threadNamespaceAndId, forKey: "ti") encoder.encodeInt64(threadMessageId.peerId.toInt64(), forKey: "tp") } + if let quote = self.quote { + encoder.encodeCodable(quote, forKey: "qu") + } else { + encoder.encodeNil(forKey: "qu") + } + encoder.encodeBool(self.isQuote, forKey: "iq") + } +} + +public class QuotedReplyMessageAttribute: MessageAttribute { + public let peerId: PeerId? + public let authorName: String? + public let quote: EngineMessageReplyQuote? + public let isQuote: Bool + + public var associatedMessageIds: [MessageId] { + return [] + } + + public var associatedPeerIds: [PeerId] { + if let peerId = self.peerId { + return [peerId] + } else { + return [] + } + } + + public init(peerId: PeerId?, authorName: String?, quote: EngineMessageReplyQuote?, isQuote: Bool) { + self.peerId = peerId + self.authorName = authorName + self.quote = quote + self.isQuote = isQuote + } + + required public init(decoder: PostboxDecoder) { + self.peerId = decoder.decodeOptionalInt64ForKey("p").flatMap(PeerId.init) + self.authorName = decoder.decodeOptionalStringForKey("a") + self.quote = decoder.decodeCodable(EngineMessageReplyQuote.self, forKey: "qu") + self.isQuote = decoder.decodeBoolForKey("iq", orElse: true) + } + + public func encode(_ encoder: PostboxEncoder) { + if let peerId = self.peerId { + encoder.encodeInt64(peerId.toInt64(), forKey: "p") + } else { + encoder.encodeNil(forKey: "p") + } + + if let authorName = self.authorName { + encoder.encodeString(authorName, forKey: "a") + } else { + encoder.encodeNil(forKey: "a") + } + + if let quote = self.quote { + encoder.encodeCodable(quote, forKey: "qu") + } else { + encoder.encodeNil(forKey: "qu") + } + + encoder.encodeBool(self.isQuote, forKey: "iq") + } +} + +extension QuotedReplyMessageAttribute { + convenience init(apiHeader: Api.MessageFwdHeader, quote: EngineMessageReplyQuote?, isQuote: Bool) { + switch apiHeader { + case let .messageFwdHeader(_, fromId, fromName, _, _, _, _, _, _): + self.init(peerId: fromId?.peerId, authorName: fromName, quote: quote, isQuote: isQuote) + } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift index 6c9bd4eda05..13dcc3e32af 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift @@ -2,14 +2,14 @@ import Foundation import Postbox public struct SynchronizeableChatInputState: Codable, Equatable { - public let replyToMessageId: MessageId? + public let replySubject: EngineMessageReplySubject? public let text: String public let entities: [MessageTextEntity] public let timestamp: Int32 public let textSelection: Range? - public init(replyToMessageId: MessageId?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?) { - self.replyToMessageId = replyToMessageId + public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?) { + self.replySubject = replySubject self.text = text self.entities = entities self.timestamp = timestamp @@ -22,10 +22,14 @@ public struct SynchronizeableChatInputState: Codable, Equatable { self.entities = (try? container.decode([MessageTextEntity].self, forKey: "e")) ?? [] self.timestamp = (try? container.decode(Int32.self, forKey: "s")) ?? 0 - if let messageIdPeerId = try? container.decodeIfPresent(Int64.self, forKey: "m.p"), let messageIdNamespace = try? container.decodeIfPresent(Int32.self, forKey: "m.n"), let messageIdId = try? container.decodeIfPresent(Int32.self, forKey: "m.i") { - self.replyToMessageId = MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId) + if let replySubject = try? container.decodeIfPresent(EngineMessageReplySubject.self, forKey: "rep") { + self.replySubject = replySubject } else { - self.replyToMessageId = nil + if let messageIdPeerId = try? container.decodeIfPresent(Int64.self, forKey: "m.p"), let messageIdNamespace = try? container.decodeIfPresent(Int32.self, forKey: "m.n"), let messageIdId = try? container.decodeIfPresent(Int32.self, forKey: "m.i") { + self.replySubject = EngineMessageReplySubject(messageId: MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId), quote: nil) + } else { + self.replySubject = nil + } } self.textSelection = nil } @@ -36,19 +40,11 @@ public struct SynchronizeableChatInputState: Codable, Equatable { try container.encode(self.text, forKey: "t") try container.encode(self.entities, forKey: "e") try container.encode(self.timestamp, forKey: "s") - if let replyToMessageId = self.replyToMessageId { - try container.encode(replyToMessageId.peerId.toInt64(), forKey: "m.p") - try container.encode(replyToMessageId.namespace, forKey: "m.n") - try container.encode(replyToMessageId.id, forKey: "m.i") - } else { - try container.encodeNil(forKey: "m.p") - try container.encodeNil(forKey: "m.n") - try container.encodeNil(forKey: "m.i") - } + try container.encodeIfPresent(self.replySubject, forKey: "rep") } public static func ==(lhs: SynchronizeableChatInputState, rhs: SynchronizeableChatInputState) -> Bool { - if lhs.replyToMessageId != rhs.replyToMessageId { + if lhs.replySubject != rhs.replySubject { return false } if lhs.text != rhs.text { @@ -103,7 +99,7 @@ func _internal_updateChatInputState(transaction: Transaction, peerId: PeerId, th let storedState = StoredPeerChatInterfaceState( overrideChatTimestamp: inputState?.timestamp, historyScrollMessageIndex: previousState?.historyScrollMessageIndex, - associatedMessageIds: (inputState?.replyToMessageId).flatMap({ [$0] }) ?? [], + associatedMessageIds: (inputState?.replySubject?.messageId).flatMap({ [$0] }) ?? [], data: updatedStateData ) if let threadId = threadId { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 699fe3dd6d5..ba725bccd09 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -167,6 +167,8 @@ public final class TelegramChannel: Peer, Equatable { public let defaultBannedRights: TelegramChatBannedRights? public let usernames: [TelegramPeerUsername] public let storiesHidden: Bool? + public let nameColor: PeerNameColor? + public let backgroundEmojiId: Int64? public var indexName: PeerIndexNameRepresentation { var addressNames = self.usernames.map { $0.username } @@ -176,14 +178,39 @@ public final class TelegramChannel: Peer, Equatable { return .title(title: self.title, addressNames: addressNames) } - public var associatedMediaIds: [MediaId]? { return nil } + public var associatedMediaIds: [MediaId]? { + if let backgroundEmojiId = self.backgroundEmojiId { + return [MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId)] + } else { + return nil + } + } public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil public var timeoutAttribute: UInt32? { return nil } - public init(id: PeerId, accessHash: TelegramPeerAccessHash?, title: String, username: String?, photo: [TelegramMediaImageRepresentation], creationDate: Int32, version: Int32, participationStatus: TelegramChannelParticipationStatus, info: TelegramChannelInfo, flags: TelegramChannelFlags, restrictionInfo: PeerAccessRestrictionInfo?, adminRights: TelegramChatAdminRights?, bannedRights: TelegramChatBannedRights?, defaultBannedRights: TelegramChatBannedRights?, usernames: [TelegramPeerUsername], storiesHidden: Bool?) { + public init( + id: PeerId, + accessHash: TelegramPeerAccessHash?, + title: String, + username: String?, + photo: [TelegramMediaImageRepresentation], + creationDate: Int32, + version: Int32, + participationStatus: TelegramChannelParticipationStatus, + info: TelegramChannelInfo, + flags: TelegramChannelFlags, + restrictionInfo: PeerAccessRestrictionInfo?, + adminRights: TelegramChatAdminRights?, + bannedRights: TelegramChatBannedRights?, + defaultBannedRights: TelegramChatBannedRights?, + usernames: [TelegramPeerUsername], + storiesHidden: Bool?, + nameColor: PeerNameColor?, + backgroundEmojiId: Int64? + ) { self.id = id self.accessHash = accessHash self.title = title @@ -200,6 +227,8 @@ public final class TelegramChannel: Peer, Equatable { self.defaultBannedRights = defaultBannedRights self.usernames = usernames self.storiesHidden = storiesHidden + self.nameColor = nameColor + self.backgroundEmojiId = backgroundEmojiId } public init(decoder: PostboxDecoder) { @@ -229,6 +258,8 @@ public final class TelegramChannel: Peer, Equatable { self.defaultBannedRights = decoder.decodeObjectForKey("dbr", decoder: { TelegramChatBannedRights(decoder: $0) }) as? TelegramChatBannedRights self.usernames = decoder.decodeObjectArrayForKey("uns") self.storiesHidden = decoder.decodeOptionalBoolForKey("sth") + self.nameColor = decoder.decodeOptionalInt32ForKey("nclr").flatMap { PeerNameColor(rawValue: $0) } + self.backgroundEmojiId = decoder.decodeOptionalInt64ForKey("bgem") } public func encode(_ encoder: PostboxEncoder) { @@ -284,6 +315,18 @@ public final class TelegramChannel: Peer, Equatable { } else { encoder.encodeNil(forKey: "sth") } + + if let nameColor = self.nameColor { + encoder.encodeInt32(nameColor.rawValue, forKey: "nclr") + } else { + encoder.encodeNil(forKey: "nclr") + } + + if let backgroundEmojiId = self.backgroundEmojiId { + encoder.encodeInt64(backgroundEmojiId, forKey: "bgem") + } else { + encoder.encodeNil(forKey: "bgem") + } } public func isEqual(_ other: Peer) -> Bool { @@ -324,27 +367,41 @@ public final class TelegramChannel: Peer, Equatable { if lhs.storiesHidden != rhs.storiesHidden { return false } - + if lhs.nameColor != rhs.nameColor { + return false + } + if lhs.backgroundEmojiId != rhs.backgroundEmojiId { + 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) + 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) } 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) + 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) } 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) + 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) } 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) + 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) } 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) + 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) + } + + 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) + } + + 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) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 9e1cf940a19..23ed143d9e3 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -109,6 +109,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case requestedPeer(buttonId: Int32, peerId: PeerId) case setChatWallpaper(wallpaper: TelegramWallpaper) case setSameChatWallpaper(wallpaper: TelegramWallpaper) + case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32) + case giveawayLaunched public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -203,6 +205,10 @@ 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)) + case 37: + self = .giveawayLaunched default: self = .unknown } @@ -382,6 +388,19 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "atp") } + case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months): + encoder.encodeInt32(36, forKey: "_rawValue") + encoder.encodeString(slug, forKey: "slug") + encoder.encodeBool(fromGiveaway, forKey: "give") + encoder.encodeBool(unclaimed, forKey: "unclaimed") + if let boostPeerId = boostPeerId { + encoder.encodeInt64(boostPeerId.toInt64(), forKey: "pi") + } else { + encoder.encodeNil(forKey: "pi") + } + encoder.encodeInt32(months, forKey: "months") + case .giveawayLaunched: + encoder.encodeInt32(37, forKey: "_rawValue") } } @@ -403,6 +422,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return peerIds case let .requestedPeer(_, peerId): return [peerId] + 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 new file mode 100644 index 00000000000..7a9ef7e3044 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift @@ -0,0 +1,91 @@ +import Postbox + +public final class TelegramMediaGiveaway: Media, Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let onlyNewSubscribers = Flags(rawValue: 1 << 0) + } + + public var id: MediaId? { + return nil + } + public var peerIds: [PeerId] { + return self.channelPeerIds + } + + public let flags: Flags + public let channelPeerIds: [PeerId] + public let countries: [String] + 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) { + self.flags = flags + self.channelPeerIds = channelPeerIds + self.countries = countries + self.quantity = quantity + self.months = months + self.untilDate = untilDate + } + + public init(decoder: PostboxDecoder) { + self.flags = Flags(rawValue: decoder.decodeInt32ForKey("flg", orElse: 0)) + self.channelPeerIds = decoder.decodeInt64ArrayForKey("cns").map { PeerId($0) } + self.countries = decoder.decodeStringArrayForKey("cnt") + self.quantity = decoder.decodeInt32ForKey("qty", orElse: 0) + self.months = decoder.decodeInt32ForKey("mts", orElse: 0) + self.untilDate = decoder.decodeInt32ForKey("unt", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.flags.rawValue, forKey: "flg") + encoder.encodeInt64Array(self.channelPeerIds.map { $0.toInt64() }, forKey: "cns") + encoder.encodeStringArray(self.countries, forKey: "cnt") + encoder.encodeInt32(self.quantity, forKey: "qty") + encoder.encodeInt32(self.months, forKey: "mts") + encoder.encodeInt32(self.untilDate, forKey: "unt") + } + + public func isLikelyToBeUpdated() -> Bool { + return false + } + + public func isEqual(to other: Media) -> Bool { + guard let other = other as? TelegramMediaGiveaway else { + return false + } + if self.flags != other.flags { + return false + } + if self.channelPeerIds != other.channelPeerIds { + return false + } + if self.countries != other.countries { + return false + } + if self.quantity != other.quantity { + return false + } + if self.months != other.months { + return false + } + if self.untilDate != other.untilDate { + return false + } + return true + } + + public func isSemanticallyEqual(to other: Media) -> Bool { + return self.isEqual(to: other) + } + + public static func ==(lhs: TelegramMediaGiveaway, rhs: TelegramMediaGiveaway) -> Bool { + return lhs.isEqual(to: rhs) + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index f56add72b1d..389628d9fa6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -82,6 +82,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let embedSize: PixelDimensions? public let duration: Int? public let author: String? + public let isMediaLargeByDefault: Bool? public let image: TelegramMediaImage? public let file: TelegramMediaFile? @@ -89,7 +90,26 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let attributes: [TelegramMediaWebpageAttribute] public let instantPage: InstantPage? - public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, story: TelegramMediaStory?, attributes: [TelegramMediaWebpageAttribute], instantPage: InstantPage?) { + public init( + url: String, + displayUrl: String, + hash: Int32, + type: String?, + websiteName: String?, + title: String?, + text: String?, + embedUrl: String?, + embedType: String?, + embedSize: PixelDimensions?, + duration: Int?, + author: String?, + isMediaLargeByDefault: Bool?, + image: TelegramMediaImage?, + file: TelegramMediaFile?, + story: TelegramMediaStory?, + attributes: [TelegramMediaWebpageAttribute], + instantPage: InstantPage? + ) { self.url = url self.displayUrl = displayUrl self.hash = hash @@ -102,6 +122,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.embedSize = embedSize self.duration = duration self.author = author + self.isMediaLargeByDefault = isMediaLargeByDefault self.image = image self.file = file self.story = story @@ -130,6 +151,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.duration = nil } self.author = decoder.decodeOptionalStringForKey("au") + self.isMediaLargeByDefault = decoder.decodeOptionalBoolForKey("lbd") if let image = decoder.decodeObjectForKey("im") as? TelegramMediaImage { self.image = image @@ -216,6 +238,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "au") } + if let isMediaLargeByDefault = self.isMediaLargeByDefault { + encoder.encodeBool(isMediaLargeByDefault, forKey: "lbd") + } else { + encoder.encodeNil(forKey: "lbd") + } if let image = self.image { encoder.encodeObject(image, forKey: "im") } else { @@ -258,6 +285,10 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage return false } + if lhs.isMediaLargeByDefault != rhs.isMediaLargeByDefault { + return false + } + if let lhsImage = lhs.image, let rhsImage = rhs.image { if !lhsImage.isEqual(to: rhsImage) { return false diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index bb7cff414e4..714303790ca 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -110,6 +110,8 @@ public final class TelegramUser: Peer, Equatable { public let emojiStatus: PeerEmojiStatus? public let usernames: [TelegramPeerUsername] public let storiesHidden: Bool? + public let nameColor: PeerNameColor? + public let backgroundEmojiId: Int64? public var nameOrPhone: String { if let firstName = self.firstName { @@ -148,8 +150,19 @@ public final class TelegramUser: Peer, Equatable { } public var associatedMediaIds: [MediaId]? { - if let emojiStatus = self.emojiStatus { - return [MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)] + 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 } @@ -170,7 +183,23 @@ public final class TelegramUser: Peer, Equatable { } } - public init(id: PeerId, accessHash: TelegramPeerAccessHash?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: [TelegramMediaImageRepresentation], botInfo: BotUserInfo?, restrictionInfo: PeerAccessRestrictionInfo?, flags: UserInfoFlags, emojiStatus: PeerEmojiStatus?, usernames: [TelegramPeerUsername], storiesHidden: Bool?) { + public init( + id: PeerId, + accessHash: TelegramPeerAccessHash?, + firstName: String?, + lastName: String?, + username: String?, + phone: String?, + photo: [TelegramMediaImageRepresentation], + botInfo: BotUserInfo?, + restrictionInfo: PeerAccessRestrictionInfo?, + flags: UserInfoFlags, + emojiStatus: PeerEmojiStatus?, + usernames: [TelegramPeerUsername], + storiesHidden: Bool?, + nameColor: PeerNameColor?, + backgroundEmojiId: Int64? + ) { self.id = id self.accessHash = accessHash self.firstName = firstName @@ -184,6 +213,8 @@ public final class TelegramUser: Peer, Equatable { self.emojiStatus = emojiStatus self.usernames = usernames self.storiesHidden = storiesHidden + self.nameColor = nameColor + self.backgroundEmojiId = backgroundEmojiId } public init(decoder: PostboxDecoder) { @@ -223,6 +254,9 @@ public final class TelegramUser: Peer, Equatable { self.usernames = decoder.decodeObjectArrayForKey("uns") self.storiesHidden = decoder.decodeOptionalBoolForKey("sth") + + self.nameColor = decoder.decodeOptionalInt32ForKey("nclr").flatMap { PeerNameColor(rawValue: $0) } + self.backgroundEmojiId = decoder.decodeOptionalInt64ForKey("bgem") } public func encode(_ encoder: PostboxEncoder) { @@ -282,6 +316,18 @@ public final class TelegramUser: Peer, Equatable { } else { encoder.encodeNil(forKey: "sth") } + + if let nameColor = self.nameColor { + encoder.encodeInt32(nameColor.rawValue, forKey: "nclr") + } else { + encoder.encodeNil(forKey: "nclr") + } + + if let backgroundEmojiId = self.backgroundEmojiId { + encoder.encodeInt64(backgroundEmojiId, forKey: "bgem") + } else { + encoder.encodeNil(forKey: "bgem") + } } public func isEqual(_ other: Peer) -> Bool { @@ -337,39 +383,53 @@ public final class TelegramUser: Peer, Equatable { if lhs.storiesHidden != rhs.storiesHidden { return false } + if lhs.nameColor != rhs.nameColor { + return false + } + if lhs.backgroundEmojiId != rhs.backgroundEmojiId { + return false + } return true } public func withUpdatedUsername(_ username: String?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) } public func withUpdatedUsernames(_ usernames: [TelegramPeerUsername]) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: usernames, storiesHidden: self.storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) } public func withUpdatedNames(firstName: String?, lastName: String?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: firstName, lastName: lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: firstName, lastName: lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) } public func withUpdatedPhone(_ phone: String?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) } public func withUpdatedPhoto(_ representations: [TelegramMediaImageRepresentation]) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: representations, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: representations, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) } public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) } public func withUpdatedFlags(_ flags: UserInfoFlags) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) } public func withUpdatedStoriesHidden(_ storiesHidden: Bool?) -> TelegramUser { - return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: storiesHidden) + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId) + } + + public func withUpdatedNameColor(_ nameColor: PeerNameColor) -> TelegramUser { + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId) + } + + public func withUpdatedBackgroundEmojiId(_ backgroundEmojiId: Int64?) -> TelegramUser { + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: self.emojiStatus, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift index ec31b3f55dc..424fbfc7b8c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift @@ -1,3 +1,4 @@ +import Foundation import Postbox public enum MessageTextEntityType: Equatable { @@ -315,3 +316,61 @@ public class TextEntitiesMessageAttribute: MessageAttribute, Equatable { return lhs.entities == rhs.entities } } + +public func messageTextEntitiesInRange(entities: [MessageTextEntity], range: NSRange, onlyQuoteable: Bool) -> [MessageTextEntity] { + let range: Range = range.lowerBound ..< range.upperBound + var result: [MessageTextEntity] = [] + loop: for entity in entities { + if onlyQuoteable { + switch entity.type { + case .Bold, .Italic, .Strikethrough, .Underline, .Spoiler, .CustomEmoji: + break + default: + continue loop + } + } + if entity.range.overlaps(range) { + var mappedRange = entity.range.clamped(to: range) + mappedRange = (mappedRange.lowerBound - range.lowerBound) ..< (mappedRange.upperBound - range.lowerBound) + result.append(MessageTextEntity(range: mappedRange, type: entity.type)) + } + } + return result +} + +public func quoteMaxLength(appConfig: AppConfiguration) -> Int { + if let data = appConfig.data, let quoteLengthMax = data["quote_length_max"] as? Double { + return Int(quoteLengthMax) + } + return 1024 +} + +public func trimStringWithEntities(string: String, entities: [MessageTextEntity], maxLength: Int) -> (string: String, entities: [MessageTextEntity]) { + let nsString = string as NSString + var range = 0 ..< nsString.length + + while range.lowerBound < nsString.length { + let c = nsString.character(at: range.lowerBound) + if c == 0x0a || c == 0x20 { + range = (range.lowerBound + 1) ..< range.upperBound + } else { + break + } + } + + while range.upperBound > range.lowerBound { + let c = nsString.character(at: range.upperBound - 1) + if c == 0x0a || c == 0x20 { + range = range.lowerBound ..< (range.upperBound - 1) + } else { + break + } + } + + while range.upperBound - range.lowerBound > maxLength { + range = range.lowerBound ..< (range.upperBound - 1) + } + + let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound) + return (nsString.substring(with: nsRange), messageTextEntitiesInRange(entities: entities, range: nsRange, onlyQuoteable: false)) +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index a90da63f662..b966accbeba 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -34,6 +34,10 @@ public extension TelegramEngine { public func updateAbout(about: String?) -> Signal { return _internal_updateAbout(account: self.account, about: about) } + + public func updateNameColorAndEmoji(nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal { + return _internal_updateNameColorAndEmoji(account: self.account, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId) + } public func unregisterNotificationToken(token: Data, type: NotificationTokenType, otherAccountUserIds: [PeerId.Id]) -> Signal { return _internal_unregisterNotificationToken(account: self.account, token: token, type: type, otherAccountUserIds: otherAccountUserIds) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift index 37c828f05f6..13d45fb7822 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift @@ -30,19 +30,47 @@ public enum UpdateAboutError { func _internal_updateAbout(account: Account, about: String?) -> Signal { return account.network.request(Api.functions.account.updateProfile(flags: about == nil ? 0 : (1 << 2), firstName: nil, lastName: nil, about: about)) - |> mapError { _ -> UpdateAboutError in + |> mapError { _ -> UpdateAboutError in + return .generic + } + |> mapToSignal { apiUser -> Signal in + return account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in + if let current = current as? CachedUserData { + return current.withUpdatedAbout(about) + } else { + return current + } + }) + } + |> castError(UpdateAboutError.self) + } +} + +public enum UpdateNameColorAndEmojiError { + case generic +} + +func _internal_updateNameColorAndEmoji(account: Account, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal { + let flags: Int32 = (1 << 0) + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(account.peerId) as? TelegramUser else { + return .complete() + } + updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId)], update: { _, updated in + return updated + }) + return .single(peer) + } + |> switchToLatest + |> castError(UpdateNameColorAndEmojiError.self) + |> mapToSignal { _ -> Signal in + return account.network.request(Api.functions.account.updateColor(flags: flags, color: nameColor.rawValue, backgroundEmojiId: backgroundEmojiId ?? 0)) + |> mapError { _ -> UpdateNameColorAndEmojiError in return .generic } - |> mapToSignal { apiUser -> Signal in - return account.postbox.transaction { transaction -> Void in - transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in - if let current = current as? CachedUserData { - return current.withUpdatedAbout(about) - } else { - return current - } - }) - } - |> castError(UpdateAboutError.self) + |> mapToSignal { _ -> Signal in + return .complete() + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 257a20326ff..3d43d551d37 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/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index dad1f563c49..ca16e2f12b1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -56,6 +56,10 @@ public enum EngineConfiguration { public let maxStoriesWeeklyCount: Int32 public let maxStoriesMonthlyCount: Int32 public let maxStoriesSuggestedReactions: Int32 + public let maxGiveawayChannelsCount: Int32 + public let maxGiveawayCountriesCount: Int32 + public let maxGiveawayPeriodSeconds: Int32 + public let minChannelNameColorLevel: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -81,7 +85,11 @@ public enum EngineConfiguration { maxExpiringStoriesCount: Int32, maxStoriesWeeklyCount: Int32, maxStoriesMonthlyCount: Int32, - maxStoriesSuggestedReactions: Int32 + maxStoriesSuggestedReactions: Int32, + maxGiveawayChannelsCount: Int32, + maxGiveawayCountriesCount: Int32, + maxGiveawayPeriodSeconds: Int32, + minChannelNameColorLevel: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -103,6 +111,10 @@ public enum EngineConfiguration { self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions + self.maxGiveawayChannelsCount = maxGiveawayChannelsCount + self.maxGiveawayCountriesCount = maxGiveawayCountriesCount + self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds + self.minChannelNameColorLevel = minChannelNameColorLevel } } } @@ -159,7 +171,11 @@ public extension EngineConfiguration.UserLimits { maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount, maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount, maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount, - maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions + maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions, + maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount, + maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount, + maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds, + minChannelNameColorLevel: userLimitsConfiguration.minChannelNameColorLevel ) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift index 53924d32234..87d5c3390f9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift @@ -155,6 +155,9 @@ public extension TelegramEngine { return results } } + + /*public func subscribe(_ ts: repeat each T) -> Signal { + }*/ public func subscribe(_ t0: T0) -> Signal { return self._subscribe(items: [t0 as! AnyPostboxViewDataItem]) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index f89b245cdf8..acef2bc6714 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -36,8 +36,37 @@ private class AdMessagesHistoryContextImpl { } struct Invite: Equatable, Codable { + enum CodingKeys: String, CodingKey { + case title + case joinHash + case nameColor + } + var title: String var joinHash: String + var nameColor: PeerNameColor? + + init(title: String, joinHash: String, nameColor: PeerNameColor? = nil) { + self.title = title + self.joinHash = joinHash + self.nameColor = nameColor + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.title = try container.decode(String.self, forKey: .title) + self.joinHash = try container.decode(String.self, forKey: .joinHash) + self.nameColor = try container.decodeIfPresent(Int32.self, forKey: .nameColor).flatMap { PeerNameColor(rawValue: $0) } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.title, forKey: .title) + try container.encode(self.joinHash, forKey: .joinHash) + try container.encodeIfPresent(self.nameColor?.rawValue, forKey: .nameColor) + } } struct WebPage: Equatable, Codable { @@ -264,7 +293,9 @@ private class AdMessagesHistoryContextImpl { bannedRights: nil, defaultBannedRights: nil, usernames: [], - storiesHidden: nil + storiesHidden: nil, + nameColor: invite.nameColor, + backgroundEmojiId: nil ) case let .webPage(webPage): author = TelegramChannel( @@ -283,7 +314,9 @@ private class AdMessagesHistoryContextImpl { bannedRights: nil, defaultBannedRights: nil, usernames: [], - storiesHidden: nil + storiesHidden: nil, + nameColor: .blue, + backgroundEmojiId: nil ) } @@ -510,7 +543,7 @@ private class AdMessagesHistoryContextImpl { } } else if let chatInvite = chatInvite, let chatInviteHash = chatInviteHash { switch chatInvite { - case let .chatInvite(flags, title, _, photo, participantsCount, participants): + case let .chatInvite(flags, title, _, photo, participantsCount, participants, nameColor): let photo = telegramMediaImageFromApiPhoto(photo).flatMap({ smallestImageRepresentation($0.representations) }) let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0, isVerified: (flags & (1 << 7)) != 0, isScam: (flags & (1 << 8)) != 0, isFake: (flags & (1 << 9)) != 0) @@ -521,20 +554,23 @@ private class AdMessagesHistoryContextImpl { target = .invite(CachedMessage.Target.Invite( title: title, - joinHash: chatInviteHash + joinHash: chatInviteHash, + nameColor: PeerNameColor(rawValue: nameColor) )) case let .chatInvitePeek(chat, _): if let peer = parseTelegramGroupOrChannel(chat: chat) { target = .invite(CachedMessage.Target.Invite( title: peer.debugDisplayTitle, - joinHash: chatInviteHash + joinHash: chatInviteHash, + nameColor: peer.nameColor )) } case let .chatInviteAlready(chat): if let peer = parseTelegramGroupOrChannel(chat: chat) { target = .invite(CachedMessage.Target.Invite( title: peer.debugDisplayTitle, - joinHash: chatInviteHash + joinHash: chatInviteHash, + nameColor: peer.nameColor )) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 9683f3d5a63..ba6a04cbfee 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -83,7 +83,7 @@ private func keepWebViewSignal(network: Network, stateManager: AccountStateManag if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } let signal: Signal = network.request(Api.functions.messages.prolongWebView(flags: flags, peer: peer, bot: bot, queryId: queryId, replyTo: replyTo, sendAs: sendAs)) |> mapError { _ -> KeepWebViewError in @@ -157,7 +157,7 @@ func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } return network.request(Api.functions.messages.requestWebView(flags: flags, peer: inputPeer, bot: inputBot, url: url, startParam: payload, themeParams: serializedThemeParams, platform: botWebViewPlatform, replyTo: replyTo, sendAs: nil)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift index 6ac6b5101d5..f5f8dab3748 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift @@ -37,13 +37,21 @@ func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, _internal_updateChatInputState(transaction: transaction, peerId: key.peerId, threadId: key.threadId, inputState: nil) if let peer = transaction.getPeer(key.peerId), let inputPeer = apiInputPeer(peer) { - var flags: Int32 = 0 var topMsgId: Int32? if let threadId = key.threadId { - flags |= (1 << 2) topMsgId = Int32(clamping: threadId) } - signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyToMsgId: nil, topMsgId: topMsgId, peer: inputPeer, message: "", entities: nil)) + var flags: Int32 = 0 + var replyTo: Api.InputReplyTo? + if let topMsgId = topMsgId { + flags |= (1 << 0) + + //inputReplyToMessage#73ec805 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector = InputReplyTo; + var innerFlags: Int32 = 0 + innerFlags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: 0, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + } + signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: "", entities: nil, media: nil)) |> `catch` { _ -> Signal in return .single(.boolFalse) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift index e9cd4967beb..0d8ecdba8e2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift @@ -4,13 +4,17 @@ import SwiftSignalKit import TelegramApi import MtProtoKit +public enum GetMessagesResult { + case progress + case result([Message]) +} public enum GetMessagesStrategy { case local case cloud(skipLocal: Bool) } -func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal <[Message], NoError> { +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 @@ -78,8 +82,8 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po } } - return combineLatest(signals) |> mapToSignal { results -> Signal<[Message], NoError> in - return postbox.transaction { transaction -> [Message] in + return .single(.progress) |> 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] = [] @@ -102,13 +106,14 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po } } - return existMessages + loadedMessages + return .result(existMessages + loadedMessages) } - } - + }) } } else { - return postboxSignal |> map {$0.0} + return postboxSignal + |> map { + return .result($0.0) + } } - } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift index 639a0eb790e..ca46e3dc8bc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift @@ -17,6 +17,7 @@ public enum EngineMedia: Equatable { case webFile(TelegramMediaWebFile) case webpage(TelegramMediaWebpage) case story(TelegramMediaStory) + case giveaway(TelegramMediaGiveaway) } public extension EngineMedia { @@ -50,6 +51,8 @@ public extension EngineMedia { return webpage.id case let .story(story): return story.id + case let .giveaway(giveaway): + return giveaway.id } } } @@ -85,6 +88,8 @@ public extension EngineMedia { self = .webpage(webpage) case let story as TelegramMediaStory: self = .story(story) + case let giveaway as TelegramMediaGiveaway: + self = .giveaway(giveaway) default: preconditionFailure() } @@ -120,6 +125,8 @@ public extension EngineMedia { return webpage case let .story(story): return story + case let .giveaway(giveaway): + return giveaway } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index 14a3e6825cf..de9bc76cf89 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import SwiftSignalKit -func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { +func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) else { return false } @@ -10,14 +10,19 @@ func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to return true } -func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { +func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { var replyToMessageId = replyToMessageId if replyToMessageId == nil, let threadId = threadId { - replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)) + replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil) + } + + var webpageUrl: String? + if case let .webpage(_, _, url, _, _) = result.message { + webpageUrl = url } var attributes: [MessageAttribute] = [] - attributes.append(OutgoingChatContextResultMessageAttribute(queryId: result.queryId, id: result.id, hideVia: hideVia)) + attributes.append(OutgoingChatContextResultMessageAttribute(queryId: result.queryId, id: result.id, hideVia: hideVia, webpageUrl: webpageUrl)) if !hideVia { attributes.append(InlineBotMessageAttribute(peerId: botId, title: nil)) } @@ -28,137 +33,154 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } switch result.message { - case let .auto(caption, entities, replyMarkup): - if let entities = entities { - attributes.append(entities) - } - if let replyMarkup = replyMarkup { - attributes.append(replyMarkup) - } - switch result { - case let .internalReference(internalReference): - if internalReference.type == "game" { - if peerId.namespace == Namespaces.Peer.SecretChat { - let filteredAttributes = attributes.filter { attribute in - if let _ = attribute as? ReplyMarkupMessageAttribute { - return false - } - return true - } - if let media: Media = internalReference.file ?? internalReference.image { - return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - } else { - return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - } - } else { - return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaGame(gameId: 0, accessHash: 0, name: "", title: internalReference.title ?? "", description: internalReference.description ?? "", image: internalReference.image, file: internalReference.file)), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + case let .auto(caption, entities, replyMarkup): + if let entities = entities { + attributes.append(entities) + } + if let replyMarkup = replyMarkup { + attributes.append(replyMarkup) + } + switch result { + case let .internalReference(internalReference): + if internalReference.type == "game" { + if peerId.namespace == Namespaces.Peer.SecretChat { + let filteredAttributes = attributes.filter { attribute in + if let _ = attribute as? ReplyMarkupMessageAttribute { + return false } - } else if let file = internalReference.file, internalReference.type == "gif" { - return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - } else if let image = internalReference.image { - return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: image), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - } else if let file = internalReference.file { - return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - } else { - return nil + return true } - case let .externalReference(externalReference): - if externalReference.type == "photo" { - if let thumbnail = externalReference.thumbnail { - var randomId: Int64 = 0 - arc4random_buf(&randomId, 8) - let thumbnailResource = thumbnail.resource - let imageDimensions = thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: tmpImage), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - } else { - return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - } - } else if externalReference.type == "document" || externalReference.type == "gif" || externalReference.type == "audio" || externalReference.type == "voice" { - var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = [] - var previewRepresentations: [TelegramMediaImageRepresentation] = [] - if let thumbnail = externalReference.thumbnail { - var randomId: Int64 = 0 - arc4random_buf(&randomId, 8) - let thumbnailResource = thumbnail.resource - - if thumbnail.mimeType.hasPrefix("video/") { - videoThumbnails.append(TelegramMediaFile.VideoThumbnail(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource)) - } else { - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - } - } - var fileName = "file" - if let content = externalReference.content { - var contentUrl: String? - if let resource = content.resource as? HttpReferenceMediaResource { - contentUrl = resource.url - } else if let resource = content.resource as? WebFileReferenceMediaResource { - contentUrl = resource.url - } - if let contentUrl = contentUrl, let url = URL(string: contentUrl) { - if !url.lastPathComponent.isEmpty { - fileName = url.lastPathComponent - } - } - } - - var fileAttributes: [TelegramMediaFileAttribute] = [] - fileAttributes.append(.FileName(fileName: fileName)) - - if externalReference.type == "gif" { - fileAttributes.append(.Animated) - } - - if let dimensions = externalReference.content?.dimensions { - fileAttributes.append(.ImageSize(size: dimensions)) - if externalReference.type == "gif" { - fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil)) - } - } - - if externalReference.type == "audio" || externalReference.type == "voice" { - fileAttributes.append(.Audio(isVoice: externalReference.type == "voice", duration: Int(Int32(externalReference.content?.duration ?? 0)), title: externalReference.title, performer: externalReference.description, waveform: nil)) - } - - var randomId: Int64 = 0 - arc4random_buf(&randomId, 8) - - let resource: TelegramMediaResource - if peerId.namespace == Namespaces.Peer.SecretChat, let webResource = externalReference.content?.resource as? WebFileReferenceMediaResource { - resource = webResource - } else { - resource = EmptyMediaResource() - } - - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes) - return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + if let media: Media = internalReference.file ?? internalReference.image { + return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } else { - return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } + } else { + return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaGame(gameId: 0, accessHash: 0, name: "", title: internalReference.title ?? "", description: internalReference.description ?? "", image: internalReference.image, file: internalReference.file)), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } + } else if let file = internalReference.file, internalReference.type == "gif" { + return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } else if let image = internalReference.image { + return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: image), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } else if let file = internalReference.file { + return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } else { + return nil } - case let .text(text, entities, _, replyMarkup): - if let entities = entities { - attributes.append(entities) - } - if let replyMarkup = replyMarkup { - attributes.append(replyMarkup) - } - return .message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - case let .mapLocation(media, replyMarkup): - if let replyMarkup = replyMarkup { - attributes.append(replyMarkup) - } - return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - case let .contact(media, replyMarkup): - if let replyMarkup = replyMarkup { - attributes.append(replyMarkup) - } - return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) - case let .invoice(media, replyMarkup): - if let replyMarkup = replyMarkup { - attributes.append(replyMarkup) + case let .externalReference(externalReference): + if externalReference.type == "photo" { + if let thumbnail = externalReference.thumbnail { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let thumbnailResource = thumbnail.resource + let imageDimensions = thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: tmpImage), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } else { + return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } + } else if externalReference.type == "document" || externalReference.type == "gif" || externalReference.type == "audio" || externalReference.type == "voice" { + var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = [] + var previewRepresentations: [TelegramMediaImageRepresentation] = [] + if let thumbnail = externalReference.thumbnail { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let thumbnailResource = thumbnail.resource + + if thumbnail.mimeType.hasPrefix("video/") { + videoThumbnails.append(TelegramMediaFile.VideoThumbnail(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource)) + } else { + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + } + } + var fileName = "file" + if let content = externalReference.content { + var contentUrl: String? + if let resource = content.resource as? HttpReferenceMediaResource { + contentUrl = resource.url + } else if let resource = content.resource as? WebFileReferenceMediaResource { + contentUrl = resource.url + } + if let contentUrl = contentUrl, let url = URL(string: contentUrl) { + if !url.lastPathComponent.isEmpty { + fileName = url.lastPathComponent + } + } + } + + var fileAttributes: [TelegramMediaFileAttribute] = [] + fileAttributes.append(.FileName(fileName: fileName)) + + if externalReference.type == "gif" { + fileAttributes.append(.Animated) + } + + if let dimensions = externalReference.content?.dimensions { + fileAttributes.append(.ImageSize(size: dimensions)) + if externalReference.type == "gif" { + fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil)) + } + } + + if externalReference.type == "audio" || externalReference.type == "voice" { + fileAttributes.append(.Audio(isVoice: externalReference.type == "voice", duration: Int(Int32(externalReference.content?.duration ?? 0)), title: externalReference.title, performer: externalReference.description, waveform: nil)) + } + + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + + let resource: TelegramMediaResource + if peerId.namespace == Namespaces.Peer.SecretChat, let webResource = externalReference.content?.resource as? WebFileReferenceMediaResource { + resource = webResource + } else { + resource = EmptyMediaResource() + } + + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes) + return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } else { + return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } - return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + } + case let .text(text, entities, disableUrlPreview, previewParameters, replyMarkup): + if let entities = entities { + attributes.append(entities) + } + if let replyMarkup = replyMarkup { + attributes.append(replyMarkup) + } + if let previewParameters = previewParameters { + attributes.append(previewParameters) + } + if disableUrlPreview { + attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) + } + return .message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + case let .mapLocation(media, replyMarkup): + if let replyMarkup = replyMarkup { + attributes.append(replyMarkup) + } + return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + case let .contact(media, replyMarkup): + if let replyMarkup = replyMarkup { + attributes.append(replyMarkup) + } + return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + case let .invoice(media, replyMarkup): + if let replyMarkup = replyMarkup { + attributes.append(replyMarkup) + } + return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) + case let .webpage(text, entities, _, previewParameters, replyMarkup): + if let entities = entities { + attributes.append(entities) + } + if let replyMarkup = replyMarkup { + attributes.append(replyMarkup) + } + if let previewParameters = previewParameters { + attributes.append(previewParameters) + } + return .message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift index dc890b2f232..47fae09fa3e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift @@ -24,7 +24,7 @@ func _internal_requestStartBot(account: Account, botPeerId: PeerId, payload: Str } } } else { - return enqueueMessages(account: account, peerId: botPeerId, messages: [.message(text: "/start", attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) |> mapToSignal { _ -> Signal in + return enqueueMessages(account: account, peerId: botPeerId, messages: [.message(text: "/start", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) |> mapToSignal { _ -> Signal in return .complete() } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index 3fed816e2f0..52dec1effb8 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/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 4978eca03b2..839ae2ee1f2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1052,7 +1052,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId } id = idValue - let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId) + let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId) if let parsedMedia = parsedMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile) } @@ -1170,7 +1170,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng if case let .updateStory(_, story) = update { switch story { case let .storyItem(_, _, _, _, _, _, media, _, _, _, _): - let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) + let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) if let parsedMedia = parsedMedia, let originalMedia = originalMedia { applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) } @@ -1540,7 +1540,7 @@ extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction): - let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let parsedMedia = parsedMedia { var parsedPrivacy: Stories.Item.Privacy? if let privacy = privacy { @@ -1993,6 +1993,20 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data())) } +public func _internal_getMessageNotificationWasDisplayed(transaction: Transaction, id: MessageId) -> Bool { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: id.peerId.toInt64()) + key.setInt32(8, value: id.id) + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedMessageNotifications, key: key)) != nil +} + +public func _internal_setMessageNotificationWasDisplayed(transaction: Transaction, id: MessageId) { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: id.peerId.toInt64()) + key.setInt32(8, value: id.id) + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedMessageNotifications, key: key), entry: CodableEntry(data: Data())) +} + func _internal_updateStoryViewsForMyReaction(isChannel: Bool, views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?) -> Stories.Item.Views? { if !isChannel { return views diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 1510de0319e..ae03d3bc13d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -132,8 +132,8 @@ public extension TelegramEngine { return _internal_clearAuthorHistory(account: self.account, peerId: peerId, memberId: memberId) } - public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { - return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) + public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { + return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) } public func requestEditLiveLocation(messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal { @@ -173,7 +173,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 <[Message], NoError> { + 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) } @@ -239,7 +239,8 @@ public extension TelegramEngine { public func enqueueOutgoingMessage( to peerId: EnginePeer.Id, - replyTo replyToMessageId: EngineMessage.Id?, + replyTo replyToMessageId: EngineMessageReplySubject?, + threadId: Int64? = nil, storyId: StoryId? = nil, content: EngineOutgoingMessageContent, silentPosting: Bool = false, @@ -275,6 +276,7 @@ public extension TelegramEngine { attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, + threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: storyId, localGroupingKey: nil, @@ -283,8 +285,6 @@ public extension TelegramEngine { ) } - - guard let message = message else { return .complete() } @@ -296,11 +296,11 @@ public extension TelegramEngine { ) } - public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { + public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) } - public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { + public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) } @@ -536,6 +536,10 @@ public extension TelegramEngine { public func invokeBotCustomMethod(botId: PeerId, method: String, params: String) -> Signal { return _internal_invokeBotCustomMethod(postbox: self.account.postbox, network: self.account.network, botId: botId, method: method, params: params) } + + public func refreshAttachMenuBots() { + let _ = managedSynchronizeAttachMenuBots(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, force: true).startStandalone() + } public func addBotToAttachMenu(botId: PeerId, allowWrite: Bool) -> Signal { return _internal_addBotToAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, allowWrite: allowWrite) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index cfb26483abb..4731a335db8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -13,12 +13,13 @@ public enum AssignAppStoreTransactionError { public enum AppStoreTransactionPurpose { case subscription case upgrade - case gift(peerId: EnginePeer.Id, currency: String, amount: Int64) 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) } -func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { - var purposeSignal: Signal +private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { switch purpose { case .subscription, .upgrade, .restore: var flags: Int32 = 0 @@ -30,19 +31,63 @@ func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: App default: break } - purposeSignal = .single(.inputStorePaymentPremiumSubscription(flags: flags)) + return .single(.inputStorePaymentPremiumSubscription(flags: flags)) case let .gift(peerId, currency, amount): - purposeSignal = account.postbox.loadedPeerWithId(peerId) + return account.postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal in - if let inputUser = apiInputUser(peer) { - return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) - } else { + guard let inputUser = apiInputUser(peer) else { return .complete() } + return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) } + case let .giftCode(peerIds, boostPeerId, currency, amount): + return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in + var flags: Int32 = 0 + var apiBoostPeer: Api.InputPeer? + var apiInputUsers: [Api.InputUser] = [] + + for peerId in peerIds { + if let user = transaction.getPeer(peerId), let apiUser = apiInputUser(user) { + apiInputUsers.append(apiUser) + } + } + + if let boostPeerId = boostPeerId, let boostPeer = transaction.getPeer(boostPeerId), let apiPeer = apiInputPeer(boostPeer) { + apiBoostPeer = apiPeer + flags |= (1 << 0) + } + + return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount) + } + case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, currency, amount): + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { + return .complete() + } + var flags: Int32 = 0 + if onlyNewSubscribers { + flags |= (1 << 0) + } + var additionalPeers: [Api.InputPeer] = [] + if !additionalPeerIds.isEmpty { + flags |= (1 << 1) + for peerId in additionalPeerIds { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + additionalPeers.append(inputPeer) + } + } + } + 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)) + } + |> switchToLatest } - - return purposeSignal +} + +func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal { + return apiInputStorePaymentPurpose(account: account, purpose: purpose) |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal in return account.network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) @@ -65,31 +110,7 @@ public enum RestoreAppStoreReceiptError { } func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { - var purposeSignal: Signal - switch purpose { - case .subscription, .restore, .upgrade: - var flags: Int32 = 0 - switch purpose { - case .upgrade: - flags |= (1 << 1) - case .restore: - flags |= (1 << 0) - default: - break - } - purposeSignal = .single(.inputStorePaymentPremiumSubscription(flags: flags)) - case let .gift(peerId, currency, amount): - purposeSignal = account.postbox.loadedPeerWithId(peerId) - |> mapToSignal { peer -> Signal in - if let inputUser = apiInputUser(peer) { - return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) - } else { - return .complete() - } - } - } - - return purposeSignal + return apiInputStorePaymentPurpose(account: account, purpose: purpose) |> mapToSignal { purpose -> Signal in return account.network.request(Api.functions.payments.canPurchasePremium(purpose: purpose)) |> map { result -> Bool in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 12926a2549b..24bada7af35 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -7,8 +7,10 @@ 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) } + public struct BotPaymentInvoiceFields: OptionSet { public var rawValue: Int32 @@ -203,17 +205,55 @@ extension BotPaymentRequestedInfo { } } -func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source: BotPaymentInvoiceSource) -> Signal { - return postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil +private func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInvoiceSource) -> Api.InputInvoice? { + switch source { + case let .message(messageId): + guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { + return nil + } + 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): + guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { + return nil + } + var flags: Int32 = 0 + if onlyNewSubscribers { + flags |= (1 << 0) + } + var additionalPeers: [Api.InputPeer] = [] + if !additionalPeerIds.isEmpty { + flags |= (1 << 1) + for peerId in additionalPeerIds { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + additionalPeers.append(inputPeer) + } } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) } + 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) + + flags = 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: input, option: option) + } +} + +func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source: BotPaymentInvoiceSource) -> Signal { + return postbox.transaction { transaction -> Api.InputInvoice? in + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(BotPaymentFormRequestError.self) |> mapToSignal { invoice -> Signal in @@ -251,15 +291,7 @@ func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, network: Network, source: BotPaymentInvoiceSource, themeParams: [String: Any]?) -> Signal { return postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil - } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) - } + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(BotPaymentFormRequestError.self) |> mapToSignal { invoice -> Signal in @@ -354,15 +386,7 @@ extension BotPaymentShippingOption { func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal { return account.postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil - } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) - } + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(ValidateBotPaymentFormError.self) |> mapToSignal { invoice -> Signal in @@ -440,15 +464,7 @@ public enum SendBotPaymentResult { func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { return account.postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil - } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) - } + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(SendBotPaymentFormError.self) |> mapToSignal { invoice -> Signal in @@ -510,6 +526,12 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } + case let .premiumGiveaway(_, _, _, _, randomId, _, _, _, _): + if message.globallyUniqueId == randomId { + if case let .Id(id) = message.id { + receiptMessageId = id + } + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift new file mode 100644 index 00000000000..14767968275 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift @@ -0,0 +1,285 @@ +import Foundation +import MtProtoKit +import SwiftSignalKit +import TelegramApi + +public struct PremiumGiftCodeInfo: Equatable { + public let slug: String + public let fromPeerId: EnginePeer.Id + public let messageId: EngineMessage.Id? + public let toPeerId: EnginePeer.Id? + public let date: Int32 + public let months: Int32 + public let usedDate: Int32? + public let isGiveaway: Bool +} + +public struct PremiumGiftCodeOption: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case users + case months + case storeProductId + case storeQuantity + case currency + case amount + } + + public let users: Int32 + public let months: Int32 + public let storeProductId: String? + public let storeQuantity: Int32 + public let currency: String + public let amount: Int64 + public init(users: Int32, months: Int32, storeProductId: String?, storeQuantity: Int32, currency: String, amount: Int64) { + self.users = users + self.months = months + self.storeProductId = storeProductId + self.storeQuantity = storeQuantity + self.currency = currency + self.amount = amount + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.users = try container.decode(Int32.self, forKey: .users) + self.months = try container.decode(Int32.self, forKey: .months) + self.storeProductId = try container.decodeIfPresent(String.self, forKey: .storeProductId) + self.storeQuantity = try container.decodeIfPresent(Int32.self, forKey: .storeQuantity) ?? 1 + self.currency = try container.decode(String.self, forKey: .currency) + self.amount = try container.decode(Int64.self, forKey: .amount) + + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.users, forKey: .users) + try container.encode(self.months, forKey: .months) + try container.encodeIfPresent(self.storeProductId, forKey: .storeProductId) + try container.encode(self.storeQuantity, forKey: .storeQuantity) + try container.encode(self.currency, forKey: .currency) + try container.encode(self.amount, forKey: .amount) + } +} + +public enum PremiumGiveawayInfo: Equatable { + public enum OngoingStatus: Equatable { + public enum DisallowReason: Equatable { + case joinedTooEarly(Int32) + case channelAdmin(EnginePeer.Id) + case disallowedCountry(String) + } + + case notQualified + case notAllowed(DisallowReason) + case participating + case almostOver + } + + public enum ResultStatus: Equatable { + case notWon + case won(slug: String) + case refunded + } + + case ongoing(startDate: Int32, status: OngoingStatus) + case finished(status: ResultStatus, startDate: Int32, finishDate: Int32, winnersCount: Int32, activatedCount: Int32) +} + +public struct PrepaidGiveaway: Equatable { + public let id: Int64 + public let months: Int32 + public let quantity: Int32 + public let date: Int32 +} + +func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal { + return account.postbox.loadedPeerWithId(peerId) + |> mapToSignal { peer in + guard let inputPeer = apiInputPeer(peer) else { + return .complete() + } + return account.network.request(Api.functions.payments.getGiveawayInfo(peer: inputPeer, msgId: messageId.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> PremiumGiveawayInfo? in + if let result = result { + switch result { + case let .giveawayInfo(flags, startDate, joinedTooEarlyDate, adminDisallowedChatId, disallowedCountry): + if (flags & (1 << 3)) != 0 { + return .ongoing(startDate: startDate, status: .almostOver) + } else if (flags & (1 << 0)) != 0 { + return .ongoing(startDate: startDate, status: .participating) + } else if let disallowedCountry = disallowedCountry { + return .ongoing(startDate: startDate, status: .notAllowed(.disallowedCountry(disallowedCountry))) + } else if let joinedTooEarlyDate = joinedTooEarlyDate { + return .ongoing(startDate: startDate, status: .notAllowed(.joinedTooEarly(joinedTooEarlyDate))) + } else if let adminDisallowedChatId = adminDisallowedChatId { + return .ongoing(startDate: startDate, status: .notAllowed(.channelAdmin(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(adminDisallowedChatId))))) + } else { + return .ongoing(startDate: startDate, status: .notQualified) + } + case let .giveawayInfoResults(flags, startDate, giftCodeSlug, finishDate, winnersCount, activatedCount): + let status: PremiumGiveawayInfo.ResultStatus + if let giftCodeSlug = giftCodeSlug { + status = .won(slug: giftCodeSlug) + } else if (flags & (1 << 1)) != 0 { + status = .refunded + } else { + status = .notWon + } + return .finished(status: status, startDate: startDate, finishDate: finishDate, winnersCount: winnersCount, activatedCount: activatedCount) + } + } else { + return nil + } + } + } +} + +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() + } + return account.network.request(Api.functions.payments.getPremiumGiftCodeOptions(flags: flags, boostPeer: inputPeer)) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.PremiumGiftCodeOption]?, NoError> in + return .single(nil) + } + |> mapToSignal { results -> Signal<[PremiumGiftCodeOption], NoError> in + if let results = results { + return .single(results.map { PremiumGiftCodeOption(apiGiftCodeOption: $0) }) + } else { + return .single([]) + } + } + } +} + +func _internal_checkPremiumGiftCode(account: Account, slug: String) -> Signal { + return account.network.request(Api.functions.payments.checkGiftCode(slug: slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + if let result = result { + switch result { + case let .checkedGiftCode(_, _, _, _, _, _, _, chats, users): + return account.postbox.transaction { transaction in + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + return PremiumGiftCodeInfo(apiCheckedGiftCode: result, slug: slug) + } + } + } else { + return .single(nil) + } + } +} + +func _internal_applyPremiumGiftCode(account: Account, slug: String) -> Signal { + return account.network.request(Api.functions.payments.applyGiftCode(slug: slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + + return .complete() + } +} + +func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal { + return account.postbox.transaction { transaction -> Signal in + var flags: Int32 = 0 + if onlyNewSubscribers { + flags |= (1 << 0) + } + + 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) + for peerId in additionalPeerIds { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + additionalPeers.append(inputPeer) + } + } + } + + if !countries.isEmpty { + flags |= (1 << 2) + } + + 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))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + return .complete() + } + } + |> switchToLatest +} + +extension PremiumGiftCodeOption { + init(apiGiftCodeOption: Api.PremiumGiftCodeOption) { + switch apiGiftCodeOption { + case let .premiumGiftCodeOption(_, users, months, storeProduct, storeQuantity, curreny, amount): + self.init(users: users, months: months, storeProductId: storeProduct, storeQuantity: storeQuantity ?? 1, currency: curreny, amount: amount) + } + } +} + +extension PremiumGiftCodeInfo { + init(apiCheckedGiftCode: Api.payments.CheckedGiftCode, slug: String) { + 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.toPeerId = toId.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) } + self.date = date + self.months = months + self.usedDate = usedDate + self.isGiveaway = (flags & (1 << 2)) != 0 + } + } +} + +public extension PremiumGiftCodeInfo { + var isUsed: Bool { + return self.usedDate != nil + } +} + +extension PrepaidGiveaway { + init(apiPrepaidGiveaway: Api.PrepaidGiveaway) { + switch apiPrepaidGiveaway { + case let .prepaidGiveaway(id, months, quantity, date): + self.id = id + self.months = months + self.quantity = quantity + self.date = date + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index c9773af9503..39b6e913899 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -45,5 +45,25 @@ public extension TelegramEngine { public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal { return _internal_canPurchasePremium(account: self.account, purpose: purpose) } + + public func checkPremiumGiftCode(slug: String) -> Signal { + return _internal_checkPremiumGiftCode(account: self.account, slug: slug) + } + + public func applyPremiumGiftCode(slug: String) -> Signal { + return _internal_applyPremiumGiftCode(account: self.account, slug: slug) + } + + public func premiumGiftCodeOptions(peerId: EnginePeer.Id) -> Signal<[PremiumGiftCodeOption], NoError> { + return _internal_premiumGiftCodeOptions(account: self.account, peerId: peerId) + } + + public func premiumGiveawayInfo(peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal { + 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) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 1fcdf35648f..6edfce7e370 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 91dfaaa75a8..924d47c1790 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -87,6 +87,8 @@ 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?) } public enum ChannelAdminLogEventError { @@ -341,12 +343,15 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net case .none: newInfo = nil } - action = .pinTopic(prevInfo: prevInfo, newInfo: newInfo) case let .channelAdminLogEventActionToggleForum(newValue): 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) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) if let action = action { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 0664a27b9c2..c42442928ed 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 bd32c6d7357..a24d8954ff2 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/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index be5a1648a5c..be5f03b5a7e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -47,6 +47,7 @@ public enum ExternalJoiningChatState { public let photoRepresentation: TelegramMediaImageRepresentation? public let participantsCount: Int32 public let participants: [EnginePeer]? + public let nameColor: PeerNameColor? } case invite(Invite) @@ -105,10 +106,10 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal mapToSignal { result -> Signal in if let result = result { switch result { - case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants): + case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants, nameColor): let photo = telegramMediaImageFromApiPhoto(invitePhoto).flatMap({ smallestImageRepresentation($0.representations) }) let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0, isVerified: (flags & (1 << 7)) != 0, isScam: (flags & (1 << 8)) != 0, isFake: (flags & (1 << 9)) != 0) - return .single(.invite(ExternalJoiningChatState.Invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({ EnginePeer(TelegramUser(user: $0)) })))) + return .single(.invite(ExternalJoiningChatState.Invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({ EnginePeer(TelegramUser(user: $0)) }), nameColor: PeerNameColor(rawValue: nameColor)))) case let .chatInviteAlready(chat): if let peer = parseTelegramGroupOrChannel(chat: chat) { return account.postbox.transaction({ (transaction) -> ExternalJoiningChatState in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 16eafaf8af1..64deebf284a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -505,6 +505,14 @@ public extension EnginePeer { } return false } + + var nameColor: PeerNameColor? { + return self._asPeer().nameColor + } + + var backgroundEmojiId: Int64? { + return self._asPeer().backgroundEmojiId + } } public extension EnginePeer { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift index 3b1bc04dcf8..55736bb3ba2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift @@ -51,7 +51,7 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl keys.append(contentsOf: peerIds.map({ .peer(peerId: $0, components: .all) })) return postbox.combinedView(keys: keys) - |> map { view -> [RecentlySearchedPeer] in + |> mapToSignal { view -> Signal<[RecentlySearchedPeer], NoError> in var result: [RecentlySearchedPeer] = [] var unreadCounts: [PeerId: Int32] = [:] if let unreadCountsView = view.views[unreadCountsKey] as? UnreadMessageCountsView { @@ -62,6 +62,7 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl } } + var migratedPeerIds: [EnginePeer.Id: EnginePeer.Id] = [:] for peerId in peerIds { if let peerView = view.views[.peer(peerId: peerId, components: .all)] as? PeerView { var presence: TelegramUserPresence? @@ -79,6 +80,10 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl unreadCount = 0 } } + + if let group = peer as? TelegramGroup, let migrationReference = group.migrationReference { + migratedPeerIds = [group.id: migrationReference.peerId] + } } var subpeerSummary: RecentlySearchedPeerSubpeerSummary? @@ -91,7 +96,20 @@ public func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[Recentl } } - return result + if !migratedPeerIds.isEmpty { + return postbox.transaction { transaction -> Signal<[RecentlySearchedPeer], NoError> in + for (previousPeerId, updatedPeerId) in migratedPeerIds { + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(previousPeerId).rawValue) + if let entry = CodableEntry(RecentPeerItem(rating: 0.0)) { + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, item: OrderedItemListEntry(id: RecentPeerItemId(updatedPeerId).rawValue, contents: entry), removeTailIfCountExceeds: 20) + } + } + return .complete() + } + |> switchToLatest + } else { + return .single(result) + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ResolvePeerByName.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ResolvePeerByName.swift index beacb443965..42aaf1c4547 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ResolvePeerByName.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ResolvePeerByName.swift @@ -14,7 +14,17 @@ public enum ResolvePeerByNameOptionRemote { case update } -func _internal_resolvePeerByName(account: Account, name: String, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { +public enum ResolvePeerIdByNameResult { + case progress + case result(PeerId?) +} + +public enum ResolvePeerResult { + case progress + case result(EnginePeer?) +} + +func _internal_resolvePeerByName(account: Account, name: String, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { var normalizedName = name if normalizedName.hasPrefix("@") { normalizedName = String(normalizedName[name.index(after: name.startIndex)...]) @@ -24,17 +34,19 @@ func _internal_resolvePeerByName(account: Account, name: String, ageLimit: Int32 return account.postbox.transaction { transaction -> CachedResolvedByNamePeer? in return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.resolvedByNamePeers, key: CachedResolvedByNamePeer.key(name: normalizedName)))?.get(CachedResolvedByNamePeer.self) - } |> mapToSignal { cachedEntry -> Signal in + } + |> mapToSignal { cachedEntry -> Signal in let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) if let cachedEntry = cachedEntry, cachedEntry.timestamp <= timestamp && cachedEntry.timestamp >= timestamp - ageLimit { - return .single(cachedEntry.peerId) + return .single(.result(cachedEntry.peerId)) } else { - return account.network.request(Api.functions.contacts.resolveUsername(username: normalizedName)) + return .single(.progress) + |> then(account.network.request(Api.functions.contacts.resolveUsername(username: normalizedName)) |> mapError { _ -> Void in return Void() } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> PeerId? in + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> ResolvePeerIdByNameResult in var peerId: PeerId? = nil switch result { @@ -52,13 +64,13 @@ func _internal_resolvePeerByName(account: Account, name: String, ageLimit: Int32 if let entry = CodableEntry(CachedResolvedByNamePeer(peerId: peerId, timestamp: timestamp)) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.resolvedByNamePeers, key: CachedResolvedByNamePeer.key(name: normalizedName)), entry: entry) } - return peerId + return .result(peerId) } |> castError(Void.self) } - |> `catch` { _ -> Signal in - return .single(nil) - } + |> `catch` { _ -> Signal in + return .single(.result(nil)) + }) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index fb35eb5f591..bb04ad90247 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 fa19c0b2ee5..8079defdc8e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -114,14 +114,19 @@ public extension TelegramEngine { return _internal_inactiveChannelList(network: self.account.network) } - public func resolvePeerByName(name: String, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { + public func resolvePeerByName(name: String, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { return _internal_resolvePeerByName(account: self.account, name: name, ageLimit: ageLimit) - |> mapToSignal { peerId -> Signal in - guard let peerId = peerId else { - return .single(nil) - } - return self.account.postbox.transaction { transaction -> EnginePeer? in - return transaction.getPeer(peerId).flatMap(EnginePeer.init) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .single(.progress) + case let .result(peerId): + guard let peerId = peerId else { + return .single(.result(nil)) + } + return self.account.postbox.transaction { transaction -> ResolvePeerResult in + return .result(transaction.getPeer(peerId).flatMap(EnginePeer.init)) + } } } } @@ -710,6 +715,10 @@ public extension TelegramEngine { return _internal_updateBotAbout(account: self.account, peerId: peerId, about: about) } + public func updatePeerNameColorAndEmoji(peerId: EnginePeer.Id, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal { + return _internal_updatePeerNameColorAndEmoji(account: self.account, peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId) + } + 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) @@ -865,7 +874,7 @@ public extension TelegramEngine { let storedState = StoredPeerChatInterfaceState( overrideChatTimestamp: state.synchronizeableInputState?.timestamp, historyScrollMessageIndex: state.historyScrollMessageIndex, - associatedMessageIds: (state.synchronizeableInputState?.replyToMessageId).flatMap({ [$0] }) ?? [], + associatedMessageIds: (state.synchronizeableInputState?.replySubject?.messageId).flatMap({ [$0] }) ?? [], data: data ) @@ -1001,7 +1010,7 @@ public extension TelegramEngine { return _internal_createForumChannelTopic(account: self.account, peerId: id, title: title, iconColor: iconColor, iconFileId: iconFileId) } - public func fetchForumChannelTopic(id: EnginePeer.Id, threadId: Int64) -> Signal { + public func fetchForumChannelTopic(id: EnginePeer.Id, threadId: Int64) -> Signal { return _internal_fetchForumChannelTopic(account: self.account, peerId: id, threadId: threadId) } @@ -1192,13 +1201,13 @@ public extension TelegramEngine { public func getChannelBoostStatus(peerId: EnginePeer.Id) -> Signal { return _internal_getChannelBoostStatus(account: self.account, peerId: peerId) } - - public func canApplyChannelBoost(peerId: EnginePeer.Id) -> Signal { - return _internal_canApplyChannelBoost(account: self.account, peerId: peerId) - } - public func applyChannelBoost(peerId: EnginePeer.Id) -> Signal { - return _internal_applyChannelBoost(account: self.account, peerId: peerId) + public func getMyBoostStatus() -> Signal { + return _internal_getMyBoostStatus(account: self.account) + } + + public func applyChannelBoost(peerId: EnginePeer.Id, slots: [Int32]) -> Signal { + return _internal_applyChannelBoost(account: self.account, peerId: peerId, slots: slots) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift index 8b7f2b7b8ec..f91d9d3fce1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift @@ -88,3 +88,42 @@ func _internal_updatePeerDescription(account: Account, peerId: PeerId, descripti } } |> mapError { _ -> UpdatePeerDescriptionError in } |> switchToLatest } + +public enum UpdatePeerNameColorAndEmojiError { + case generic + case channelBoostRequired +} + +func _internal_updatePeerNameColorAndEmoji(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) { + 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 + } + 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 } + } + } else { + return .fail(.generic) + } + } else { + return .fail(.generic) + } + } + |> castError(UpdatePeerNameColorAndEmojiError.self) + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index cb443640323..d53d9d0e50f 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 mapToSignal { - return _internal_resolvePeerByName(account: account, name: $0) + return _internal_resolvePeerByName(account: account, name: $0) |> mapToSignal { result in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } } |> filter { $0 != nil } |> map { $0! } |> mapToSignal { peerId -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Utils/TempBox.swift b/submodules/TelegramCore/Sources/TelegramEngine/Utils/TempBox.swift index c678eaef245..96d018b1647 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Utils/TempBox.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Utils/TempBox.swift @@ -5,4 +5,5 @@ public typealias EngineTempBox = TempBox public extension EngineTempBox { typealias File = TempBoxFile + typealias Directory = TempBoxDirectory } diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index 26666d71eec..be8a1269404 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -51,7 +51,7 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul if let telegramUser = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { parsedPeers.append(telegramUser) switch user { - case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId): + case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId, _, _): let isMin = (flags & (1 << 20)) != 0 let storiesUnavailable = (flags2 & (1 << 4)) != 0 @@ -72,7 +72,7 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul } for (_, chat) in peers.chats { switch chat { - case let .channel(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId): + case let .channel(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId, _, _): let isMin = (flags & (1 << 12)) != 0 let storiesUnavailable = (flags2 & (1 << 3)) != 0 @@ -315,7 +315,7 @@ func updatePeerPresences(transaction: Transaction, accountPeerId: PeerId, peerPr parsedPresences[peerId] = presence default: switch user { - case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let isMin = (flags & (1 << 20)) != 0 if isMin, let _ = transaction.getPeerPresence(peerId: peerId) { } else { @@ -383,7 +383,7 @@ func updateContacts(transaction: Transaction, apiUsers: [Api.User]) { for user in apiUsers { var isContact: Bool? switch user { - case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 20)) == 0 { isContact = (flags & (1 << 11)) != 0 } diff --git a/submodules/TelegramCore/Sources/Utils/Log.swift b/submodules/TelegramCore/Sources/Utils/Log.swift index 3d0410a786a..4278b78c028 100644 --- a/submodules/TelegramCore/Sources/Utils/Log.swift +++ b/submodules/TelegramCore/Sources/Utils/Log.swift @@ -108,6 +108,8 @@ public final class Logger { setPostboxLogger({ s in Logger.shared.log("Postbox", s) Logger.shared.shortLog("Postbox", s) + }, sync: { + Logger.shared.sync() }) } @@ -128,6 +130,14 @@ public final class Logger { self.basePath = basePath } + public func sync() { + self.queue.sync { + if let (currentFile, _) = self.file { + let _ = currentFile.sync() + } + } + } + public func collectLogs(prefix: String? = nil) -> Signal<[(String, String)], NoError> { return Signal { subscriber in self.queue.async { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 29d44b0b9ab..1d3cb2680ca 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -437,6 +437,17 @@ public extension Message { } } +public extension Message { + var webpagePreviewAttribute: WebpagePreviewMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? WebpagePreviewMessageAttribute { + return attribute + } + } + return nil + } +} + public func _internal_parseMediaAttachment(data: Data) -> Media? { guard let object = Api.parse(Buffer(buffer: MemoryBuffer(data: data))) else { return nil diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 27cb58c644a..101069d5c34 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -233,6 +233,36 @@ public extension Peer { return false } } + + var nameColor: PeerNameColor? { + switch self { + case let user as TelegramUser: + if let nameColor = user.nameColor { + return nameColor + } else { + return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) + } + case let channel as TelegramChannel: + if let nameColor = channel.nameColor { + return nameColor + } else { + return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) + } + default: + return nil + } + } + + var backgroundEmojiId: Int64? { + switch self { + case let user as TelegramUser: + return user.backgroundEmojiId + case let channel as TelegramChannel: + return channel.backgroundEmojiId + default: + return nil + } + } } public extension TelegramPeerUsername { diff --git a/submodules/TelegramCore/Sources/WebpagePreview.swift b/submodules/TelegramCore/Sources/WebpagePreview.swift index 132b1366a2a..af5e284bf53 100644 --- a/submodules/TelegramCore/Sources/WebpagePreview.swift +++ b/submodules/TelegramCore/Sources/WebpagePreview.swift @@ -5,13 +5,18 @@ import TelegramApi import MtProtoKit -public func webpagePreview(account: Account, url: String, webpageId: MediaId? = nil) -> Signal { +public enum WebpagePreviewResult : Equatable { + case progress + case result(TelegramMediaWebpage?) +} + +public func webpagePreview(account: Account, url: String, webpageId: MediaId? = nil) -> Signal { return webpagePreviewWithProgress(account: account, url: url) - |> mapToSignal { next -> Signal in + |> mapToSignal { next -> Signal in if case let .result(result) = next { - return .single(result) + return .single(.result(result)) } else { - return .complete() + return .single(.progress) } } } @@ -47,7 +52,8 @@ public func webpagePreviewWithProgress(account: Account, url: String, webpageId: } } switch result { - case let .messageMediaWebPage(webpage): + case let .messageMediaWebPage(flags, webpage): + let _ = flags if let media = telegramMediaWebpageFromApiWebpage(webpage, url: url) { if case .Loaded = media.content { return .single(.result(media)) @@ -74,45 +80,73 @@ public func webpagePreviewWithProgress(account: Account, url: String, webpageId: |> switchToLatest } -public func actualizedWebpage(postbox: Postbox, network: Network, webpage: TelegramMediaWebpage) -> Signal { +public func actualizedWebpage(account: Account, webpage: TelegramMediaWebpage) -> Signal { if case let .Loaded(content) = webpage.content { - return network.request(Api.functions.messages.getWebPage(url: content.url, hash: content.hash)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal in - if let result = result, let updatedWebpage = telegramMediaWebpageFromApiWebpage(result, url: nil), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId == webpage.webpageId { - return postbox.transaction { transaction -> TelegramMediaWebpage in - updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage) - return updatedWebpage - } - } else if let result = result, case let .webPageNotModified(_, viewsValue) = result, let views = viewsValue, case let .Loaded(content) = webpage.content { - let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, story: content.story, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }))) - let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent) - return postbox.transaction { transaction -> TelegramMediaWebpage in - updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage) - return updatedWebpage + return account.network.request(Api.functions.messages.getWebPage(url: content.url, hash: content.hash)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + if let result = result { + return account.postbox.transaction { transaction -> Signal in + switch result { + case let .webPage(apiWebpage, chats, users): + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + + if let updatedWebpage = telegramMediaWebpageFromApiWebpage(apiWebpage, url: nil), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId == webpage.webpageId { + return .single(updatedWebpage) + } else if case let .webPageNotModified(_, viewsValue) = apiWebpage, let views = viewsValue, case let .Loaded(content) = webpage.content { + let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent( + url: content.url, + displayUrl: content.displayUrl, + hash: content.hash, + type: content.type, + websiteName: content.websiteName, + title: content.title, + text: content.text, + embedUrl: content.embedUrl, + embedType: content.embedType, + embedSize: content.embedSize, + duration: content.duration, + author: content.author, + isMediaLargeByDefault: content.isMediaLargeByDefault, + image: content.image, + file: content.file, + story: content.story, + attributes: content.attributes, + instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }) + )) + let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent) + updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage) + return .single(updatedWebpage) + } } - } else { return .complete() } + |> switchToLatest + } else { + return .complete() } + } } else { return .complete() } } -func updatedRemoteWebpage(postbox: Postbox, network: Network, webPage: WebpageReference) -> Signal { +func updatedRemoteWebpage(postbox: Postbox, network: Network, accountPeerId: EnginePeer.Id, webPage: WebpageReference) -> Signal { if case let .webPage(id, url) = webPage.content { return network.request(Api.functions.messages.getWebPage(url: url, hash: 0)) |> map(Optional.init) - |> `catch` { _ -> Signal in + |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal in - if let result = result, let updatedWebpage = telegramMediaWebpageFromApiWebpage(result, url: nil), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId.id == id { + if let result = result, case let .webPage(webpage, chats, users) = result, let updatedWebpage = telegramMediaWebpageFromApiWebpage(webpage, url: nil), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId.id == id { return postbox.transaction { transaction -> TelegramMediaWebpage? in + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) if transaction.getMedia(updatedWebpage.webpageId) != nil { updateMessageMedia(transaction: transaction, id: updatedWebpage.webpageId, media: updatedWebpage) } diff --git a/submodules/TelegramIntents/Sources/TelegramIntents.swift b/submodules/TelegramIntents/Sources/TelegramIntents.swift index fb11b19cb64..f6a782d87c9 100644 --- a/submodules/TelegramIntents/Sources/TelegramIntents.swift +++ b/submodules/TelegramIntents/Sources/TelegramIntents.swift @@ -151,7 +151,7 @@ public func donateSendMessageIntent(account: Account, sharedContext: SharedAccou if let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), font: avatarFont, letters: peer.displayLetters, peerId: peer.id) + drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), font: avatarFont, letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor) })?.withRenderingMode(.alwaysOriginal) { avatarImage = image } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 866a403ee99..812bae542c3 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -181,6 +181,10 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case storyStealthModeReplyCount = 47 case viewOnceTooltip = 48 case displayStoryUnmuteTooltip = 49 + case chatReplyOptionsTip = 50 + case displayStoryInteractionGuide = 51 + case dismissedPremiumAppIconsBadge = 52 + case replyQuoteTextSelectionTip = 53 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -355,6 +359,10 @@ private struct ApplicationSpecificNoticeKeys { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatForwardOptionsTip.key) } + static func chatReplyOptionsTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatReplyOptionsTip.key) + } + static func interactiveEmojiSyncTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.interactiveEmojiSyncTip.key) } @@ -434,6 +442,18 @@ private struct ApplicationSpecificNoticeKeys { static func displayStoryUnmuteTooltip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryUnmuteTooltip.key) } + + static func displayStoryInteractionGuide() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryInteractionGuide.key) + } + + static func dismissedPremiumAppIconsBadge() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumAppIconsBadge.key) + } + + static func replyQuoteTextSelectionTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.replyQuoteTextSelectionTip.key) + } } public struct ApplicationSpecificNotice { @@ -909,6 +929,30 @@ public struct ApplicationSpecificNotice { } } } + + public static func getReplyQuoteTextSelectionTips(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.replyQuoteTextSelectionTip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementReplyQuoteTextSelectionTips(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.replyQuoteTextSelectionTip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + currentValue += count + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.replyQuoteTextSelectionTip(), entry) + } + } + } public static func getMessageViewsPrivacyTips(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Int32 in @@ -1245,6 +1289,33 @@ public struct ApplicationSpecificNotice { } } + public static func getChatReplyOptionsTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.chatReplyOptionsTip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementChatReplyOptionsTip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.chatReplyOptionsTip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.chatReplyOptionsTip(), entry) + } + + return Int(previousValue) + } + } + public static func getClearStorageDismissedTipSize(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Int32 in if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.clearStorageDismissedTipSize())?.get(ApplicationSpecificCounterNotice.self) { @@ -1659,4 +1730,46 @@ public struct ApplicationSpecificNotice { } |> take(1) } + + public static func setDisplayStoryInteractionGuide(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { + transaction.setNotice(ApplicationSpecificNoticeKeys.displayStoryInteractionGuide(), entry) + } + } + |> ignoreValues + } + + public static func displayStoryInteractionGuide(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.displayStoryInteractionGuide()) + |> map { view -> Bool in + if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { + return true + } else { + return false + } + } + |> take(1) + } + + public static func setDismissedPremiumAppIconsBadge(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPremiumAppIconsBadge(), entry) + } + } + |> ignoreValues + } + + public static func dismissedPremiumAppIconsBadge(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedPremiumAppIconsBadge()) + |> map { view -> Bool in + if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { + return true + } else { + return false + } + } + |> take(1) + } } diff --git a/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift b/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift index 094d3bd8576..b0a432b8a96 100644 --- a/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift @@ -13,6 +13,10 @@ public final class ChatPresentationThemeData: Equatable { self.wallpaper = wallpaper } + public func withTheme(_ theme: PresentationTheme) -> ChatPresentationThemeData { + return ChatPresentationThemeData(theme: theme, wallpaper: self.wallpaper) + } + public static func ==(lhs: ChatPresentationThemeData, rhs: ChatPresentationThemeData) -> Bool { return lhs.theme === rhs.theme && lhs.wallpaper == rhs.wallpaper } @@ -60,6 +64,21 @@ public final class ChatPresentationData { self.animatedEmojiScale = animatedEmojiScale } + + public func withTheme(_ theme: ChatPresentationThemeData) -> ChatPresentationData { + return ChatPresentationData( + theme: self.theme, + fontSize: self.fontSize, + strings: self.strings, + dateTimeFormat: self.dateTimeFormat, + nameDisplayOrder: self.nameDisplayOrder, + disableAnimations: self.disableAnimations, + largeEmoji: self.largeEmoji, + chatBubbleCorners: self.chatBubbleCorners, + animatedEmojiScale: self.animatedEmojiScale, + isPreview: self.isPreview + ) + } } extension ChatPresentationData { diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index c9a2e45a2b6..df5ba1f61b0 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -61,7 +61,7 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit 0x0771ff, 0x9047ff, 0xa256bf, - ] + ].reversed() } else { bubbleColors = [accentColor.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98).rgb, accentColor.rgb] } @@ -444,7 +444,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati ), itemCheckColors: PresentationThemeFillStrokeForeground( fillColor: UIColor(rgb: 0xffffff), - strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), + strokeColor: UIColor(rgb: 0xffffff, alpha: 0.3), foregroundColor: UIColor(rgb: 0x000000) ), controlSecondaryColor: UIColor(rgb: 0xffffff, alpha: 0.5), diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 048308dd712..997b19916ed 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -382,27 +382,27 @@ public final class PrincipalThemeEssentialGraphics { self.chatMessageBackgroundIncomingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingExtractedOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundIncomingMergedTopMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingMergedTopOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingMergedTopShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundIncomingMergedTopHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundIncomingMergedTopHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundIncomingMergedTopSideMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingMergedTopSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingMergedTopSideOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingMergedTopSideShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundIncomingMergedTopSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundIncomingMergedTopSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundIncomingMergedBottomMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingMergedBottomOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingMergedBottomShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundIncomingMergedBothMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingMergedBothImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) self.chatMessageBackgroundIncomingMergedBothOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundIncomingMergedBothShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundOutgoingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) @@ -411,27 +411,27 @@ public final class PrincipalThemeEssentialGraphics { self.chatMessageBackgroundOutgoingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingExtractedOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundOutgoingHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundOutgoingHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .white, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundOutgoingMergedTopMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedTopOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingMergedTopShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .white, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundOutgoingMergedTopSideMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedTopSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedTopSideOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingMergedTopSideShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .white, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundOutgoingMergedBottomMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedBottomOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingMergedBottomShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .white, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundOutgoingMergedBothMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedBothOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingMergedBothShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .white, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.chatMessageBackgroundIncomingMergedSideMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingMergedSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill[0], strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) @@ -441,8 +441,8 @@ public final class PrincipalThemeEssentialGraphics { self.chatMessageBackgroundOutgoingMergedSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) self.chatMessageBackgroundOutgoingMergedSideOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) self.chatMessageBackgroundOutgoingMergedSideShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill[0], strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) - self.chatMessageBackgroundIncomingMergedSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) - self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true) + self.chatMessageBackgroundIncomingMergedSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) + self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .white, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: highlightKnockout, extendedEdges: true, alwaysFillColor: true).withRenderingMode(.alwaysTemplate) self.checkBubbleFullImage = generateCheckImage(partial: false, color: theme.message.outgoingCheckColor, width: 11.0)! self.checkBubblePartialImage = generateCheckImage(partial: true, color: theme.message.outgoingCheckColor, width: 11.0)! diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 9ea7bde53ba..b7df50600df 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -41,6 +41,7 @@ public enum PresentationResourceKey: Int32 { case itemListCheckIcon case itemListSecondaryCheckIcon case itemListPlusIcon + case itemListRoundPlusIcon case itemListDeleteIcon case itemListDeleteIndicatorIcon case itemListReorderIndicatorIcon @@ -69,6 +70,7 @@ public enum PresentationResourceKey: Int32 { case itemListImageIcon case itemListCloudIcon case itemListTopicArrowIcon + case itemListAddBoostsIcon case itemListVoiceCallIcon case itemListVideoCallIcon @@ -298,6 +300,12 @@ public enum PresentationResourceKey: Int32 { case storyViewListLikeIcon case navigationPostStoryIcon + + case chatReplyBackgroundTemplateIncomingImage + case chatReplyBackgroundTemplateOutgoingDashedImage + case chatReplyServiceBackgroundTemplateImage + + case chatBubbleCloseIcon } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 880d70520de..0a52e304a5a 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1292,4 +1292,64 @@ public struct PresentationResourcesChat { return generateTintedImage(image: UIImage(bundleImageName: "Stories/InputLikeOn"), color: UIColor(rgb: 0xFF3B30)) }) } + + public static func chatReplyBackgroundTemplateImage(_ theme: PresentationTheme, dashedOutgoing: Bool) -> UIImage? { + let key: PresentationResourceKey = dashedOutgoing ? .chatReplyBackgroundTemplateOutgoingDashedImage : .chatReplyBackgroundTemplateIncomingImage + return theme.image(key.rawValue, { theme in + let radius: CGFloat = 4.0 + let lineWidth: CGFloat = 3.0 + + return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: radius).cgPath) + context.clip() + + context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.white.withAlphaComponent(dashedOutgoing ? 0.2 : 1.0).cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height))) + })?.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate) + }) + } + + public static func chatReplyServiceBackgroundTemplateImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatReplyServiceBackgroundTemplateImage.rawValue, { theme in + let radius: CGFloat = 3.0 + + return generateImage(CGSize(width: radius * 2.0 + 1.0, height: radius * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: CGSize(width: radius, height: size.height)), cornerRadius: radius).cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) + }) + } + + public static func chatBubbleCloseIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleCloseIcon.rawValue, { theme in + return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + let color = theme.chat.message.incoming.secondaryTextColor + context.setAlpha(color.alpha) + context.setBlendMode(.copy) + + context.setStrokeColor(theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(1.0).cgColor) + context.setLineWidth(1.0 + UIScreenPixel) + context.setLineCap(.round) + + let bounds = CGRect(origin: .zero, size: size).insetBy(dx: 1.0 + UIScreenPixel, dy: 1.0 + UIScreenPixel) + + context.move(to: CGPoint(x: bounds.minX, y: bounds.minY)) + context.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY)) + context.strokePath() + + context.move(to: CGPoint(x: bounds.maxX, y: bounds.minY)) + context.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY)) + context.strokePath() + }) + }) + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 9d009cdc176..3949ad4c0cd 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -63,6 +63,12 @@ public struct PresentationResourcesItemList { }) } + public static func roundPlusIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListRoundPlusIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddRoundIcon"), color: theme.list.itemAccentColor) + }) + } + public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor) @@ -270,6 +276,12 @@ public struct PresentationResourcesItemList { }) } + public static func addBoostsIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListAddBoostsIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Premium/AddBoosts"), color: theme.list.itemAccentColor) + }) + } + public static func cornersImage(_ theme: PresentationTheme, top: Bool, bottom: Bool) -> UIImage? { if !top && !bottom { return nil diff --git a/submodules/TelegramStringFormatting/Sources/Geo.swift b/submodules/TelegramStringFormatting/Sources/Geo.swift index 6824af398b7..cb065e12d72 100644 --- a/submodules/TelegramStringFormatting/Sources/Geo.swift +++ b/submodules/TelegramStringFormatting/Sources/Geo.swift @@ -44,3 +44,15 @@ public func stringForDistance(strings: PresentationStrings, distance: CLLocation return distanceFormatter.string(fromDistance: distance) } + +public func flagEmoji(countryCode: String) -> String { + if countryCode.uppercased() == "FT" { + return "🏴‍☠️" + } + let base : UInt32 = 127397 + var flagString = "" + for v in countryCode.uppercased().unicodeScalars { + flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!) + } + return flagString +} diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index 4efe89b396c..f372aa491ee 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -26,6 +26,7 @@ public enum MessageContentKindKey { case dice case invoice case story + case giveaway } public enum MessageContentKind: Equatable { @@ -48,6 +49,7 @@ public enum MessageContentKind: Equatable { case dice(String) case invoice(String) case story + case giveaway public func isSemanticallyEqual(to other: MessageContentKind) -> Bool { switch self { @@ -165,6 +167,12 @@ public enum MessageContentKind: Equatable { } else { return false } + case .giveaway: + if case .giveaway = other { + return true + } else { + return false + } } } @@ -208,6 +216,8 @@ public enum MessageContentKind: Equatable { return .invoice case .story: return .story + case .giveaway: + return .giveaway } } } @@ -344,6 +354,14 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil } case .story: return .story + case .giveaway: + return .giveaway + case let .webpage(webpage): + if let message, message.text.isEmpty, case let .Loaded(content) = webpage.content { + return .text(NSAttributedString(string: content.displayUrl)) + } else { + return nil + } default: return nil } @@ -397,6 +415,8 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation return (NSAttributedString(string: text), true) case .story: return (NSAttributedString(string: strings.Message_Story), true) + case .giveaway: + return (NSAttributedString(string: strings.Message_Giveaway), true) } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 0998610d159..6e0823ad616 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -898,6 +898,11 @@ 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 .giveawayLaunched: + let resultTitleString = strings.Notification_GiveawayStarted(compactAuthorName) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index be449634d72..d551796d019 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -369,6 +369,75 @@ swift_library( "//submodules/TelegramUI/Components/Chat/AccessoryPanelNode", "//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode", "//submodules/TelegramUI/Components/LegacyMessageInputPanel", + "//submodules/StatisticsUI", + "//submodules/TelegramUI/Components/PremiumGiftAttachmentScreen", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/EditableTokenListNode", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatOverscrollControl", + "//submodules/TelegramUI/Components/AudioWaveformNode", + "//submodules/TelegramUI/Components/Chat/ChatBotInfoItem", + "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", + "//submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode", + "//submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode", + "//submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode", + "//submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent", + "//submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode", + "//submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode", + "//submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode", + "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageShareButton", + "//submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode", + "//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer", + "//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode", + "//submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode", + "//submodules/TelegramUI/Components/Chat/ChatRecentActionsController", + "//submodules/TelegramUI/Components/Chat/ChatNavigationButton", + "//submodules/TelegramUI/Components/Chat/ChatLoadingNode", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", + "//submodules/TelegramUI/Components/ContextMenuScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AudioWaveformNode/BUILD b/submodules/TelegramUI/Components/AudioWaveformNode/BUILD new file mode 100644 index 00000000000..7b6c35efc38 --- /dev/null +++ b/submodules/TelegramUI/Components/AudioWaveformNode/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AudioWaveformNode", + module_name = "AudioWaveformNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ChatPresentationInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/AudioWaveformNode.swift b/submodules/TelegramUI/Components/AudioWaveformNode/Sources/AudioWaveformNode.swift similarity index 95% rename from submodules/TelegramUI/Sources/AudioWaveformNode.swift rename to submodules/TelegramUI/Components/AudioWaveformNode/Sources/AudioWaveformNode.swift index 0569bcba8f7..f1073863bff 100644 --- a/submodules/TelegramUI/Sources/AudioWaveformNode.swift +++ b/submodules/TelegramUI/Components/AudioWaveformNode/Sources/AudioWaveformNode.swift @@ -20,8 +20,8 @@ private final class AudioWaveformNodeParameters: NSObject { } } -final class AudioWaveformNode: ASDisplayNode { - enum Gravity { +public final class AudioWaveformNode: ASDisplayNode { + public enum Gravity { case bottom case center } @@ -30,7 +30,7 @@ final class AudioWaveformNode: ASDisplayNode { private var color: UIColor? private var gravity: Gravity? - var progress: CGFloat? { + public var progress: CGFloat? { didSet { if self.progress != oldValue { self.setNeedsDisplay() @@ -38,13 +38,13 @@ final class AudioWaveformNode: ASDisplayNode { } } - override init() { + override public init() { super.init() self.isOpaque = false } - override var frame: CGRect { + override public var frame: CGRect { get { return super.frame } set(value) { @@ -57,7 +57,7 @@ final class AudioWaveformNode: ASDisplayNode { } } - func setup(color: UIColor, gravity: Gravity, waveform: AudioWaveform?) { + public func setup(color: UIColor, gravity: Gravity, waveform: AudioWaveform?) { if self.color == nil || !self.color!.isEqual(color) || self.waveform != waveform || self.gravity != gravity { self.color = color self.gravity = gravity @@ -66,7 +66,7 @@ final class AudioWaveformNode: ASDisplayNode { } } - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return AudioWaveformNodeParameters(waveform: self.waveform, color: self.color, gravity: self.gravity, progress: self.progress) } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 3f7291657a2..5cc53a71849 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -1509,12 +1509,8 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer { animationCache: context.animationCache, animationRenderer: context.animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: false, - isEmojiSelection: false, + subject: isGroup ? .groupPhoto : .profilePhoto, hasTrending: false, - isProfilePhotoEmojiSelection: !isGroup, - isGroupPhotoEmojiSelection: isGroup, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, diff --git a/submodules/TelegramUI/Components/ButtonComponent/BUILD b/submodules/TelegramUI/Components/ButtonComponent/BUILD index 967ce9b5047..8d1557f50b0 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/BUILD +++ b/submodules/TelegramUI/Components/ButtonComponent/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/ActivityIndicator", + "//submodules/Components/BundleIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index fa77559ed72..f23ba40ed20 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -4,16 +4,20 @@ import Display import ComponentFlow import AnimatedTextComponent import ActivityIndicator +import BundleIconComponent public final class ButtonBadgeComponent: Component { let fillColor: UIColor + let style: ButtonTextContentComponent.BadgeStyle let content: AnyComponent public init( fillColor: UIColor, + style: ButtonTextContentComponent.BadgeStyle, content: AnyComponent ) { self.fillColor = fillColor + self.style = style self.content = content } @@ -21,6 +25,9 @@ public final class ButtonBadgeComponent: Component { if lhs.fillColor != rhs.fillColor { return false } + if lhs.style != rhs.style { + return false + } if lhs.content != rhs.content { return false } @@ -46,7 +53,13 @@ public final class ButtonBadgeComponent: Component { } public func update(component: ButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let height: CGFloat = 20.0 + let height: CGFloat + switch component.style { + case .round: + height = 20.0 + case .roundedRectangle: + height = 18.0 + } let contentInset: CGFloat = 10.0 let themeUpdated = self.component?.fillColor != component.fillColor @@ -71,7 +84,12 @@ public final class ButtonBadgeComponent: Component { } if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height { - self.backgroundView.image = generateStretchableFilledCircleImage(diameter: backgroundFrame.height, color: component.fillColor) + switch component.style { + case .round: + self.backgroundView.image = generateStretchableFilledCircleImage(diameter: backgroundFrame.height, color: component.fillColor) + case .roundedRectangle: + self.backgroundView.image = generateFilledRoundedRectImage(size: CGSize(width: height, height: height), cornerRadius: 4.0, color: component.fillColor)?.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0)) + } } return backgroundFrame.size @@ -88,11 +106,18 @@ public final class ButtonBadgeComponent: Component { } public final class ButtonTextContentComponent: Component { + public enum BadgeStyle { + case round + case roundedRectangle + } + public let text: String public let badge: Int public let textColor: UIColor public let badgeBackground: UIColor public let badgeForeground: UIColor + public let badgeStyle: BadgeStyle + public let badgeIconName: String? public let combinedAlignment: Bool public init( @@ -101,6 +126,8 @@ public final class ButtonTextContentComponent: Component { textColor: UIColor, badgeBackground: UIColor, badgeForeground: UIColor, + badgeStyle: BadgeStyle = .round, + badgeIconName: String? = nil, combinedAlignment: Bool = false ) { self.text = text @@ -108,6 +135,8 @@ public final class ButtonTextContentComponent: Component { self.textColor = textColor self.badgeBackground = badgeBackground self.badgeForeground = badgeForeground + self.badgeStyle = badgeStyle + self.badgeIconName = badgeIconName self.combinedAlignment = combinedAlignment } @@ -127,6 +156,12 @@ public final class ButtonTextContentComponent: Component { if lhs.badgeForeground != rhs.badgeForeground { return false } + if lhs.badgeStyle != rhs.badgeStyle { + return false + } + if lhs.badgeIconName != rhs.badgeIconName { + return false + } if lhs.combinedAlignment != rhs.combinedAlignment { return false } @@ -158,7 +193,10 @@ public final class ButtonTextContentComponent: Component { self.component = component self.componentState = state - let badgeSpacing: CGFloat = 6.0 + var badgeSpacing: CGFloat = 6.0 + if component.badgeIconName != nil { + badgeSpacing += 4.0 + } let contentSize = self.content.update( transition: .immediate, @@ -182,17 +220,34 @@ public final class ButtonTextContentComponent: Component { badge = ComponentView() self.badge = badge } + + var badgeContent: [AnyComponentWithIdentity] = [] + if let badgeIconName = component.badgeIconName { + badgeContent.append(AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent( + name: badgeIconName, + tintColor: component.badgeForeground + ))) + ) + } + badgeContent.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: .monospacedNumbers), + color: component.badgeForeground, + items: [ + AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0)) + ] + ))) + ) + badgeSize = badge.update( transition: badgeTransition, component: AnyComponent(ButtonBadgeComponent( fillColor: component.badgeBackground, - content: AnyComponent(AnimatedTextComponent( - font: Font.semibold(15.0), - color: component.badgeForeground, - items: [ - AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0)) - ] - )) + style: component.badgeStyle, + content: AnyComponent(HStack(badgeContent, spacing: 2.0)) )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) diff --git a/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift index 029259e437f..ce4d43e4524 100644 --- a/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift +++ b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift @@ -8,19 +8,22 @@ public final class CameraButton: Component { let tag: AnyObject? let isEnabled: Bool let action: () -> Void + let longTapAction: (() -> Void)? public init( content: AnyComponentWithIdentity, minSize: CGSize? = nil, tag: AnyObject? = nil, isEnabled: Bool = true, - action: @escaping () -> Void + action: @escaping () -> Void, + longTapAction: (() -> Void)? = nil ) { self.content = content self.minSize = minSize self.tag = tag self.isEnabled = isEnabled self.action = action + self.longTapAction = longTapAction } public func tagged(_ tag: AnyObject) -> CameraButton { @@ -29,7 +32,8 @@ public final class CameraButton: Component { minSize: self.minSize, tag: tag, isEnabled: self.isEnabled, - action: self.action + action: self.action, + longTapAction: self.longTapAction ) } @@ -50,6 +54,7 @@ public final class CameraButton: Component { } public final class View: UIButton, ComponentTaggedView { + private let containerView = UIView() public var contentView: ComponentHostView private var component: CameraButton? @@ -71,10 +76,14 @@ public final class CameraButton: Component { } else { scale = 1.0 } - transition.setScale(view: self, scale: scale) + transition.setScale(view: self.containerView, scale: scale) } + + private var longTapGestureRecognizer: UILongPressGestureRecognizer? public override init(frame: CGRect) { + self.containerView.isUserInteractionEnabled = false + self.contentView = ComponentHostView() self.contentView.isUserInteractionEnabled = false self.contentView.layer.allowsGroupOpacity = true @@ -83,9 +92,14 @@ public final class CameraButton: Component { self.isExclusiveTouch = true - self.addSubview(self.contentView) + self.addSubview(self.containerView) + self.containerView.addSubview(self.contentView) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + let longTapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress)) + self.longTapGestureRecognizer = longTapGestureRecognizer + self.addGestureRecognizer(longTapGestureRecognizer) } required init?(coder: NSCoder) { @@ -102,6 +116,10 @@ public final class CameraButton: Component { return false } + @objc private func handleLongPress() { + self.component?.longTapAction?() + } + @objc private func pressed() { self.component?.action() } @@ -131,12 +149,12 @@ public final class CameraButton: Component { self.contentView = ComponentHostView() self.contentView.isUserInteractionEnabled = false self.contentView.layer.allowsGroupOpacity = true - self.addSubview(self.contentView) + self.containerView.addSubview(self.contentView) if transition.animation.isImmediate { previousContentView.removeFromSuperview() } else { - self.addSubview(previousContentView) + self.containerView.addSubview(previousContentView) previousContentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in previousContentView?.removeFromSuperview() }) @@ -159,8 +177,13 @@ public final class CameraButton: Component { self.updateScale(transition: transition) self.isEnabled = component.isEnabled + self.longTapGestureRecognizer?.isEnabled = component.longTapAction != nil + + self.contentView.bounds = CGRect(origin: .zero, size: contentSize) + self.contentView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) - self.contentView.frame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize) + self.containerView.bounds = CGRect(origin: .zero, size: size) + self.containerView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) return size } diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index 641269c1717..6e070546b70 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -80,6 +80,7 @@ swift_library( "//submodules/Utils/VolumeButtons", "//submodules/TelegramNotices", "//submodules/DeviceAccess", + "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", ], visibility = [ diff --git a/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal b/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal index fb4e2d3d538..aeb2812c0e7 100644 --- a/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal +++ b/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal @@ -67,7 +67,8 @@ fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]], float t = AARadius / resolution.y; - float cAlpha = 1.0 - primaryParameters.y; + float cAlpha = min(1.0, 1.0 - primaryParameters.y); + float minColor = min(1.0, 1.0 + primaryParameters.y); float bound = primaryParameters.x + 0.05; if (abs(offset) > bound) { cAlpha = mix(0.0, 1.0, min(1.0, (abs(offset) - bound) * 2.4)); @@ -75,5 +76,5 @@ fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]], float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset)); - return half4(c, max(cAlpha, 0.231), max(cAlpha, 0.188), c); + return half4(min(minColor, c), min(minColor, max(cAlpha, 0.231)), min(minColor, max(cAlpha, 0.188)), c); } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index db414d199d8..ef03a997099 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -30,42 +30,69 @@ enum CameraMode: Equatable { case video } -private struct CameraState: Equatable { +struct CameraState: Equatable { enum Recording: Equatable { case none case holding case handsFree } + enum FlashTint: Equatable { + case white + case yellow + case blue + + var color: UIColor { + switch self { + case .white: + return .white + case .yellow: + return UIColor(rgb: 0xffed8c) + case .blue: + return UIColor(rgb: 0x8cdfff) + } + } + } + let mode: CameraMode let position: Camera.Position let flashMode: Camera.FlashMode let flashModeDidChange: Bool + let flashTint: FlashTint + let flashTintSize: CGFloat let recording: Recording let duration: Double let isDualCameraEnabled: Bool func updatedMode(_ mode: CameraMode) -> CameraState { - return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedPosition(_ position: Camera.Position) -> CameraState { - return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + } + + func updatedFlashTint(_ flashTint: FlashTint) -> CameraState { + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + } + + func updatedFlashTintSize(_ size: CGFloat) -> CameraState { + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: size, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedDuration(_ duration: Double) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedIsDualCameraEnabled(_ isDualCameraEnabled: Bool) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled) } } @@ -162,6 +189,7 @@ private final class CameraScreenComponent: CombinedComponent { enum ImageKey: Hashable { case cancel case flip + case flashImage } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -171,9 +199,21 @@ private final class CameraScreenComponent: CombinedComponent { var image: UIImage switch key { case .cancel: - image = UIImage(bundleImageName: "Camera/CloseIcon")! + image = UIImage(bundleImageName: "Camera/CloseIcon")!.withRenderingMode(.alwaysTemplate) case .flip: - image = UIImage(bundleImageName: "Camera/FlipIcon")! + image = UIImage(bundleImageName: "Camera/FlipIcon")!.withRenderingMode(.alwaysTemplate) + case .flashImage: + image = generateImage(CGSize(width: 393.0, height: 852.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.2, 0.6, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 - 10.0) + context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width, options: .drawsAfterEndLocation) + })!.withRenderingMode(.alwaysTemplate) } cachedImages[key] = image return image @@ -201,6 +241,9 @@ private final class CameraScreenComponent: CombinedComponent { var swipeHint: CaptureControlsComponent.SwipeHint = .none var isTransitioning = false + var displayingFlashTint = false + var previousFlashMode: Camera.FlashMode? + private let hapticFeedback = HapticFeedback() init( @@ -361,25 +404,74 @@ private final class CameraScreenComponent: CombinedComponent { } func updateCameraMode(_ mode: CameraMode) { - guard let controller = self.getController(), let _ = controller.camera else { + guard let controller = self.getController(), let camera = controller.camera else { return } + controller.updateCameraState({ $0.updatedMode(mode) }, transition: .spring(duration: 0.3)) + + var flashOn = controller.cameraState.flashMode == .on + if case .video = mode, case .auto = controller.cameraState.flashMode { + camera.setFlashMode(.on) + flashOn = true + } + + self.updateScreenBrightness(flashOn: flashOn) } - + func toggleFlashMode() { guard let controller = self.getController(), let camera = controller.camera else { return } + var flashOn = false switch controller.cameraState.flashMode { case .off: + flashOn = true camera.setFlashMode(.on) case .on: - camera.setFlashMode(.auto) + if controller.cameraState.mode == .video { + camera.setFlashMode(.off) + } else { + camera.setFlashMode(.auto) + } default: camera.setFlashMode(.off) } self.hapticFeedback.impact(.light) + + self.updateScreenBrightness(flashOn: flashOn) + } + + func updateFlashTint(_ tint: CameraState.FlashTint?) { + guard let controller = self.getController(), let camera = controller.camera else { + return + } + if let tint { + controller.updateCameraState({ $0.updatedFlashTint(tint) }, transition: .easeInOut(duration: 0.2)) + } else { + camera.setFlashMode(.off) + self.updateScreenBrightness(flashOn: false) + } + } + + func updateFlashTintSize(_ size: CGFloat) { + guard let controller = self.getController() else { + return + } + + controller.updateCameraState({ $0.updatedFlashTintSize(size) }, transition: .immediate) + } + + func presentFlashTint() { + guard let controller = self.getController(), let camera = controller.camera else { + return + } + camera.setFlashMode(.on) + + self.displayingFlashTint = true + self.updated(transition: .immediate) + + self.updateScreenBrightness(flashOn: true) } private var lastFlipTimestamp: Double? @@ -435,7 +527,7 @@ private final class CameraScreenComponent: CombinedComponent { self.updated(transition: .easeInOut(duration: 0.2)) } - private var isTakingPhoto = false + var isTakingPhoto = false func takePhoto() { guard let controller = self.getController(), let camera = controller.camera else { return @@ -447,20 +539,103 @@ private final class CameraScreenComponent: CombinedComponent { controller.node.dismissAllTooltips() - let takePhoto = camera.takePhoto() - |> mapToSignal { value -> Signal in - switch value { - case .began: - return .single(.pendingImage) - case let .finished(image, additionalImage, _): - return .single(.image(CameraScreen.Result.Image(image: image, additionalImage: additionalImage, additionalImagePosition: .topRight))) - case .failed: - return .complete() + let takePhoto = { + let takePhoto = camera.takePhoto() + |> mapToSignal { value -> Signal in + switch value { + case .began: + return .single(.pendingImage) + case let .finished(image, additionalImage, _): + return .single(.image(CameraScreen.Result.Image(image: image, additionalImage: additionalImage, additionalImagePosition: .topRight))) + case .failed: + return .complete() + } } + self.completion.invoke(takePhoto) } - self.completion.invoke(takePhoto) - Queue.mainQueue().after(1.0) { - self.isTakingPhoto = false + + let isFrontCamera = controller.cameraState.position == .front || controller.cameraState.isDualCameraEnabled + let isFlashOn = controller.cameraState.flashMode == .on + + if isFrontCamera && isFlashOn { + let previousBrightness = UIScreen.main.brightness + UIScreen.main.brightness = 1.0 + + let flashController = CameraFrontFlashOverlayController(color: controller.cameraState.flashTint.color) + controller.presentInGlobalOverlay(flashController) + + Queue.mainQueue().after(0.1, { + takePhoto() + + Queue.mainQueue().after(0.5, { + self.isTakingPhoto = false + + self.brightnessArguments = (CACurrentMediaTime(), 0.25, UIScreen.main.brightness, previousBrightness) + self.animateBrightnessChange() + flashController.dismissAnimated() + }) + }) + } else { + takePhoto() + Queue.mainQueue().after(1.0) { + self.isTakingPhoto = false + } + } + } + + private var initialBrightness: CGFloat? + private var brightnessArguments: (Double, Double, CGFloat, CGFloat)? + private var brightnessAnimator: ConstantDisplayLinkAnimator? + + func updateScreenBrightness(flashOn: Bool?) { + guard let controller = self.getController() else { + return + } + let isFrontCamera = controller.cameraState.position == .front + let isVideo = controller.cameraState.mode == .video + let isFlashOn = flashOn ?? (controller.cameraState.flashMode == .on) + + if isFrontCamera && isVideo && isFlashOn { + if self.initialBrightness == nil { + self.initialBrightness = UIScreen.main.brightness + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, 1.0) + self.animateBrightnessChange() + } + } else { + if let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } + } + } + + private func animateBrightnessChange() { + if self.brightnessAnimator == nil { + self.brightnessAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.animateBrightnessChange() + }) + self.brightnessAnimator?.isPaused = true + } + + if let (startTime, duration, initial, target) = self.brightnessArguments { + self.brightnessAnimator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + UIScreen.main.brightness = value + + if t >= 1.0 { + self.brightnessArguments = nil + self.brightnessAnimator?.isPaused = true + self.brightnessAnimator?.invalidate() + self.brightnessAnimator = nil + } + } else { + self.brightnessAnimator?.isPaused = true + self.brightnessAnimator?.invalidate() + self.brightnessAnimator = nil } } @@ -474,17 +649,21 @@ private final class CameraScreenComponent: CombinedComponent { controller.node.dismissAllTooltips() - self.resultDisposable.set((camera.startRecording() - |> deliverOnMainQueue).start(next: { [weak self] duration in - if let self, let controller = self.getController() { - controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1)) - if duration > 59.0 { - self.stopVideoRecording() + let startRecording = { + self.resultDisposable.set((camera.startRecording() + |> deliverOnMainQueue).start(next: { [weak self] duration in + if let self, let controller = self.getController() { + controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1)) + if duration > 59.0 { + self.stopVideoRecording() + } } - } - })) + })) + } controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(0.0) }, transition: .spring(duration: 0.4)) + + startRecording() } func stopVideoRecording() { @@ -505,6 +684,12 @@ private final class CameraScreenComponent: CombinedComponent { }) controller.updateCameraState({ $0.updatedRecording(.none).updatedDuration(0.0) }, transition: .spring(duration: 0.4)) + + if case .front = controller.cameraState.position, let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } } func lockVideoRecording() { @@ -528,6 +713,7 @@ private final class CameraScreenComponent: CombinedComponent { static var body: Body { let placeholder = Child(PlaceholderComponent.self) + let frontFlash = Child(Image.self) let cancelButton = Child(CameraButton.self) let captureControls = Child(CaptureControlsComponent.self) let zoomControl = Child(ZoomComponent.self) @@ -536,6 +722,7 @@ private final class CameraScreenComponent: CombinedComponent { let dualButton = Child(CameraButton.self) let modeControl = Child(ModeComponent.self) let hintLabel = Child(HintLabelComponent.self) + let flashTintControl = Child(FlashTintControlComponent.self) let timeBackground = Child(RoundedRectangle.self) let timeLabel = Child(MultilineTextComponent.self) @@ -627,6 +814,26 @@ private final class CameraScreenComponent: CombinedComponent { // ) } + let displayFrontFlash = component.cameraState.recording != .none || component.cameraState.mode == .video || state.displayingFlashTint + var controlsTintColor: UIColor = .white + if case .front = component.cameraState.position, case .on = component.cameraState.flashMode, displayFrontFlash { + let frontFlash = frontFlash.update( + component: Image(image: state.image(.flashImage), tintColor: component.cameraState.flashTint.color), + availableSize: availableSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(frontFlash + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + .scale(1.5 - component.cameraState.flashTintSize * 0.5) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + + if !state.isTakingPhoto { + controlsTintColor = .black + } + } + let shutterState: ShutterButtonState if state.isTransitioning { shutterState = .transition @@ -660,6 +867,7 @@ private final class CameraScreenComponent: CombinedComponent { isTablet: isTablet, hasAppeared: component.hasAppeared && hasAllRequiredAccess, hasAccess: hasAllRequiredAccess, + tintColor: controlsTintColor, shutterState: shutterState, lastGalleryAsset: state.lastGalleryAsset, tag: captureControlsTag, @@ -737,6 +945,7 @@ private final class CameraScreenComponent: CombinedComponent { .position(captureControlsPosition) ) + var flashButtonPosition: CGPoint? let topControlInset: CGFloat = 20.0 if case .none = component.cameraState.recording, !state.isTransitioning { let cancelButton = cancelButton.update( @@ -746,6 +955,7 @@ private final class CameraScreenComponent: CombinedComponent { component: AnyComponent( Image( image: state.image(.cancel), + tintColor: controlsTintColor, size: CGSize(width: 40.0, height: 40.0) ) ) @@ -771,7 +981,11 @@ private final class CameraScreenComponent: CombinedComponent { let flashIconName: String switch component.cameraState.flashMode { case .off: - flashIconName = "flash_off" + if let previousFlashMode = state.previousFlashMode, previousFlashMode == .on { + flashIconName = "flash_onToOff" + } else { + flashIconName = "flash_off" + } case .on: flashIconName = "flash_on" case .auto: @@ -779,6 +993,7 @@ private final class CameraScreenComponent: CombinedComponent { @unknown default: flashIconName = "flash_off" } + state.previousFlashMode = component.cameraState.flashMode flashContentComponent = AnyComponentWithIdentity( id: "animatedIcon", @@ -790,7 +1005,7 @@ private final class CameraScreenComponent: CombinedComponent { range: nil, waitForCompletion: false ), - colors: [:], + colors: ["__allcolors__": controlsTintColor], size: CGSize(width: 40.0, height: 40.0) ) ) @@ -801,7 +1016,7 @@ private final class CameraScreenComponent: CombinedComponent { component: AnyComponent( BundleIconComponent( name: "Camera/FlashOffIcon", - tintColor: nil + tintColor: controlsTintColor ) ) ) @@ -815,13 +1030,21 @@ private final class CameraScreenComponent: CombinedComponent { if let state { state.toggleFlashMode() } + }, + longTapAction: { [weak state] in + if let state { + state.presentFlashTint() + } } ).tagged(flashButtonTag), availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) + + let position = CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0) + flashButtonPosition = position context.add(flashButton - .position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0)) + .position(position) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) @@ -832,7 +1055,10 @@ private final class CameraScreenComponent: CombinedComponent { content: AnyComponentWithIdentity( id: "dual", component: AnyComponent( - DualIconComponent(isSelected: component.cameraState.isDualCameraEnabled) + DualIconComponent( + isSelected: component.cameraState.isDualCameraEnabled, + tintColor: controlsTintColor + ) ) ), action: { [weak state] in @@ -861,7 +1087,8 @@ private final class CameraScreenComponent: CombinedComponent { component: AnyComponent( FlipButtonContentComponent( action: animateFlipAction, - maskFrame: .zero + maskFrame: .zero, + tintColor: controlsTintColor ) ) ), @@ -894,7 +1121,7 @@ private final class CameraScreenComponent: CombinedComponent { let durationString = String(format: "%02d:%02d", (duration / 60) % 60, duration % 60) let timeLabel = timeLabel.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: durationString, font: Font.with(size: 21.0, design: .camera), textColor: .white)), + text: .plain(NSAttributedString(string: durationString, font: Font.with(size: 21.0, design: .camera), textColor: controlsTintColor)), horizontalAlignment: .center, textShadowColor: UIColor(rgb: 0x000000, alpha: 0.2) ), @@ -944,7 +1171,10 @@ private final class CameraScreenComponent: CombinedComponent { } if let hintText { let hintLabel = hintLabel.update( - component: HintLabelComponent(text: hintText), + component: HintLabelComponent( + text: hintText, + tintColor: controlsTintColor + ), availableSize: availableSize, transition: .immediate ) @@ -968,6 +1198,7 @@ private final class CameraScreenComponent: CombinedComponent { component: ModeComponent( isTablet: isTablet, strings: environment.strings, + tintColor: controlsTintColor, availableModes: [.photo, .video], currentMode: component.cameraState.mode, updatedMode: { [weak state] mode in @@ -993,6 +1224,33 @@ private final class CameraScreenComponent: CombinedComponent { .disappear(.default(alpha: true)) ) } + + if let flashButtonPosition, state.displayingFlashTint { + let flashTintControl = flashTintControl.update( + component: FlashTintControlComponent( + position: flashButtonPosition.offsetBy(dx: 0.0, dy: 27.0), + tint: component.cameraState.flashTint, + size: component.cameraState.flashTintSize, + update: { [weak state] tint in + state?.updateFlashTint(tint) + }, + updateSize: { [weak state] size in + state?.updateFlashTintSize(size) + }, + dismiss: { [weak state] in + state?.displayingFlashTint = false + state?.updated(transition: .easeInOut(duration: 0.2)) + } + ), + availableSize: availableSize, + transition: context.transition + ) + context.add(flashTintControl + .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + .disappear(.default(alpha: true)) + ) + } + return availableSize } } @@ -1042,12 +1300,6 @@ private class BlurView: UIVisualEffectView { } public class CameraScreen: ViewController { - public enum Mode { - case generic - case story - case instantVideo - } - public enum PIPPosition: Int32 { case topLeft case topRight @@ -1287,6 +1539,8 @@ public class CameraScreen: ViewController { position: cameraFrontPosition ? .front : .back, flashMode: .off, flashModeDidChange: false, + flashTint: .white, + flashTintSize: 1.0, recording: .none, duration: 0.0, isDualCameraEnabled: isDualCameraEnabled @@ -1371,7 +1625,7 @@ public class CameraScreen: ViewController { self.additionalPreviewView.isPreviewing ) |> filter { $0 && $1 } - |> take(1)).start(next: { [weak self] _, _ in + |> take(1)).startStandalone(next: { [weak self] _, _ in self?.mainPreviewView.removePlaceholder(delay: 0.35) self?.additionalPreviewView.removePlaceholder(delay: 0.35) }) @@ -1379,7 +1633,7 @@ public class CameraScreen: ViewController { let _ = (self.mainPreviewView.isPreviewing |> filter { $0 } |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in self?.mainPreviewView.removePlaceholder(delay: 0.35) }) } @@ -2192,6 +2446,13 @@ public class CameraScreen: ViewController { let componentFrame = CGRect(origin: .zero, size: componentSize) transition.setFrame(view: componentView, frame: componentFrame) } + + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag), view.layer.shadowOpacity.isZero { + view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) + view.layer.shadowRadius = 3.0 + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.25 + } if let view = self.componentHost.findTaggedView(tag: flashButtonTag), view.layer.shadowOpacity.isZero { view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) @@ -2330,6 +2591,12 @@ public class CameraScreen: ViewController { if isTablet && isFirstTime { self.animateIn() } + + if self.cameraState.flashMode == .on && (self.cameraState.recording != .none || self.cameraState.mode == .video) { + self.controller?.statusBarStyle = .Black + } else { + self.controller?.statusBarStyle = .White + } } } @@ -2338,7 +2605,6 @@ public class CameraScreen: ViewController { } private let context: AccountContext - fileprivate let mode: Mode fileprivate let holder: CameraHolder? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool) -> TransitionOut? @@ -2388,14 +2654,12 @@ public class CameraScreen: ViewController { public init( context: AccountContext, - mode: Mode, holder: CameraHolder? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, completion: @escaping (Signal, ResultTransition?, @escaping () -> Void) -> Void ) { self.context = context - self.mode = mode self.holder = holder self.transitionIn = transitionIn self.transitionOut = transitionOut @@ -2630,13 +2894,12 @@ public class CameraScreen: ViewController { self.node.camera?.stopCapture(invalidate: true) self.isDismissed = true if animated { + self.ignoreStatusBar = true if let layout = self.validLayout, layout.metrics.isTablet { - self.statusBar.updateStatusBarStyle(.Ignore, animated: true) self.node.animateOut(completion: { self.dismiss(animated: false) }) } else { - self.statusBar.updateStatusBarStyle(.Ignore, animated: true) if !interactive { if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate) @@ -2697,16 +2960,41 @@ public class CameraScreen: ViewController { } } + private var statusBarStyle: StatusBarStyle = .White { + didSet { + if self.statusBarStyle != oldValue { + self.updateStatusBarAppearance() + } + } + } + private var ignoreStatusBar = false { + didSet { + if self.ignoreStatusBar != oldValue { + self.updateStatusBarAppearance() + } + } + } + + private func updateStatusBarAppearance() { + let effectiveStatusBarStyle: StatusBarStyle + if !self.ignoreStatusBar { + effectiveStatusBarStyle = self.statusBarStyle + } else { + effectiveStatusBarStyle = .Ignore + } + self.statusBar.updateStatusBarStyle(effectiveStatusBarStyle, animated: true) + } + public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) { if let layout = self.validLayout, layout.metrics.isTablet { return } if dismissing { if transitionFraction < 0.7 || velocity < -1000.0 { - self.statusBar.updateStatusBarStyle(.Ignore, animated: true) + self.ignoreStatusBar = true self.requestDismiss(animated: true, interactive: true) } else { - self.statusBar.updateStatusBarStyle(.White, animated: true) + self.ignoreStatusBar = false self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in if let self, let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) @@ -2715,7 +3003,8 @@ public class CameraScreen: ViewController { } } else { if transitionFraction > 0.33 || velocity > 1000.0 { - self.statusBar.updateStatusBarStyle(.White, animated: true) + self.ignoreStatusBar = false + self.updateStatusBarAppearance() self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in if let self, let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) @@ -2724,7 +3013,8 @@ public class CameraScreen: ViewController { } }) } else { - self.statusBar.updateStatusBarStyle(.Ignore, animated: true) + self.ignoreStatusBar = true + self.updateStatusBarAppearance() self.requestDismiss(animated: true, interactive: true) } } @@ -2752,17 +3042,23 @@ private final class DualIconComponent: Component { typealias EnvironmentType = Empty let isSelected: Bool + let tintColor: UIColor init( - isSelected: Bool + isSelected: Bool, + tintColor: UIColor ) { self.isSelected = isSelected + self.tintColor = tintColor } static func ==(lhs: DualIconComponent, rhs: DualIconComponent) -> Bool { if lhs.isSelected != rhs.isSelected { return false } + if lhs.tintColor != rhs.tintColor { + return false + } return true } @@ -2781,7 +3077,7 @@ private final class DualIconComponent: Component { if let image = UIImage(bundleImageName: "Camera/DualIcon"), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0) - 1.0), size: image.size)) } - }) + })?.withRenderingMode(.alwaysTemplate) let selectedImage = generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) @@ -2793,7 +3089,7 @@ private final class DualIconComponent: Component { context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0) - 1.0), size: image.size), mask: cgImage) context.fill(CGRect(origin: .zero, size: size)) } - }) + })?.withRenderingMode(.alwaysTemplate) self.iconView.image = image self.iconView.highlightedImage = selectedImage @@ -2818,6 +3114,8 @@ private final class DualIconComponent: Component { self.iconView.frame = CGRect(origin: .zero, size: size) self.iconView.isHighlighted = component.isSelected + self.iconView.tintColor = component.tintColor + return size } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index d98fcf0b560..cdec27a8cce 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -31,6 +31,7 @@ private extension SimpleShapeLayer { private final class ShutterButtonContentComponent: Component { let isTablet: Bool let hasAppeared: Bool + let tintColor: UIColor let shutterState: ShutterButtonState let blobState: ShutterBlobView.BlobState let highlightedAction: ActionSlot @@ -40,6 +41,7 @@ private final class ShutterButtonContentComponent: Component { init( isTablet: Bool, hasAppeared: Bool, + tintColor: UIColor, shutterState: ShutterButtonState, blobState: ShutterBlobView.BlobState, highlightedAction: ActionSlot, @@ -48,6 +50,7 @@ private final class ShutterButtonContentComponent: Component { ) { self.isTablet = isTablet self.hasAppeared = hasAppeared + self.tintColor = tintColor self.shutterState = shutterState self.blobState = blobState self.highlightedAction = highlightedAction @@ -62,6 +65,9 @@ private final class ShutterButtonContentComponent: Component { if lhs.hasAppeared != rhs.hasAppeared { return false } + if lhs.tintColor != rhs.tintColor { + return false + } if lhs.shutterState != rhs.shutterState { return false } @@ -156,7 +162,7 @@ private final class ShutterButtonContentComponent: Component { } } } - + let innerColor: UIColor let innerSize: CGSize let ringSize: CGSize @@ -164,7 +170,7 @@ private final class ShutterButtonContentComponent: Component { var recordingProgress: Float? switch component.shutterState { case .generic, .disabled: - innerColor = .white + innerColor = component.tintColor innerSize = CGSize(width: 60.0, height: 60.0) ringSize = CGSize(width: 68.0, height: 68.0) case .video: @@ -188,7 +194,7 @@ private final class ShutterButtonContentComponent: Component { } self.ringLayer.fillColor = UIColor.clear.cgColor - self.ringLayer.strokeColor = UIColor.white.cgColor + self.ringLayer.strokeColor = component.tintColor.cgColor self.ringLayer.lineWidth = ringWidth let ringPath = CGPath( ellipseIn: CGRect( @@ -204,7 +210,7 @@ private final class ShutterButtonContentComponent: Component { self.ringLayer.position = CGPoint(x: maximumShutterSize.width / 2.0, y: maximumShutterSize.height / 2.0) if let blobView = self.blobView { - blobView.updateState(component.blobState, transition: transition) + blobView.updateState(component.blobState, tintColor: innerColor, transition: transition) if component.isTablet { blobView.bounds = CGRect(origin: .zero, size: CGSize(width: maximumShutterSize.width, height: 440.0)) } else { @@ -247,14 +253,16 @@ private final class ShutterButtonContentComponent: Component { final class FlipButtonContentComponent: Component { private let action: ActionSlot private let maskFrame: CGRect + private let tintColor: UIColor - init(action: ActionSlot, maskFrame: CGRect) { + init(action: ActionSlot, maskFrame: CGRect, tintColor: UIColor) { self.action = action self.maskFrame = maskFrame + self.tintColor = tintColor } static func ==(lhs: FlipButtonContentComponent, rhs: FlipButtonContentComponent) -> Bool { - return lhs.maskFrame == rhs.maskFrame + return lhs.maskFrame == rhs.maskFrame && lhs.tintColor == rhs.tintColor } final class View: UIView { @@ -280,7 +288,7 @@ final class FlipButtonContentComponent: Component { self.maskLayer.masksToBounds = true self.maskLayer.cornerRadius = 16.0 - self.icon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.cgImage + self.icon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.withRenderingMode(.alwaysTemplate).cgImage self.darkIcon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.cgImage self.darkIcon.layerTintColor = UIColor.black.cgColor } @@ -326,6 +334,8 @@ final class FlipButtonContentComponent: Component { let size = CGSize(width: 48.0, height: 48.0) + self.icon.layerTintColor = component.tintColor.cgColor + self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) self.icon.bounds = CGRect(origin: .zero, size: size) @@ -350,13 +360,15 @@ final class FlipButtonContentComponent: Component { final class LockContentComponent: Component { private let maskFrame: CGRect + private let tintColor: UIColor - init(maskFrame: CGRect) { + init(maskFrame: CGRect, tintColor: UIColor) { self.maskFrame = maskFrame + self.tintColor = tintColor } static func ==(lhs: LockContentComponent, rhs: LockContentComponent) -> Bool { - return lhs.maskFrame == rhs.maskFrame + return lhs.maskFrame == rhs.maskFrame && lhs.tintColor == rhs.tintColor } final class View: UIView { @@ -383,7 +395,7 @@ final class LockContentComponent: Component { self.maskLayer.masksToBounds = true self.maskLayer.cornerRadius = 24.0 - self.icon.contents = UIImage(bundleImageName: "Camera/LockIcon")?.cgImage + self.icon.contents = UIImage(bundleImageName: "Camera/LockIcon")?.withRenderingMode(.alwaysTemplate).cgImage self.darkIcon.contents = UIImage(bundleImageName: "Camera/LockedIcon")?.cgImage self.darkIcon.layerTintColor = UIColor.black.cgColor } @@ -397,6 +409,8 @@ final class LockContentComponent: Component { let size = CGSize(width: 30.0, height: 30.0) + self.icon.layerTintColor = component.tintColor.cgColor + self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) self.icon.bounds = CGRect(origin: .zero, size: size) @@ -447,6 +461,7 @@ final class CaptureControlsComponent: Component { let isTablet: Bool let hasAppeared: Bool let hasAccess: Bool + let tintColor: UIColor let shutterState: ShutterButtonState let lastGalleryAsset: PHAsset? let tag: AnyObject? @@ -465,6 +480,7 @@ final class CaptureControlsComponent: Component { isTablet: Bool, hasAppeared: Bool, hasAccess: Bool, + tintColor: UIColor, shutterState: ShutterButtonState, lastGalleryAsset: PHAsset?, tag: AnyObject?, @@ -482,6 +498,7 @@ final class CaptureControlsComponent: Component { self.isTablet = isTablet self.hasAppeared = hasAppeared self.hasAccess = hasAccess + self.tintColor = tintColor self.shutterState = shutterState self.lastGalleryAsset = lastGalleryAsset self.tag = tag @@ -507,6 +524,9 @@ final class CaptureControlsComponent: Component { if lhs.hasAccess != rhs.hasAccess { return false } + if lhs.tintColor != rhs.tintColor { + return false + } if lhs.shutterState != rhs.shutterState { return false } @@ -578,8 +598,7 @@ final class CaptureControlsComponent: Component { private let shutterHightlightedAction = ActionSlot() - private let lockImage = UIImage(bundleImageName: "Camera/LockIcon") - private let zoomImage = UIImage(bundleImageName: "Camera/ZoomIcon") + private let zoomImage = UIImage(bundleImageName: "Camera/ZoomIcon")?.withRenderingMode(.alwaysTemplate) private var didFlip = false @@ -946,8 +965,10 @@ final class CaptureControlsComponent: Component { transition.setBounds(view: galleryButtonView, bounds: CGRect(origin: .zero, size: galleryButtonFrame.size)) transition.setPosition(view: galleryButtonView, position: galleryButtonFrame.center) + let normalAlpha = component.tintColor.rgb == 0xffffff ? 1.0 : 0.6 + transition.setScale(view: galleryButtonView, scale: isRecording || isTransitioning ? 0.1 : 1.0) - transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0) + transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : normalAlpha) } if !component.isTablet && component.hasAccess { @@ -963,7 +984,8 @@ final class CaptureControlsComponent: Component { component: AnyComponent( FlipButtonContentComponent( action: component.flipAnimationAction, - maskFrame: flipButtonMaskFrame + maskFrame: flipButtonMaskFrame, + tintColor: component.tintColor ) ) ), @@ -1011,6 +1033,7 @@ final class CaptureControlsComponent: Component { ShutterButtonContentComponent( isTablet: component.isTablet, hasAppeared: component.hasAppeared, + tintColor: component.tintColor, shutterState: component.shutterState, blobState: blobState, highlightedAction: self.shutterHightlightedAction, @@ -1072,6 +1095,7 @@ final class CaptureControlsComponent: Component { component: AnyComponent( Image( image: self.zoomImage, + tintColor: component.tintColor, size: hintIconSize ) ), @@ -1117,7 +1141,8 @@ final class CaptureControlsComponent: Component { id: "lock", component: AnyComponent( LockContentComponent( - maskFrame: lockMaskFrame + maskFrame: lockMaskFrame, + tintColor: component.tintColor ) ) ), diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift new file mode 100644 index 00000000000..55287c4c530 --- /dev/null +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import RoundedRectWithTailPath + +private final class FlashColorComponent: Component { + let tint: CameraState.FlashTint? + let isSelected: Bool + let action: () -> Void + + init( + tint: CameraState.FlashTint?, + isSelected: Bool, + action: @escaping () -> Void + ) { + self.tint = tint + self.isSelected = isSelected + self.action = action + } + + static func == (lhs: FlashColorComponent, rhs: FlashColorComponent) -> Bool { + return lhs.tint == rhs.tint && lhs.isSelected == rhs.isSelected + } + + final class View: UIButton { + private var component: FlashColorComponent? + + private var contentView: UIView + + private let circleLayer: SimpleShapeLayer + private var ringLayer: CALayer? + private var iconLayer: CALayer? + + private var currentIsHighlighted: Bool = false { + didSet { + if self.currentIsHighlighted != oldValue { + self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 + } + } + } + + override init(frame: CGRect) { + self.contentView = UIView(frame: CGRect(origin: .zero, size: frame.size)) + self.contentView.isUserInteractionEnabled = false + self.circleLayer = SimpleShapeLayer() + + super.init(frame: frame) + + self.addSubview(self.contentView) + self.contentView.layer.addSublayer(self.circleLayer) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.action() + } + + override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + self.currentIsHighlighted = true + + return super.beginTracking(touch, with: event) + } + + override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { + self.currentIsHighlighted = false + + super.endTracking(touch, with: event) + } + + override public func cancelTracking(with event: UIEvent?) { + self.currentIsHighlighted = false + + super.cancelTracking(with: event) + } + + func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + let contentSize = CGSize(width: 30.0, height: 30.0) + self.contentView.frame = CGRect(origin: .zero, size: contentSize) + + let bounds = CGRect(origin: .zero, size: contentSize) + self.layer.allowsGroupOpacity = true + self.contentView.layer.allowsGroupOpacity = true + + self.circleLayer.frame = bounds + if self.ringLayer == nil { + let ringLayer = SimpleLayer() + ringLayer.backgroundColor = UIColor.clear.cgColor + ringLayer.cornerRadius = contentSize.width / 2.0 + ringLayer.borderWidth = 1.0 + UIScreenPixel + ringLayer.frame = CGRect(origin: .zero, size: contentSize) + self.contentView.layer.insertSublayer(ringLayer, at: 0) + self.ringLayer = ringLayer + } + + if component.isSelected { + transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 3.0 - UIScreenPixel, dy: 3.0 - UIScreenPixel), transform: nil)) + } else { + transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds, transform: nil)) + } + + if let color = component.tint?.color { + self.circleLayer.fillColor = color.cgColor + self.ringLayer?.borderColor = color.cgColor + } else { + if self.iconLayer == nil { + let iconLayer = SimpleLayer() + iconLayer.contents = UIImage(bundleImageName: "Camera/FlashOffIcon")?.cgImage + iconLayer.contentsGravity = .resizeAspect + iconLayer.frame = bounds.insetBy(dx: -4.0, dy: -4.0) + self.contentView.layer.addSublayer(iconLayer) + self.iconLayer = iconLayer + } + + self.circleLayer.fillColor = UIColor(rgb: 0xffffff, alpha: 0.1).cgColor + self.ringLayer?.borderColor = UIColor.clear.cgColor + } + + return contentSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class FlashTintControlComponent: Component { + let position: CGPoint + let tint: CameraState.FlashTint + let size: CGFloat + let update: (CameraState.FlashTint?) -> Void + let updateSize: (CGFloat) -> Void + let dismiss: () -> Void + + init( + position: CGPoint, + tint: CameraState.FlashTint, + size: CGFloat, + update: @escaping (CameraState.FlashTint?) -> Void, + updateSize: @escaping (CGFloat) -> Void, + dismiss: @escaping () -> Void + ) { + self.position = position + self.tint = tint + self.size = size + self.update = update + self.updateSize = updateSize + self.dismiss = dismiss + } + + static func == (lhs: FlashTintControlComponent, rhs: FlashTintControlComponent) -> Bool { + return lhs.position == rhs.position && lhs.tint == rhs.tint && lhs.size == rhs.size + } + + final class View: UIButton { + private var component: FlashTintControlComponent? + + private let dismissView = UIView() + private let containerView = UIView() + private let effectView: UIVisualEffectView + private let maskLayer = CAShapeLayer() + private let swatches = ComponentView() + private let sliderView: SliderView + + override init(frame: CGRect) { + self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + + var sizeUpdateImpl: ((CGFloat) -> Void)? + self.sliderView = SliderView(minValue: 0.0, maxValue: 1.0, value: 1.0, valueChanged: { value , _ in + sizeUpdateImpl?(value) + }) + + super.init(frame: frame) + + self.containerView.layer.anchorPoint = CGPoint(x: 0.8, y: 0.0) + + self.addSubview(self.dismissView) + self.addSubview(self.containerView) + self.containerView.addSubview(self.effectView) + self.containerView.addSubview(self.sliderView) + + self.dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapped))) + + sizeUpdateImpl = { [weak self] size in + if let self, let component { + component.updateSize(size) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func dismissTapped() { + self.component?.dismiss() + } + + func update(component: FlashTintControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let isFirstTime = self.component == nil + self.component = component + + let size = CGSize(width: 184.0, height: 92.0) + + self.sliderView.frame = CGRect(origin: CGPoint(x: 8.0, y: size.height - 38.0), size: CGSize(width: size.width - 16.0, height: 30.0)) + + if isFirstTime { + self.sliderView.value = component.size + + self.maskLayer.path = generateRoundedRectWithTailPath(rectSize: size, cornerRadius: 10.0, tailSize: CGSize(width: 18, height: 7.0), tailRadius: 1.0, tailPosition: 0.8, transformTail: false).cgPath + self.maskLayer.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + 7.0)) + self.effectView.layer.mask = self.maskLayer + } + + let swatchesSize = self.swatches.update( + transition: transition, + component: AnyComponent( + HStack( + [ + AnyComponentWithIdentity( + id: "off", + component: AnyComponent( + FlashColorComponent( + tint: nil, + isSelected: false, + action: { + component.update(nil) + component.dismiss() + } + ) + ) + ), + AnyComponentWithIdentity( + id: "white", + component: AnyComponent( + FlashColorComponent( + tint: .white, + isSelected: component.tint == .white, + action: { + component.update(.white) + } + ) + ) + ), + AnyComponentWithIdentity( + id: "yellow", + component: AnyComponent( + FlashColorComponent( + tint: .yellow, + isSelected: component.tint == .yellow, + action: { + component.update(.yellow) + } + ) + ) + ), + AnyComponentWithIdentity( + id: "blue", + component: AnyComponent( + FlashColorComponent( + tint: .blue, + isSelected: component.tint == .blue, + action: { + component.update(.blue) + } + ) + ) + ) + ], + spacing: 16.0 + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.swatches.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - swatchesSize.width) / 2.0), y: 8.0), size: swatchesSize) + } + + self.dismissView.frame = CGRect(origin: .zero, size: availableSize) + + self.containerView.bounds = CGRect(origin: .zero, size: size) + self.containerView.center = component.position + + self.effectView.frame = CGRect(origin: CGPoint(x: 0.0, y: -7.0), size: CGSize(width: size.width, height: size.height + 7.0)) + + if isFirstTime { + self.containerView.layer.animateScale(from: 0.0, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + return availableSize + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.containerView.frame.contains(point) { + return self.dismissView + } + return super.hitTest(point, with: event) + } + } + + 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) + } +} + +private final class SliderView: UIView { + private let foregroundView: UIView + private let knobView: UIImageView + + let minValue: CGFloat + let maxValue: CGFloat + var value: CGFloat = 1.0 { + didSet { + self.updateValue() + } + } + + private let valueChanged: (CGFloat, Bool) -> Void + + private let hapticFeedback = HapticFeedback() + + init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + self.minValue = minValue + self.maxValue = maxValue + self.value = value + self.valueChanged = valueChanged + + self.foregroundView = UIView() + self.foregroundView.backgroundColor = UIColor(rgb: 0x8b8b8a) + + self.knobView = UIImageView(image: generateFilledCircleImage(diameter: 30.0, color: .white)) + + super.init(frame: .zero) + + self.backgroundColor = UIColor(rgb: 0x3e3e3e) + self.clipsToBounds = true + self.layer.cornerRadius = 15.0 + + self.foregroundView.isUserInteractionEnabled = false + self.knobView.isUserInteractionEnabled = false + + self.addSubview(self.foregroundView) + self.addSubview(self.knobView) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.addGestureRecognizer(panGestureRecognizer) + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.addGestureRecognizer(tapGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateValue(transition: Transition = .immediate) { + let width = self.frame.width + + let range = self.maxValue - self.minValue + let value = (self.value - self.minValue) / range + + transition.setFrame(view: self.foregroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: 15.0 + value * (width - 30.0), height: 30.0))) + transition.setFrame(view: self.knobView, frame: CGRect(origin: CGPoint(x: (width - 30.0) * value, y: 0.0), size: CGSize(width: 30.0, height: 30.0))) + } + + @objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) { + let range = self.maxValue - self.minValue + switch gestureRecognizer.state { + case .began: + break + case .changed: + let previousValue = self.value + + let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x + let delta = translation / self.bounds.width * range + self.value = max(self.minValue, min(self.maxValue, self.value + delta)) + gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view) + + if self.value == 1.0 && previousValue != 1.0 { + self.hapticFeedback.impact(.soft) + } else if self.value == 0.0 && previousValue != 0.0 { + self.hapticFeedback.impact(.soft) + } + if abs(previousValue - self.value) >= 0.001 { + self.valueChanged(self.value, false) + } + case .ended: + let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x + let delta = translation / self.bounds.width * range + self.value = max(self.minValue, min(self.maxValue, self.value + delta)) + self.valueChanged(self.value, true) + default: + break + } + } + + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + let range = self.maxValue - self.minValue + let location = gestureRecognizer.location(in: gestureRecognizer.view) + self.value = max(self.minValue, min(self.maxValue, self.minValue + location.x / self.bounds.width * range)) + self.valueChanged(self.value, true) + } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } +} + +final class CameraFrontFlashOverlayController: ViewController { + class Node: ASDisplayNode { + init(color: UIColor) { + super.init() + + self.backgroundColor = color + } + } + + private let color: UIColor + init(color: UIColor) { + self.color = color + + super.init(navigationBarPresentationData: nil) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = Node(color: self.color) + self.displayNodeDidLoad() + } + + func dismissAnimated() { + self.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + self.dismiss() + }) + } +} diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index bfb1926d4ef..001b3a22217 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -21,6 +21,7 @@ private let buttonSize = CGSize(width: 55.0, height: 44.0) final class ModeComponent: Component { let isTablet: Bool let strings: PresentationStrings + let tintColor: UIColor let availableModes: [CameraMode] let currentMode: CameraMode let updatedMode: (CameraMode) -> Void @@ -29,6 +30,7 @@ final class ModeComponent: Component { init( isTablet: Bool, strings: PresentationStrings, + tintColor: UIColor, availableModes: [CameraMode], currentMode: CameraMode, updatedMode: @escaping (CameraMode) -> Void, @@ -36,6 +38,7 @@ final class ModeComponent: Component { ) { self.isTablet = isTablet self.strings = strings + self.tintColor = tintColor self.availableModes = availableModes self.currentMode = currentMode self.updatedMode = updatedMode @@ -49,6 +52,9 @@ final class ModeComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.tintColor != rhs.tintColor { + return false + } if lhs.availableModes != rhs.availableModes { return false } @@ -82,8 +88,17 @@ final class ModeComponent: Component { self.pressed() } - func update(value: String, selected: Bool) { - self.setAttributedTitle(NSAttributedString(string: value.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: selected ? UIColor(rgb: 0xf8d74a) : .white, paragraphAlignment: .center), for: .normal) + func update(value: String, selected: Bool, tintColor: UIColor) { + let accentColor: UIColor + let normalColor: UIColor + if tintColor.rgb == 0xffffff { + accentColor = UIColor(rgb: 0xf8d74a) + normalColor = .white + } else { + accentColor = tintColor + normalColor = tintColor.withAlphaComponent(0.5) + } + self.setAttributedTitle(NSAttributedString(string: value.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: selected ? accentColor : normalColor, paragraphAlignment: .center), for: .normal) } } @@ -152,7 +167,7 @@ final class ModeComponent: Component { updatedMode(mode) } - itemView.update(value: mode.title(strings: component.strings), selected: mode == component.currentMode) + itemView.update(value: mode.title(strings: component.strings), selected: mode == component.currentMode, tintColor: component.tintColor) itemView.bounds = CGRect(origin: .zero, size: itemFrame.size) if isTablet { @@ -199,17 +214,23 @@ final class ModeComponent: Component { final class HintLabelComponent: Component { let text: String + let tintColor: UIColor init( - text: String + text: String, + tintColor: UIColor ) { self.text = text + self.tintColor = tintColor } static func ==(lhs: HintLabelComponent, rhs: HintLabelComponent) -> Bool { if lhs.text != rhs.text { return false } + if lhs.tintColor != rhs.tintColor { + return false + } return true } @@ -247,7 +268,7 @@ final class HintLabelComponent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: .white)), + text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: component.tintColor)), horizontalAlignment: .center ) ), diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index a944705c843..1d5e0b34d66 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -64,7 +64,6 @@ private final class AnimatableProperty { } func tick(timestamp: Double) -> Bool { - guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else { return false } @@ -163,10 +162,14 @@ final class ShutterBlobView: UIView { } } - var primaryRedness: CGFloat { + func primaryRedness(tintColor: UIColor) -> CGFloat { switch self { case .generic: - return 0.0 + if tintColor.rgb == 0x000000 { + return -1.0 + } else { + return 0.0 + } default: return 1.0 } @@ -290,14 +293,14 @@ final class ShutterBlobView: UIView { self.displayLink?.invalidate() } - func updateState(_ state: BlobState, transition: Transition = .immediate) { + func updateState(_ state: BlobState, tintColor: UIColor, transition: Transition = .immediate) { guard self.state != state else { return } self.state = state - + self.primarySize.update(value: state.primarySize, transition: transition) - self.primaryRedness.update(value: state.primaryRedness, transition: transition) + self.primaryRedness.update(value: state.primaryRedness(tintColor: tintColor), transition: transition) self.primaryCornerRadius.update(value: state.primaryCornerRadius, transition: transition) self.secondarySize.update(value: state.secondarySize, transition: transition) self.secondaryRedness.update(value: state.secondaryRedness, transition: transition) @@ -453,7 +456,6 @@ final class ShutterBlobView: UIView { renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1) renderEncoder.endEncoding() - var storedDrawable: MetalImageLayer.Drawable? = drawable commandBuffer.addCompletedHandler { _ in DispatchQueue.main.async { diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/BUILD new file mode 100644 index 00000000000..85fd55a2dfa --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatBotInfoItem", + module_name = "ChatBotInfoItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/UrlEscaping", + "//submodules/PhotoResources", + "//submodules/AccountContext", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramUniversalVideoContent", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift similarity index 82% rename from submodules/TelegramUI/Sources/ChatBotInfoItem.swift rename to submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index cd34983cc4e..c755c7aa445 100644 --- a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -14,6 +14,7 @@ import UniversalMediaPlayer import TelegramUniversalVideoContent import WallpaperBackgroundNode import ChatControllerInteraction +import ChatMessageBubbleContentNode private let messageFont = Font.regular(17.0) private let messageBoldFont = Font.semibold(17.0) @@ -21,7 +22,7 @@ private let messageItalicFont = Font.italic(17.0) private let messageBoldItalicFont = Font.semiboldItalic(17.0) private let messageFixedFont = UIFont(name: "Menlo-Regular", size: 16.0) ?? UIFont.systemFont(ofSize: 17.0) -final class ChatBotInfoItem: ListViewItem { +public final class ChatBotInfoItem: ListViewItem { fileprivate let title: String fileprivate let text: String fileprivate let photo: TelegramMediaImage? @@ -30,7 +31,7 @@ final class ChatBotInfoItem: ListViewItem { fileprivate let presentationData: ChatPresentationData fileprivate let context: AccountContext - init(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?, controllerInteraction: ChatControllerInteraction, presentationData: ChatPresentationData, context: AccountContext) { + public init(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?, controllerInteraction: ChatControllerInteraction, presentationData: ChatPresentationData, context: AccountContext) { self.title = title self.text = text self.photo = photo @@ -40,7 +41,7 @@ final class ChatBotInfoItem: ListViewItem { self.context = context } - 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) { let configure = { let node = ChatBotInfoItemNode() @@ -65,7 +66,7 @@ final class ChatBotInfoItem: ListViewItem { } } - 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? ChatBotInfoItemNode { let nodeLayout = nodeValue.asyncLayout() @@ -83,20 +84,20 @@ final class ChatBotInfoItem: ListViewItem { } } -final class ChatBotInfoItemNode: ListViewItemNode { - var controllerInteraction: ChatControllerInteraction? - - let offsetContainer: ASDisplayNode - let backgroundNode: ASImageNode - let imageNode: TransformImageNode - var videoNode: UniversalVideoNode? - let titleNode: TextNode - let textNode: TextNode +public final class ChatBotInfoItemNode: ListViewItemNode { + public var controllerInteraction: ChatControllerInteraction? + + public let offsetContainer: ASDisplayNode + public let backgroundNode: ASImageNode + public let imageNode: TransformImageNode + public var videoNode: UniversalVideoNode? + public let titleNode: TextNode + public let textNode: TextNode private var linkHighlightingNode: LinkHighlightingNode? private let fetchDisposable = MetaDisposable() - var currentTextAndEntities: (String, [MessageTextEntity])? + public var currentTextAndEntities: (String, [MessageTextEntity])? private var theme: ChatPresentationThemeData? @@ -107,7 +108,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { private var item: ChatBotInfoItem? - init() { + public init() { self.offsetContainer = ASDisplayNode() self.backgroundNode = ASImageNode() @@ -162,20 +163,20 @@ final class ChatBotInfoItemNode: ListViewItemNode { videoNode.play() } - override func didLoad() { + override public func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) recognizer.tapActionAtPoint = { [weak self] point in if let strongSelf = self { let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true) - switch tapAction { - case .none: - break - case .ignore: - return .fail - case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji: - return .waitForSingleTap + switch tapAction.content { + case .none: + break + case .ignore: + return .fail + case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji: + return .waitForSingleTap } } @@ -189,7 +190,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { self.view.addGestureRecognizer(recognizer) } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { super.updateAbsoluteRect(rect, within: containerSize) self.absolutePosition = (rect, containerSize) @@ -201,7 +202,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: ChatBotInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + public func asyncLayout() -> (_ item: ChatBotInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeImageLayout = self.imageNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) @@ -229,7 +230,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { updatedTextAndEntities = (item.text, generateTextEntities(item.text, enabledTypes: .all)) } - let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.presentationData.theme.theme.chat.message.infoPrimaryTextColor, linkColor: item.presentationData.theme.theme.chat.message.infoLinkTextColor, baseFont: messageFont, linkFont: messageFont, boldFont: messageBoldFont, italicFont: messageItalicFont, boldItalicFont: messageBoldItalicFont, fixedFont: messageFixedFont, blockQuoteFont: messageFont, message: nil) + let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.presentationData.theme.theme.chat.message.infoPrimaryTextColor, linkColor: item.presentationData.theme.theme.chat.message.infoLinkTextColor, baseFont: messageFont, linkFont: messageFont, boldFont: messageBoldFont, italicFont: messageItalicFont, boldItalicFont: messageBoldItalicFont, fixedFont: messageFixedFont, blockQuoteFont: messageFont, message: nil, adjustQuoteFontSize: true) let horizontalEdgeInset: CGFloat = 10.0 + params.leftInset let horizontalContentInset: CGFloat = 12.0 @@ -350,7 +351,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { } } - override func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { + override public func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { if height.isLessThanOrEqualTo(0.0) { transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) } else { @@ -358,25 +359,25 @@ final class ChatBotInfoItemNode: ListViewItemNode { } } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } - 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: duration * 0.5) } - 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: duration * 0.5, removeOnCompletion: false) } - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let result = super.point(inside: point, with: event) let extra = self.offsetContainer.frame.contains(point) return result || extra } - func updateTouchesAtPoint(_ point: CGPoint?) { + public func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [CGRect]? if let point = point { @@ -418,7 +419,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { } } - func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -426,71 +427,71 @@ final class ChatBotInfoItemNode: ListViewItemNode { if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .textMention(peerName) + return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return .botCommand(botCommand) + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { - case .tap: - let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) - switch tapAction { + case .tap: + let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) + switch tapAction.content { + case .none, .ignore: + break + case let .url(url): + self.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, progress: tapAction.activate?())) + case let .peerMention(peerId, _, _): + if let item = self.item { + let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + if let peer = peer { + self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) + } + }) + } + case let .textMention(name): + self.item?.controllerInteraction.openPeerMention(name, tapAction.activate?()) + case let .botCommand(command): + self.item?.controllerInteraction.sendBotCommand(nil, command) + case let .hashtag(peerName, hashtag): + self.item?.controllerInteraction.openHashtag(peerName, hashtag) + default: + break + } + case .longTap, .doubleTap: + if let item = self.item, self.backgroundNode.frame.contains(location) { + let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) + switch tapAction.content { case .none, .ignore: break - case let .url(url, concealed): - self.item?.controllerInteraction.openUrl(url, concealed, nil, nil) - case let .peerMention(peerId, _, _): - if let item = self.item { - let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - if let peer = peer { - self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) - } - }) - } + case let .url(url): + item.controllerInteraction.longTap(.url(url.url), nil) + case let .peerMention(peerId, mention, _): + item.controllerInteraction.longTap(.peerMention(peerId, mention), nil) case let .textMention(name): - self.item?.controllerInteraction.openPeerMention(name) + item.controllerInteraction.longTap(.mention(name), nil) case let .botCommand(command): - self.item?.controllerInteraction.sendBotCommand(nil, command) - case let .hashtag(peerName, hashtag): - self.item?.controllerInteraction.openHashtag(peerName, hashtag) + item.controllerInteraction.longTap(.command(command), nil) + case let .hashtag(_, hashtag): + item.controllerInteraction.longTap(.hashtag(hashtag), nil) default: break - } - case .longTap, .doubleTap: - if let item = self.item, self.backgroundNode.frame.contains(location) { - let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) - switch tapAction { - case .none, .ignore: - break - case let .url(url, _): - item.controllerInteraction.longTap(.url(url), nil) - case let .peerMention(peerId, mention, _): - item.controllerInteraction.longTap(.peerMention(peerId, mention), nil) - case let .textMention(name): - item.controllerInteraction.longTap(.mention(name), nil) - case let .botCommand(command): - item.controllerInteraction.longTap(.command(command), nil) - case let .hashtag(_, hashtag): - item.controllerInteraction.longTap(.hashtag(hashtag), nil) - default: - break } } default: diff --git a/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/BUILD new file mode 100644 index 00000000000..8edacf5a555 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatBotStartInputPanelNode", + module_name = "ChatBotStartInputPanelNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/ChatPresentationInterfaceState", + "//submodules/SolidRoundedButtonNode", + "//submodules/TooltipUI", + "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/Sources/ChatBotStartInputPanelNode.swift similarity index 85% rename from submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift rename to submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/Sources/ChatBotStartInputPanelNode.swift index ced09c12294..f9a983a9111 100644 --- a/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode/Sources/ChatBotStartInputPanelNode.swift @@ -9,15 +9,16 @@ import TelegramPresentationData import ChatPresentationInterfaceState import SolidRoundedButtonNode import TooltipUI +import ChatInputPanelNode -final class ChatBotStartInputPanelNode: ChatInputPanelNode { +public final class ChatBotStartInputPanelNode: ChatInputPanelNode { private let button: SolidRoundedButtonNode private var statusDisposable: Disposable? private var presentationInterfaceState: ChatPresentationInterfaceState? - override var interfaceInteraction: ChatPanelInterfaceInteraction? { + override public var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { if let _ = self.interfaceInteraction { if self.statusDisposable == nil { @@ -51,7 +52,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { private var tooltipController: TooltipScreen? private var tooltipDismissed = false - init(theme: PresentationTheme, strings: PresentationStrings) { + public init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme self.strings = strings @@ -72,7 +73,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.tooltipController?.dismiss() } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { if self.theme !== theme || self.strings !== strings { self.theme = theme self.strings = strings @@ -81,7 +82,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { } } - @objc func buttonPressed() { + @objc private func buttonPressed() { guard let _ = self.context, let presentationInterfaceState = self.presentationInterfaceState else { return } @@ -95,7 +96,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { super.updateAbsoluteRect(rect, within: containerSize, transition: transition) let absoluteFrame = self.button.view.convert(self.button.bounds, to: nil) @@ -107,7 +108,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } @@ -154,7 +155,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { return panelHeight } - override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override public func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { return defaultHeight(metrics: metrics) + 27.0 } } diff --git a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/BUILD new file mode 100644 index 00000000000..8a7107a81e0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatButtonKeyboardInputNode", + module_name = "ChatButtonKeyboardInputNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ChatPresentationInterfaceState", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/ChatInputNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift similarity index 95% rename from submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift rename to submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift index a92e298b644..3c7dfab0a4d 100644 --- a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift @@ -197,7 +197,7 @@ private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNo } } -final class ChatButtonKeyboardInputNode: ChatInputNode { +public final class ChatButtonKeyboardInputNode: ChatInputNode { private let context: AccountContext private let controllerInteraction: ChatControllerInteraction @@ -212,7 +212,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { private var theme: PresentationTheme? - init(context: AccountContext, controllerInteraction: ChatControllerInteraction) { + public init(context: AccountContext, controllerInteraction: ChatControllerInteraction) { self.context = context self.controllerInteraction = controllerInteraction @@ -236,7 +236,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { self.addSubnode(self.separatorNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { @@ -245,7 +245,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } private var absoluteRect: (CGRect, CGSize)? - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { self.absoluteRect = (rect, containerSize) if let backgroundNode = self.backgroundNode { @@ -263,7 +263,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { + override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) if self.backgroundNode == nil { @@ -370,7 +370,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } } - @objc func buttonPressed(_ button: ASButtonNode) { + @objc private func buttonPressed(_ button: ASButtonNode) { if let button = button as? ChatButtonKeyboardInputButtonNode, let markupButton = button.button { var dismissIfOnce = false switch markupButton.action { @@ -378,7 +378,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { self.controllerInteraction.sendMessage(markupButton.title) dismissIfOnce = true case let .url(url): - self.controllerInteraction.openUrl(url, true, nil, nil) + self.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: true)) case .requestMap: self.controllerInteraction.shareCurrentLocation() case .requestPhone: diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD new file mode 100644 index 00000000000..cc26e420ef0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatChannelSubscriberInputPanelNode", + module_name = "ChatChannelSubscriberInputPanelNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AlertUI", + "//submodules/PresentationDataUtils", + "//submodules/PeerInfoUI", + "//submodules/UndoUI", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift similarity index 96% rename from submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift rename to submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index 74276d1072f..cc5bd993c50 100644 --- a/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -11,6 +11,7 @@ import PresentationDataUtils import PeerInfoUI import UndoUI import ChatPresentationInterfaceState +import ChatInputPanelNode private enum SubscriberAction: Equatable { case join @@ -113,7 +114,7 @@ private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterface private let badgeFont = Font.regular(14.0) -final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { +public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private let button: HighlightableButtonNode private let discussButton: HighlightableButtonNode private let discussButtonText: ImmediateTextNode @@ -133,7 +134,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, Bool, LayoutMetrics)? - override init() { + public override init() { self.button = HighlightableButtonNode() self.discussButton = HighlightableButtonNode() self.activityIndicator = UIActivityIndicatorView(style: .gray) @@ -176,18 +177,18 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { self.badgeDisposable.dispose() } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } return super.hitTest(point, with: event) } - @objc func helpPressed() { + @objc private func helpPressed() { self.interfaceInteraction?.presentGigagroupHelp() } - @objc func buttonPressed() { + @objc private func buttonPressed() { guard let context = self.context, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { return } @@ -269,7 +270,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, force: false) } @@ -361,7 +362,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { return panelHeight } - override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override public func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { return defaultHeight(metrics: metrics) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/BUILD b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/BUILD new file mode 100644 index 00000000000..436d2751716 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatContextResultPeekContent", + module_name = "ChatContextResultPeekContent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/PhotoResources", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/SoftwareVideo", + "//submodules/TelegramUI/Components/MultiplexedVideoNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift index c1dd40fcea4..d32cd4adf89 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent/Sources/ChatContextResultPeekContent.swift @@ -12,34 +12,34 @@ import ContextUI import SoftwareVideo import MultiplexedVideoNode -final class ChatContextResultPeekContent: PeekControllerContent { - let account: Account - let contextResult: ChatContextResult - let menu: [ContextMenuItem] +public final class ChatContextResultPeekContent: PeekControllerContent { + public let account: Account + public let contextResult: ChatContextResult + public let menu: [ContextMenuItem] - init(account: Account, contextResult: ChatContextResult, menu: [ContextMenuItem]) { + public init(account: Account, contextResult: ChatContextResult, menu: [ContextMenuItem]) { self.account = account self.contextResult = contextResult self.menu = menu } - func presentation() -> PeekControllerContentPresentation { + public func presentation() -> PeekControllerContentPresentation { return .contained } - func menuActivation() -> PeerControllerMenuActivation { + public func menuActivation() -> PeerControllerMenuActivation { return .drag } - func menuItems() -> [ContextMenuItem] { + public func menuItems() -> [ContextMenuItem] { return self.menu } - func node() -> PeekControllerContentNode & ASDisplayNode { + public func node() -> PeekControllerContentNode & ASDisplayNode { return ChatContextResultPeekNode(account: self.account, contextResult: self.contextResult) } - func topAccessoryNode() -> ASDisplayNode? { + public func topAccessoryNode() -> ASDisplayNode? { let arrowNode = ASImageNode() if let image = UIImage(bundleImageName: "Peek/Arrow") { arrowNode.image = image @@ -48,11 +48,11 @@ final class ChatContextResultPeekContent: PeekControllerContent { return arrowNode } - func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? { + public func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? { return nil } - func isEqual(to: PeekControllerContent) -> Bool { + public func isEqual(to: PeekControllerContent) -> Bool { if let to = to as? ChatContextResultPeekContent { return self.contextResult == to.contextResult } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD new file mode 100644 index 00000000000..881c4fbbf06 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatHistoryEntry", + module_name = "ChatHistoryEntry", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/TemporaryCachedPeerDataManager", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift similarity index 89% rename from submodules/TelegramUI/Sources/ChatHistoryEntry.swift rename to submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index 3c0a1f1cb1e..ee01ff53fd2 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -12,15 +12,15 @@ public enum ChatMessageEntryContentType { } public struct ChatMessageEntryAttributes: Equatable { - var rank: CachedChannelAdminRank? - var isContact: Bool - var contentTypeHint: ChatMessageEntryContentType - var updatingMedia: ChatUpdatingMessageMedia? - var isPlaying: Bool - var isCentered: Bool - var authorStoryStats: PeerStoryStats? + public var rank: CachedChannelAdminRank? + public var isContact: Bool + public var contentTypeHint: ChatMessageEntryContentType + public var updatingMedia: ChatUpdatingMessageMedia? + public var isPlaying: Bool + public var isCentered: Bool + public var authorStoryStats: PeerStoryStats? - init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?) { + public init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?) { self.rank = rank self.isContact = isContact self.contentTypeHint = contentTypeHint @@ -41,7 +41,7 @@ public struct ChatMessageEntryAttributes: Equatable { } } -enum ChatHistoryEntry: Identifiable, Comparable { +public enum ChatHistoryEntry: Identifiable, Comparable { case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryLocation?, ChatHistoryMessageSelection, ChatMessageEntryAttributes) case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData) case UnreadEntry(MessageIndex, ChatPresentationData) @@ -49,7 +49,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { case ChatInfoEntry(String, String, TelegramMediaImage?, TelegramMediaFile?, ChatPresentationData) case SearchEntry(PresentationTheme, PresentationStrings) - var stableId: UInt64 { + public var stableId: UInt64 { switch self { case let .MessageEntry(message, _, _, _, _, attributes): let type: UInt64 @@ -75,7 +75,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - var index: MessageIndex { + public var index: MessageIndex { switch self { case let .MessageEntry(message, _, _, _, _, _): return message.index @@ -92,7 +92,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - var firstIndex: MessageIndex { + public var firstIndex: MessageIndex { switch self { case let .MessageEntry(message, _, _, _, _, _): return message.index @@ -109,7 +109,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { + public static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { switch lhs { case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection, lhsAttributes): switch rhs { @@ -120,6 +120,18 @@ enum ChatHistoryEntry: Identifiable, Comparable { if lhsMessage.stableVersion != rhsMessage.stableVersion { return false } + + if lhsMessage.peers.count != rhsMessage.peers.count { + return false + } + for (id, peer) in lhsMessage.peers { + if let otherPeer = rhsMessage.peers[id] { + if !peer.isEqual(otherPeer) { + return false + } + } + } + if lhsMessage.media.count != rhsMessage.media.count { return false } @@ -264,7 +276,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - static func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { + public static func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { let lhsIndex = lhs.index let rhsIndex = rhs.index if lhsIndex == rhsIndex { diff --git a/submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/BUILD new file mode 100644 index 00000000000..39b8a7ad0b1 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInputContextPanelNode", + module_name = "ChatInputContextPanelNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/Sources/ChatInputContextPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/Sources/ChatInputContextPanelNode.swift new file mode 100644 index 00000000000..c6818c877d0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode/Sources/ChatInputContextPanelNode.swift @@ -0,0 +1,42 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import ChatPresentationInterfaceState +import ChatControllerInteraction + +public enum ChatInputContextPanelPlacement { + case overPanels + case overTextInput +} + +open class ChatInputContextPanelNode: ASDisplayNode { + public let context: AccountContext + open var interfaceInteraction: ChatPanelInterfaceInteraction? + open var placement: ChatInputContextPanelPlacement = .overPanels + open var theme: PresentationTheme + open var fontSize: PresentationFontSize + + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) { + self.context = context + self.theme = theme + self.fontSize = fontSize + + super.init() + } + + open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + } + + open func animateOut(completion: @escaping () -> Void) { + completion() + } + + open var topItemFrame: CGRect? { + return nil + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD new file mode 100644 index 00000000000..57e1434665a --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInputPanelNode", + module_name = "ChatInputPanelNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/ChatPresentationInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift new file mode 100644 index 00000000000..3c44da17c23 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift @@ -0,0 +1,43 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import AccountContext +import ChatPresentationInterfaceState + +public protocol ChatInputPanelViewForOverlayContent: UIView { + func maybeDismissContent(point: CGPoint) +} + +open class ChatInputPanelNode: ASDisplayNode { + open var context: AccountContext? + open var interfaceInteraction: ChatPanelInterfaceInteraction? + open var prevInputPanelNode: ChatInputPanelNode? + + open var viewForOverlayContent: ChatInputPanelViewForOverlayContent? + + open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + } + + open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + return 0.0 + } + + open func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + return 0.0 + } + + open func defaultHeight(metrics: LayoutMetrics) -> CGFloat { + if case .regular = metrics.widthClass, case .regular = metrics.heightClass { + return 49.0 + } else { + return 45.0 + } + } + + open func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { + return false + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD new file mode 100644 index 00000000000..a9607fe68e4 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInputTextNode", + module_name = "ChatInputTextNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl", + "//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD new file mode 100644 index 00000000000..a588d9c6011 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD @@ -0,0 +1,23 @@ + +objc_library( + name = "ChatInputTextViewImpl", + enable_modules = True, + module_name = "ChatInputTextViewImpl", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.c", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h new file mode 100755 index 00000000000..528c45c33f8 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h @@ -0,0 +1,28 @@ +#ifndef ChatInputTextViewImpl_h +#define ChatInputTextViewImpl_h + +#import +#import + +@interface ChatInputTextViewImplTargetForAction: NSObject + +@property (nonatomic, strong, readonly) id _Nullable target; + +- (instancetype _Nonnull)initWithTarget:(id _Nullable)target; + +@end + +@interface ChatInputTextViewImpl : UITextView + +@property (nonatomic, copy) bool (^ _Nullable shouldCopy)(); +@property (nonatomic, copy) bool (^ _Nullable shouldPaste)(); +@property (nonatomic, copy) bool (^ _Nullable shouldRespondToAction)(SEL _Nullable); +@property (nonatomic, copy) bool (^ _Nullable shouldReturn)(); +@property (nonatomic, copy) void (^ _Nullable backspaceWhileEmpty)(); +@property (nonatomic, copy) void (^ _Nullable dropAutocorrectioniOS16)(); + +- (instancetype _Nonnull)initWithFrame:(CGRect)frame textContainer:(NSTextContainer * _Nullable)textContainer disableTiling:(bool)disableTiling; + +@end + +#endif /* Lottie_h */ diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m new file mode 100755 index 00000000000..8e96504a123 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m @@ -0,0 +1,143 @@ +#import + +@implementation ChatInputTextViewImplTargetForAction + +- (instancetype)initWithTarget:(id _Nullable)target { + self = [super init]; + if (self != nil) { + _target = target; + } + return self; +} + +@end + +@interface ChatInputTextViewImpl () { + UIGestureRecognizer *_tapRecognizer; +} + +@end + +@implementation ChatInputTextViewImpl + +- (instancetype _Nonnull)initWithFrame:(CGRect)frame textContainer:(NSTextContainer * _Nullable)textContainer disableTiling:(bool)disableTiling { + self = [super initWithFrame:frame textContainer:textContainer]; + if (self != nil) { + if (disableTiling) { + SEL selector = NSSelectorFromString(@"_disableTiledViews"); + if (selector && [self respondsToSelector:selector]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self performSelector:selector]; +#pragma clang diagnostic pop + } + } + + if (@available(iOS 17.0, *)) { + } else { + _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(workaroundTapGesture:)]; + _tapRecognizer.cancelsTouchesInView = false; + _tapRecognizer.delaysTouchesBegan = false; + _tapRecognizer.delaysTouchesEnded = false; + _tapRecognizer.delegate = self; + [self addGestureRecognizer:_tapRecognizer]; + } + } + return self; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + return true; +} + +- (void)workaroundTapGesture:(UITapGestureRecognizer *)recognizer { + if (recognizer.state == UIGestureRecognizerStateEnded) { + static Class promptClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptClass = NSClassFromString([[NSString alloc] initWithFormat:@"%@AutocorrectInlinePrompt", @"UI"]); + }); + UIView *result = [self hitTest:[recognizer locationInView:self] withEvent:nil]; + if (result != nil && [result class] == promptClass) { + if (_dropAutocorrectioniOS16) { + _dropAutocorrectioniOS16(); + } + } + } +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + if (_shouldRespondToAction) { + if (!_shouldRespondToAction(action)) { + return false; + } + } + + if (action == @selector(paste:)) { + NSArray *items = [UIMenuController sharedMenuController].menuItems; + if (((UIMenuItem *)items.firstObject).action == @selector(toggleBoldface:)) { + return false; + } + return true; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + static SEL promptForReplaceSelector; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptForReplaceSelector = NSSelectorFromString(@"_promptForReplace:"); + }); + if (action == promptForReplaceSelector) { + return false; + } +#pragma clang diagnostic pop + + if (action == @selector(toggleUnderline:)) { + return false; + } + + return [super canPerformAction:action withSender:sender]; +} + +- (id)targetForAction:(SEL)action withSender:(id)__unused sender { + return [super targetForAction:action withSender:sender]; +} + +- (void)copy:(id)sender { + if (_shouldCopy == nil || _shouldCopy()) { + [super copy:sender]; + } +} + +- (void)paste:(id)sender { + if (_shouldPaste == nil || _shouldPaste()) { + [super paste:sender]; + } +} + +- (NSArray *)keyCommands { + UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)]; + return @[ + plainReturn + ]; +} + +- (void)handlePlainReturn:(id)__unused sender { + if (_shouldReturn) { + _shouldReturn(); + } +} + +- (void)deleteBackward { + bool notify = self.text.length == 0; + [super deleteBackward]; + if (notify) { + if (_backspaceWhileEmpty) { + _backspaceWhileEmpty(); + } + } +} + +@end diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift new file mode 100644 index 00000000000..c293c74e2fc --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -0,0 +1,833 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import AppBundle +import ChatInputTextViewImpl +import MessageInlineBlockBackgroundView + +public protocol ChatInputTextNodeDelegate: AnyObject { + func chatInputTextNodeDidUpdateText() + func chatInputTextNodeShouldReturn() -> Bool + func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) + func chatInputTextNodeDidBeginEditing() + func chatInputTextNodeDidFinishEditing() + func chatInputTextNodeBackspaceWhileEmpty() + + @available(iOS 13.0, *) + func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu + + func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool + func chatInputTextNodeShouldCopy() -> Bool + func chatInputTextNodeShouldPaste() -> Bool + + func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool +} + +open class ChatInputTextNode: ASDisplayNode, UITextViewDelegate { + public weak var delegate: ChatInputTextNodeDelegate? { + didSet { + self.textView.customDelegate = self.delegate + } + } + + private var selectionChangedForEditedText: Bool = false + + public var textView: ChatInputTextView { + return self.view as! ChatInputTextView + } + + public var keyboardAppearance: UIKeyboardAppearance { + get { + return self.textView.keyboardAppearance + } + set { + guard newValue != self.keyboardAppearance else { + return + } + self.textView.keyboardAppearance = newValue + self.textView.reloadInputViews() + } + } + + public var initialPrimaryLanguage: String? { + get { + return self.textView.initialPrimaryLanguage + } set(value) { + self.textView.initialPrimaryLanguage = value + } + } + + public func isCurrentlyEmoji() -> Bool { + return false + } + + public var textInputMode: UITextInputMode? { + return self.textView.textInputMode + } + + public var selectedRange: NSRange { + get { + return self.textView.selectedRange + } set(value) { + if self.textView.selectedRange != value { + self.textView.selectedRange = value + } + } + } + + public var attributedText: NSAttributedString? { + get { + return self.textView.attributedText + } set(value) { + self.textView.attributedText = value + } + } + + public var isRTL: Bool { + return self.textView.isRTL + } + + public var selectionRect: CGRect { + guard let range = self.textView.selectedTextRange else { + return self.textView.bounds + } + return self.textView.firstRect(for: range) + } + + public var textContainerInset: UIEdgeInsets { + get { + return self.textView.defaultTextContainerInset + } set(value) { + let targetValue = UIEdgeInsets(top: value.top, left: value.left, bottom: value.bottom, right: value.right) + if self.textView.defaultTextContainerInset != value { + self.textView.defaultTextContainerInset = targetValue + } + } + } + + public init(disableTiling: Bool = false) { + super.init() + + self.setViewBlock({ + return ChatInputTextView(disableTiling: disableTiling) + }) + + self.textView.delegate = self + self.textView.shouldRespondToAction = { [weak self] action in + guard let self, let action else { + return false + } + if let delegate = self.delegate { + return delegate.chatInputTextNodeShouldRespondToAction(action: action) + } else { + return true + } + } + } + + public func resetInitialPrimaryLanguage() { + } + + public func textHeightForWidth(_ width: CGFloat, rightInset: CGFloat) -> CGFloat { + return self.textView.textHeightForWidth(width, rightInset: rightInset) + } + + @objc public func textViewDidBeginEditing(_ textView: UITextView) { + self.delegate?.chatInputTextNodeDidBeginEditing() + } + + @objc public func textViewDidEndEditing(_ textView: UITextView) { + self.delegate?.chatInputTextNodeDidFinishEditing() + } + + @objc public func textViewDidChange(_ textView: UITextView) { + self.selectionChangedForEditedText = true + + self.delegate?.chatInputTextNodeDidUpdateText() + + self.textView.updateTextContainerInset() + } + + @objc public func textViewDidChangeSelection(_ textView: UITextView) { + if self.textView.isPreservingSelection { + return + } + + self.selectionChangedForEditedText = false + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.delegate?.chatInputTextNodeDidChangeSelection(dueToEditing: self.selectionChangedForEditedText) + } + } + + @available(iOS 16.0, *) + @objc public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { + return self.delegate?.chatInputTextNodeMenu(forTextRange: range, suggestedActions: suggestedActions) + } + + @objc public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let delegate = self.delegate else { + return true + } + if self.textView.isPreservingText { + return false + } + return delegate.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) + } + + public func updateLayout(size: CGSize) { + self.textView.updateLayout(size: size) + } +} + +private final class ChatInputTextContainer: NSTextContainer { + var rightInset: CGFloat = 0.0 + + override var isSimpleRectangularTextContainer: Bool { + return false + } + + override init(size: CGSize) { + super.init(size: size) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func lineFragmentRect(forProposedRect proposedRect: CGRect, at characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remaining remainingRect: UnsafeMutablePointer?) -> CGRect { + var result = super.lineFragmentRect(forProposedRect: proposedRect, at: characterIndex, writingDirection: baseWritingDirection, remaining: remainingRect) + + result.origin.x -= 5.0 + result.size.width -= 5.0 + result.size.width -= self.rightInset + + if let textStorage = self.layoutManager?.textStorage { + let string: NSString = textStorage.string as NSString + let index = Int(characterIndex) + if index >= 0 && index < string.length { + let attributes = textStorage.attributes(at: index, effectiveRange: nil) + let blockQuote = attributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] as? NSObject + if let blockQuote { + result.origin.x += 9.0 + result.size.width -= 9.0 + result.size.width -= 7.0 + + var isFirstLine = false + if index == 0 { + isFirstLine = true + } else { + let previousAttributes = textStorage.attributes(at: index - 1, effectiveRange: nil) + let previousBlockQuote = previousAttributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] as? NSObject + if let previousBlockQuote { + if !blockQuote.isEqual(previousBlockQuote) { + isFirstLine = true + } + } else { + isFirstLine = true + } + } + + if (isFirstLine) { + result.size.width -= 18.0 + } + } + } + } + + result.size.width = max(1.0, result.size.width) + + return result + } +} + +public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDelegate, NSTextStorageDelegate { + public final class Theme: Equatable { + public final class Quote: Equatable { + public enum LineStyle: Equatable { + case solid(color: UIColor) + case doubleDashed(mainColor: UIColor, secondaryColor: UIColor) + case tripleDashed(mainColor: UIColor, secondaryColor: UIColor, tertiaryColor: UIColor) + } + public let background: UIColor + public let foreground: UIColor + public let lineStyle: LineStyle + + public init( + background: UIColor, + foreground: UIColor, + lineStyle: LineStyle + ) { + self.background = background + self.foreground = foreground + self.lineStyle = lineStyle + } + + public static func ==(lhs: Quote, rhs: Quote) -> Bool { + if !lhs.background.isEqual(rhs.background) { + return false + } + if !lhs.foreground.isEqual(rhs.foreground) { + return false + } + if lhs.lineStyle != rhs.lineStyle { + return false + } + return true + } + } + + public let quote: Quote + + public init(quote: Quote) { + self.quote = quote + } + + public static func ==(lhs: Theme, rhs: Theme) -> Bool { + if lhs.quote != rhs.quote { + return false + } + return true + } + } + + override public var attributedText: NSAttributedString? { + get { + return super.attributedText + } set(value) { + if self.attributedText != value { + let selectedRange = self.selectedRange + let preserveSelectedRange = selectedRange.location != self.textStorage.length + + super.attributedText = value ?? NSAttributedString() + + if preserveSelectedRange { + self.isPreservingSelection = true + self.selectedRange = selectedRange + self.isPreservingSelection = false + } + + self.updateTextContainerInset() + } + } + } + + fileprivate var isPreservingSelection: Bool = false + fileprivate var isPreservingText: Bool = false + + public weak var customDelegate: ChatInputTextNodeDelegate? + + public var theme: Theme? { + didSet { + if self.theme != oldValue { + self.updateTextElements() + } + } + } + + private let customTextContainer: ChatInputTextContainer + private let customTextStorage: NSTextStorage + private let customLayoutManager: NSLayoutManager + + private let measurementTextContainer: ChatInputTextContainer + private let measurementTextStorage: NSTextStorage + private let measurementLayoutManager: NSLayoutManager + + private var validLayoutSize: CGSize? + private var isUpdatingLayout: Bool = false + + private var blockQuotes: [Int: QuoteBackgroundView] = [:] + + public var defaultTextContainerInset: UIEdgeInsets = UIEdgeInsets() { + didSet { + if self.defaultTextContainerInset != oldValue { + self.updateTextContainerInset() + } + } + } + + private var didInitializePrimaryInputLanguage: Bool = false + public var initialPrimaryLanguage: String? + + override public var textInputMode: UITextInputMode? { + if !self.didInitializePrimaryInputLanguage { + self.didInitializePrimaryInputLanguage = true + if let initialPrimaryLanguage = self.initialPrimaryLanguage { + for inputMode in UITextInputMode.activeInputModes { + if let primaryLanguage = inputMode.primaryLanguage, primaryLanguage == initialPrimaryLanguage { + return inputMode + } + } + } + } + return super.textInputMode + } + + override public var bounds: CGRect { + didSet { + assert(true) + } + } + + public init(disableTiling: Bool) { + self.customTextContainer = ChatInputTextContainer(size: CGSize(width: 100.0, height: 100000.0)) + self.customLayoutManager = NSLayoutManager() + self.customTextStorage = NSTextStorage() + self.customTextStorage.addLayoutManager(self.customLayoutManager) + self.customLayoutManager.addTextContainer(self.customTextContainer) + + self.measurementTextContainer = ChatInputTextContainer(size: CGSize(width: 100.0, height: 100000.0)) + self.measurementLayoutManager = NSLayoutManager() + self.measurementTextStorage = NSTextStorage() + self.measurementTextStorage.addLayoutManager(self.measurementLayoutManager) + self.measurementLayoutManager.addTextContainer(self.measurementTextContainer) + + super.init(frame: CGRect(), textContainer: self.customTextContainer, disableTiling: disableTiling) + + self.textContainerInset = UIEdgeInsets() + self.backgroundColor = nil + self.isOpaque = false + + self.customTextContainer.widthTracksTextView = false + self.customTextContainer.heightTracksTextView = false + + self.measurementTextContainer.widthTracksTextView = false + self.measurementTextContainer.heightTracksTextView = false + + self.customLayoutManager.delegate = self + self.measurementLayoutManager.delegate = self + + self.customTextStorage.delegate = self + self.measurementTextStorage.delegate = self + + self.dropAutocorrectioniOS16 = { [weak self] in + guard let self else { + return + } + + self.isPreservingSelection = true + self.isPreservingText = true + + let rangeCopy = self.selectedRange + var fakeRange = rangeCopy + if fakeRange.location != 0 { + fakeRange.location -= 1 + } + self.unmarkText() + self.selectedRange = fakeRange + self.selectedRange = rangeCopy + + self.isPreservingSelection = false + self.isPreservingText = false + } + + self.shouldCopy = { [weak self] in + guard let self else { + return true + } + return self.customDelegate?.chatInputTextNodeShouldCopy() ?? true + } + self.shouldPaste = { [weak self] in + guard let self else { + return true + } + return self.customDelegate?.chatInputTextNodeShouldPaste() ?? true + } + self.shouldReturn = { [weak self] in + guard let self else { + return true + } + return self.customDelegate?.chatInputTextNodeShouldReturn() ?? true + } + self.backspaceWhileEmpty = { [weak self] in + guard let self else { + return + } + self.customDelegate?.chatInputTextNodeBackspaceWhileEmpty() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func scrollRectToVisible(_ rect: CGRect, animated: Bool) { + var rect = rect + if rect.maxY > self.contentSize.height - 8.0 { + rect = CGRect(origin: CGPoint(x: rect.minX, y: self.contentSize.height - 1.0), size: CGSize(width: rect.width, height: 1.0)) + } + + var animated = animated + if self.isUpdatingLayout { + animated = false + } + + super.scrollRectToVisible(rect, animated: animated) + } + + @objc public func layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingBeforeGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { + guard let textStorage = layoutManager.textStorage else { + return 0.0 + } + let characterIndex = Int(layoutManager.characterIndexForGlyph(at: glyphIndex)) + if characterIndex < 0 || characterIndex >= textStorage.length { + return 0.0 + } + + let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil) + guard let blockQuote = attributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject else { + return 0.0 + } + + if characterIndex != 0 { + let previousAttributes = textStorage.attributes(at: characterIndex - 1, effectiveRange: nil) + let previousBlockQuote = previousAttributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject + if let previousBlockQuote, blockQuote.isEqual(previousBlockQuote) { + return 0.0 + } + } + + return 8.0 + } + + @objc public func layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { + guard let textStorage = layoutManager.textStorage else { + return 0.0 + } + var characterIndex = Int(layoutManager.characterIndexForGlyph(at: glyphIndex)) + characterIndex -= 1 + if characterIndex < 0 { + characterIndex = 0 + } + if characterIndex < 0 || characterIndex >= textStorage.length { + return 0.0 + } + + let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil) + guard let blockQuote = attributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject else { + return 0.0 + } + + if characterIndex + 1 < textStorage.length { + let nextAttributes = textStorage.attributes(at: characterIndex + 1, effectiveRange: nil) + let nextBlockQuote = nextAttributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject + if let nextBlockQuote, blockQuote.isEqual(nextBlockQuote) { + return 0.0 + } + } + + return 8.0 + } + + public func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) { + if textStorage !== self.customTextStorage { + return + } + } + + public func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) { + if textContainer !== self.customTextContainer { + return + } + self.updateTextElements() + } + + public func updateTextContainerInset() { + var result = self.defaultTextContainerInset + + var horizontalInsetsUpdated = false + if self.customTextContainer.rightInset != result.right { + horizontalInsetsUpdated = true + self.customTextContainer.rightInset = result.right + } + + result.left = 0.0 + result.right = 0.0 + + if self.customTextStorage.length != 0 { + let topAttributes = self.customTextStorage.attributes(at: 0, effectiveRange: nil) + let bottomAttributes = self.customTextStorage.attributes(at: self.customTextStorage.length - 1, effectiveRange: nil) + + if topAttributes[NSAttributedString.Key("Attribute__Blockquote")] != nil { + result.top += 7.0 + } + if bottomAttributes[NSAttributedString.Key("Attribute__Blockquote")] != nil { + result.bottom += 8.0 + } + } + + if self.textContainerInset != result { + self.textContainerInset = result + } + if horizontalInsetsUpdated { + self.customLayoutManager.invalidateLayout(forCharacterRange: NSRange(location: 0, length: self.customTextStorage.length), actualCharacterRange: nil) + self.customLayoutManager.ensureLayout(for: self.customTextContainer) + } + + self.updateTextElements() + } + + public func textHeightForWidth(_ width: CGFloat, rightInset: CGFloat) -> CGFloat { + let measureSize = CGSize(width: width, height: 1000000.0) + + if self.measurementTextStorage != self.attributedText || self.measurementTextContainer.size != measureSize || self.measurementTextContainer.rightInset != rightInset { + self.measurementTextContainer.rightInset = rightInset + self.measurementTextStorage.setAttributedString(self.attributedText ?? NSAttributedString()) + self.measurementTextContainer.size = measureSize + self.measurementLayoutManager.invalidateLayout(forCharacterRange: NSRange(location: 0, length: self.measurementTextStorage.length), actualCharacterRange: nil) + self.measurementLayoutManager.ensureLayout(for: self.measurementTextContainer) + } + + let textSize = self.measurementLayoutManager.usedRect(for: self.measurementTextContainer).size + + return textSize.height + self.textContainerInset.top + self.textContainerInset.bottom + } + + public func updateLayout(size: CGSize) { + let measureSize = CGSize(width: size.width, height: 1000000.0) + + if self.textContainer.size != measureSize { + self.textContainer.size = measureSize + self.customLayoutManager.invalidateLayout(forCharacterRange: NSRange(location: 0, length: self.customTextStorage.length), actualCharacterRange: nil) + self.customLayoutManager.ensureLayout(for: self.customTextContainer) + } + } + + override public func setNeedsLayout() { + super.setNeedsLayout() + } + + override public func layoutSubviews() { + let isLayoutUpdated = self.validLayoutSize != self.bounds.size + self.validLayoutSize = self.bounds.size + + self.isUpdatingLayout = isLayoutUpdated + + super.layoutSubviews() + + self.isUpdatingLayout = false + } + + public func updateTextElements() { + var blockQuoteIndex = 0 + var validBlockQuotes: [Int] = [] + + self.textStorage.enumerateAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), in: NSRange(location: 0, length: self.textStorage.length), using: { value, range, _ in + if let value { + let _ = value + + let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) + if self.customLayoutManager.isValidGlyphIndex(glyphRange.location) && self.customLayoutManager.isValidGlyphIndex(glyphRange.location + glyphRange.length - 1) { + } else { + return + } + + let id = blockQuoteIndex + + let blockQuote: QuoteBackgroundView + if let current = self.blockQuotes[id] { + blockQuote = current + } else { + blockQuote = QuoteBackgroundView() + self.blockQuotes[id] = blockQuote + self.insertSubview(blockQuote, at: 0) + } + + var boundingRect = self.customLayoutManager.boundingRect(forGlyphRange: glyphRange, in: self.customTextContainer) + + boundingRect = CGRect() + var startIndex = glyphRange.lowerBound + while startIndex < glyphRange.upperBound { + var effectiveRange = NSRange(location: NSNotFound, length: 0) + let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange) + if boundingRect.isEmpty { + boundingRect = rect + } else { + boundingRect = boundingRect.union(rect) + } + if effectiveRange.location != NSNotFound { + startIndex = max(startIndex + 1, effectiveRange.upperBound) + } else { + break + } + } + + boundingRect.origin.y += self.defaultTextContainerInset.top + + boundingRect.origin.x -= 4.0 + boundingRect.size.width += 4.0 + boundingRect.size.width += 18.0 + boundingRect.size.width = min(boundingRect.size.width, self.bounds.width - 18.0) + + boundingRect.origin.y -= 4.0 + boundingRect.size.height += 8.0 + + blockQuote.frame = boundingRect + if let theme = self.theme { + blockQuote.update(size: boundingRect.size, theme: theme.quote) + } + + validBlockQuotes.append(blockQuoteIndex) + blockQuoteIndex += 1 + } + }) + + var removedBlockQuotes: [Int] = [] + for (id, blockQuote) in self.blockQuotes { + if !validBlockQuotes.contains(id) { + removedBlockQuotes.append(id) + blockQuote.removeFromSuperview() + } + } + for id in removedBlockQuotes { + self.blockQuotes.removeValue(forKey: id) + } + } + + override public func caretRect(for position: UITextPosition) -> CGRect { + var result = super.caretRect(for: position) + + if "".isEmpty { + return result + } + + guard let textStorage = self.customLayoutManager.textStorage else { + return result + } + let _ = textStorage + + let index = self.offset(from: self.beginningOfDocument, to: position) + + let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSMakeRange(index, 1), actualCharacterRange: nil) + var boundingRect = self.customLayoutManager.boundingRect(forGlyphRange: glyphRange, in: self.customTextContainer) + + boundingRect.origin.y += 5.0 + + result.origin.y = boundingRect.minY + result.size.height = boundingRect.height + + return result + } + + override public func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { + let sourceRects = super.selectionRects(for: range) + + var result: [UITextSelectionRect] = [] + for rect in sourceRects { + var mappedRect = rect.rect + //mappedRect.size.height = 10.0 + mappedRect.size.height += 0.0 + result.append(CustomTextSelectionRect( + rect: mappedRect, + writingDirection: rect.writingDirection, + containsStart: rect.containsStart, + containsEnd: rect.containsEnd, + isVertical: rect.isVertical + )) + } + + return result + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + return result + } +} + +private final class CustomTextSelectionRect: UITextSelectionRect { + let rectValue: CGRect + let writingDirectionValue: NSWritingDirection + let containsStartValue: Bool + let containsEndValue: Bool + let isVerticalValue: Bool + + override var rect: CGRect { + return self.rectValue + } + override var writingDirection: NSWritingDirection { + return self.writingDirectionValue + } + override var containsStart: Bool { + return self.containsStartValue + } + override var containsEnd: Bool { + return self.containsEndValue + } + override var isVertical: Bool { + return self.isVerticalValue + } + + init(rect: CGRect, writingDirection: NSWritingDirection, containsStart: Bool, containsEnd: Bool, isVertical: Bool) { + self.rectValue = rect + self.writingDirectionValue = writingDirection + self.containsStartValue = containsStart + self.containsEndValue = containsEnd + self.isVerticalValue = isVertical + } +} + +private let quoteIcon: UIImage = { + return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed().withRenderingMode(.alwaysTemplate) +}() + +private final class QuoteBackgroundView: UIView { + private let backgroundView: MessageInlineBlockBackgroundView + private let iconView: UIImageView + + private var theme: ChatInputTextView.Theme.Quote? + + override init(frame: CGRect) { + self.backgroundView = MessageInlineBlockBackgroundView() + self.iconView = UIImageView(image: quoteIcon) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize, theme: ChatInputTextView.Theme.Quote) { + if self.theme != theme { + self.theme = theme + + self.iconView.tintColor = theme.foreground + } + + self.iconView.frame = CGRect(origin: CGPoint(x: size.width - 4.0 - quoteIcon.size.width, y: 4.0), size: quoteIcon.size) + + var primaryColor: UIColor + var secondaryColor: UIColor? + var tertiaryColor: UIColor? + switch theme.lineStyle { + case let .solid(color): + primaryColor = color + case let .doubleDashed(mainColor, secondaryColorValue): + primaryColor = mainColor + secondaryColor = secondaryColorValue + case let .tripleDashed(mainColor, secondaryColorValue, tertiaryColorValue): + primaryColor = mainColor + secondaryColor = secondaryColorValue + tertiaryColor = tertiaryColorValue + } + + self.backgroundView.update( + size: size, + isTransparent: false, + primaryColor: primaryColor, + secondaryColor: secondaryColor, + thirdColor: tertiaryColor, + pattern: nil, + animation: .None + ) + self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode/BUILD new file mode 100644 index 00000000000..d3250e2fadc --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInstantVideoMessageDurationNode", + module_name = "ChatInstantVideoMessageDurationNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/MediaPlayer:UniversalMediaPlayer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatInstantVideoMessageDurationNode.swift b/submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode/Sources/ChatInstantVideoMessageDurationNode.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatInstantVideoMessageDurationNode.swift rename to submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode/Sources/ChatInstantVideoMessageDurationNode.swift index 64e726819e8..e75e5296777 100644 --- a/submodules/TelegramUI/Sources/ChatInstantVideoMessageDurationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode/Sources/ChatInstantVideoMessageDurationNode.swift @@ -46,10 +46,10 @@ private final class ChatInstantVideoMessageDurationNodeParameters: NSObject { } } -final class ChatInstantVideoMessageDurationNode: ASImageNode { +public final class ChatInstantVideoMessageDurationNode: ASImageNode { private var textColor: UIColor - var defaultDuration: Double? { + public var defaultDuration: Double? { didSet { if self.defaultDuration != oldValue { self.updateTimestamp() @@ -58,7 +58,7 @@ final class ChatInstantVideoMessageDurationNode: ASImageNode { } } - var isSeen: Bool = false { + public var isSeen: Bool = false { didSet { if self.isSeen != oldValue { self.updateContents() @@ -92,7 +92,7 @@ final class ChatInstantVideoMessageDurationNode: ASImageNode { private var statusDisposable: Disposable? private var statusValuePromise = Promise() - var status: Signal? { + public var status: Signal? { didSet { if let status = self.status { self.statusValuePromise.set(status) @@ -102,10 +102,10 @@ final class ChatInstantVideoMessageDurationNode: ASImageNode { } } - var size: CGSize = CGSize() - var sizeUpdated: ((CGSize) -> Void)? + public var size: CGSize = CGSize() + public var sizeUpdated: ((CGSize) -> Void)? - init(textColor: UIColor) { + public init(textColor: UIColor) { self.textColor = textColor super.init() @@ -127,7 +127,7 @@ final class ChatInstantVideoMessageDurationNode: ASImageNode { self.updateTimer?.invalidate() } - func updateTheme(textColor: UIColor) { + public func updateTheme(textColor: UIColor) { if !self.textColor.isEqual(textColor) { self.textColor = textColor self.updateContents() @@ -149,7 +149,7 @@ final class ChatInstantVideoMessageDurationNode: ASImageNode { self.updateTimer = nil } - func updateTimestamp() { + public func updateTimestamp() { if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { let timestampSeconds: Double if !statusValue.generationTimestamp.isZero { diff --git a/submodules/TelegramUI/Components/Chat/ChatLoadingNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatLoadingNode/BUILD new file mode 100644 index 00000000000..d3a63d832d4 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatLoadingNode/BUILD @@ -0,0 +1,34 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatLoadingNode", + module_name = "ChatLoadingNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/ActivityIndicator", + "//submodules/WallpaperBackgroundNode", + "//submodules/ShimmerEffect", + "//submodules/ChatPresentationInterfaceState", + "//submodules/AccountContext", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatLoadingNode.swift b/submodules/TelegramUI/Components/Chat/ChatLoadingNode/Sources/ChatLoadingNode.swift similarity index 88% rename from submodules/TelegramUI/Sources/ChatLoadingNode.swift rename to submodules/TelegramUI/Components/Chat/ChatLoadingNode/Sources/ChatLoadingNode.swift index b194b51613a..760609e3c5a 100644 --- a/submodules/TelegramUI/Sources/ChatLoadingNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatLoadingNode/Sources/ChatLoadingNode.swift @@ -10,13 +10,20 @@ import WallpaperBackgroundNode import ShimmerEffect import ChatPresentationInterfaceState import AccountContext +import ChatMessageItem +import ChatMessageItemView +import ChatMessageStickerItemNode +import ChatMessageInstantVideoItemNode +import ChatMessageAnimatedStickerItemNode +import ChatMessageBubbleItemNode +import ChatMessageItemImpl -final class ChatLoadingNode: ASDisplayNode { +public final class ChatLoadingNode: ASDisplayNode { private let backgroundNode: NavigationBackgroundNode private let activityIndicator: ActivityIndicator private let offset: CGPoint - init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) { + public init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) { self.backgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: theme, wallpaper: chatWallpaper), enableBlur: context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: theme, wallpaper: chatWallpaper)) let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: chatWallpaper) @@ -33,7 +40,7 @@ final class ChatLoadingNode: ASDisplayNode { self.addSubnode(self.activityIndicator) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) let backgroundSize: CGFloat = 30.0 @@ -44,7 +51,7 @@ final class ChatLoadingNode: ASDisplayNode { transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - activitySize.width) / 2.0) + self.offset.x, y: displayRect.minY + floor((displayRect.height - activitySize.height) / 2.0) + self.offset.y), size: activitySize)) } - var progressFrame: CGRect { + public var progressFrame: CGRect { return self.backgroundNode.frame } } @@ -53,22 +60,22 @@ private let avatarSize = CGSize(width: 38.0, height: 38.0) private let avatarImage = generateFilledCircleImage(diameter: avatarSize.width, color: .white) private let avatarBorderImage = generateCircleImage(diameter: avatarSize.width, lineWidth: 1.0 - UIScreenPixel, color: .white) -final class ChatLoadingPlaceholderMessageContainer { - var avatarNode: ASImageNode? - var avatarBorderNode: ASImageNode? +public final class ChatLoadingPlaceholderMessageContainer { + public var avatarNode: ASImageNode? + public var avatarBorderNode: ASImageNode? - let bubbleNode: ASImageNode - let bubbleBorderNode: ASImageNode + public let bubbleNode: ASImageNode + public let bubbleBorderNode: ASImageNode - var parentView: UIView? { + public var parentView: UIView? { return self.bubbleNode.supernode?.view } - var frame: CGRect { + public var frame: CGRect { return self.bubbleNode.frame } - init(bubbleImage: UIImage?, bubbleBorderImage: UIImage?) { + public init(bubbleImage: UIImage?, bubbleBorderImage: UIImage?) { self.bubbleNode = ASImageNode() self.bubbleNode.displaysAsynchronously = false self.bubbleNode.image = bubbleImage @@ -78,12 +85,12 @@ final class ChatLoadingPlaceholderMessageContainer { self.bubbleBorderNode.image = bubbleBorderImage } - func setup(maskNode: ASDisplayNode, borderMaskNode: ASDisplayNode) { + public func setup(maskNode: ASDisplayNode, borderMaskNode: ASDisplayNode) { maskNode.addSubnode(self.bubbleNode) borderMaskNode.addSubnode(self.bubbleBorderNode) } - func animateWith(_ listItemNode: ListViewItemNode, delay: Double, transition: ContainedViewLayoutTransition) { + public func animateWith(_ listItemNode: ListViewItemNode, delay: Double, transition: ContainedViewLayoutTransition) { listItemNode.allowsGroupOpacity = true listItemNode.alpha = 1.0 listItemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay, completion: { _ in @@ -91,17 +98,17 @@ final class ChatLoadingPlaceholderMessageContainer { }) if let bubbleItemNode = listItemNode as? ChatMessageBubbleItemNode { - bubbleItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) + bubbleItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition) } else if let stickerItemNode = listItemNode as? ChatMessageStickerItemNode { - stickerItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) + stickerItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition) } else if let stickerItemNode = listItemNode as? ChatMessageAnimatedStickerItemNode { - stickerItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) + stickerItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition) } else if let videoItemNode = listItemNode as? ChatMessageInstantVideoItemNode { - videoItemNode.animateFromLoadingPlaceholder(messageContainer: self, delay: delay, transition: transition) + videoItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition) } } - func update(size: CGSize, hasAvatar: Bool, rect: CGRect, transition: ContainedViewLayoutTransition) { + public func update(size: CGSize, hasAvatar: Bool, rect: CGRect, transition: ContainedViewLayoutTransition) { var avatarOffset: CGFloat = 0.0 if hasAvatar && self.avatarNode == nil { @@ -133,7 +140,7 @@ final class ChatLoadingPlaceholderMessageContainer { } } -final class ChatLoadingPlaceholderNode: ASDisplayNode { +public final class ChatLoadingPlaceholderNode: ASDisplayNode { private weak var backgroundNode: WallpaperBackgroundNode? private let context: AccountContext @@ -155,7 +162,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { private var validLayout: (CGSize, UIEdgeInsets, LayoutMetrics)? - init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode) { + public init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode) { self.context = context self.backgroundNode = backgroundNode @@ -201,7 +208,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { } } - override func didLoad() { + override public func didLoad() { super.didLoad() self.containerNode.view.mask = self.maskNode.view @@ -217,10 +224,8 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { } private var bottomInset: (Int, CGFloat)? - func setup(_ historyNode: ChatHistoryNode, updating: Bool = false) { - guard let listNode = historyNode as? ListView else { - return - } + public func setup(_ historyNode: ListView, updating: Bool = false) { + let listNode = historyNode var listItemNodes: [ASDisplayNode] = [] var count = 0 @@ -285,10 +290,11 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { } private var didAnimateOut = false - func animateOut(_ historyNode: ChatHistoryNode, completion: @escaping () -> Void = {}) { - guard let listNode = historyNode as? ListView, let (size, _, _) = self.validLayout else { + public func animateOut(_ historyNode: ListView, completion: @escaping () -> Void = {}) { + guard let (size, _, _) = self.validLayout else { return } + let listNode = historyNode self.didAnimateOut = true self.backgroundNode?.updateIsLooping(false) @@ -372,7 +378,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { } } - func addContentOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + public func addContentOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { self.maskNode.bounds = self.maskNode.bounds.offsetBy(dx: 0.0, dy: -offset) self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: -offset) transition.animateOffsetAdditive(node: self.maskNode, offset: offset) @@ -382,7 +388,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { } } - func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) { + public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) { self.absolutePosition = (rect, containerSize) if let backgroundContent = self.backgroundContent { var backgroundFrame = backgroundContent.frame @@ -392,14 +398,14 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { } } - enum ChatType: Equatable { + public enum ChatType: Equatable { case generic case user case group case channel } private var chatType: ChatType = .channel - func updatePresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) { + public func updatePresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) { var chatType: ChatType = .channel if let peer = chatPresentationInterfaceState.renderedPeer?.peer { if peer is TelegramUser { @@ -423,7 +429,7 @@ final class ChatLoadingPlaceholderNode: ASDisplayNode { } } - func updateLayout(size: CGSize, insets: UIEdgeInsets, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, insets: UIEdgeInsets, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition) { self.validLayout = (size, insets, metrics) let bounds = CGRect(origin: .zero, size: size) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/BUILD new file mode 100644 index 00000000000..e487f7b72c7 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageActionBubbleContentNode", + module_name = "ChatMessageActionBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/LocalizedPeerData", + "//submodules/UrlEscaping", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramUniversalVideoContent", + "//submodules/GalleryUI", + "//submodules/WallpaperBackgroundNode", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift similarity index 91% rename from submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index 19313d76968..3d49bde8e36 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -19,18 +19,20 @@ import GalleryUI import WallpaperBackgroundNode import InvisibleInkDustNode import TextNodeWithEntities +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview) } -class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { - let labelNode: TextNodeWithEntities +public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { + public let labelNode: TextNodeWithEntities private var dustNode: InvisibleInkDustNode? - var backgroundNode: WallpaperBubbleBackgroundNode? - var backgroundColorNode: ASDisplayNode - let backgroundMaskNode: ASImageNode - var linkHighlightingNode: LinkHighlightingNode? + public var backgroundNode: WallpaperBubbleBackgroundNode? + public var backgroundColorNode: ASDisplayNode + public let backgroundMaskNode: ASImageNode + public var linkHighlightingNode: LinkHighlightingNode? private let mediaBackgroundNode: ASImageNode fileprivate var imageNode: TransformImageNode? @@ -44,7 +46,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])? private var absoluteRect: (CGRect, CGSize)? - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { if oldValue != self.visibility { switch self.visibility { @@ -62,7 +64,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } - required init() { + required public init() { self.labelNode = TextNodeWithEntities() self.labelNode.textNode.isUserInteractionEnabled = false self.labelNode.textNode.displaysAsynchronously = false @@ -79,7 +81,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.labelNode.textNode) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -87,11 +89,11 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { self.fetchDisposable.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let imageNode = self.imageNode, self.item?.message.id == messageId { return (imageNode, imageNode.bounds, { [weak self] in guard let strongSelf = self, let imageNode = strongSelf.imageNode else { @@ -120,7 +122,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false var currentMedia: Media? if let item = item { @@ -150,7 +152,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { return mediaHidden } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode) let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage @@ -373,12 +375,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.insertSubnode(backgroundNode, at: 0) } } - - if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { - strongSelf.backgroundColorNode.isHidden = true - } else { - strongSelf.backgroundColorNode.isHidden = true - } + strongSelf.backgroundColorNode.isHidden = true } else { if strongSelf.backgroundMaskNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundMaskNode, at: 0) @@ -436,7 +433,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if let backgroundNode = self.backgroundNode { @@ -447,19 +444,19 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } - override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } - override func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { if let backgroundNode = self.backgroundNode { backgroundNode.offsetSpring(value: value, duration: duration, damping: damping) } } - override func updateTouchesAtPoint(_ point: CGPoint?) { + override public func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [(CGRect, CGRect)]? let textNodeFrame = self.labelNode.textNode.frame @@ -512,7 +509,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.labelNode.textNode.frame if let (index, attributes) = self.labelNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -520,29 +517,29 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { if let (attributeText, fullText) = self.labelNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true) + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .textMention(peerName) + return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return .botCommand(botCommand) + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) } } if let imageNode = imageNode, imageNode.frame.contains(point) { - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { if let item = self.item, item.message.media.contains(where: { $0 is TelegramMediaStory }) { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } else { - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD new file mode 100644 index 00000000000..43f3ea0a24b --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageActionButtonsNode", + module_name = "ChatMessageActionButtonsNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/WallpaperBackgroundNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift similarity index 88% rename from submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index bbbda3cf3b8..08017ba7739 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -10,6 +10,56 @@ import WallpaperBackgroundNode private let titleFont = Font.medium(16.0) +private extension UIBezierPath { + convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) { + self.init() + + let path = CGMutablePath() + + let topLeft = rect.origin + let topRight = CGPoint(x: rect.maxX, y: rect.minY) + let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY) + let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY) + + if topLeftRadius != .zero { + path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.move(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + if topRightRadius != .zero { + path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y)) + path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius)) + } else { + path.addLine(to: CGPoint(x: topRight.x, y: topRight.y)) + } + + if bottomRightRadius != .zero { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius)) + path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y)) + } else { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y)) + } + + if bottomLeftRadius != .zero { + path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y)) + path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius)) + } else { + path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y)) + } + + if topLeftRadius != .zero { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius)) + path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + path.closeSubpath() + self.cgPath = path + } +} + private final class ChatMessageActionButtonNode: ASDisplayNode { //private let backgroundBlurNode: NavigationBackgroundNode private var backgroundBlurView: PortalView? @@ -293,7 +343,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size) titleNode.layer.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - animation.animator.updatePosition(layer: titleNode.layer, position: titleFrame.center, completion: nil) + animation.animator.updatePosition(layer: titleNode.layer, position: CGPoint(x: titleFrame.midX, y: titleFrame.midY), completion: nil) if let buttonView = node.buttonView { buttonView.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)) @@ -316,17 +366,17 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } -final class ChatMessageActionButtonsNode: ASDisplayNode { +public final class ChatMessageActionButtonsNode: ASDisplayNode { private var buttonNodes: [ChatMessageActionButtonNode] = [] private var buttonPressedWrapper: ((ReplyMarkupButton) -> Void)? private var buttonLongTappedWrapper: ((ReplyMarkupButton) -> Void)? - var buttonPressed: ((ReplyMarkupButton) -> Void)? - var buttonLongTapped: ((ReplyMarkupButton) -> Void)? + public var buttonPressed: ((ReplyMarkupButton) -> Void)? + public var buttonLongTapped: ((ReplyMarkupButton) -> Void)? private var absolutePosition: (CGRect, CGSize)? - override init() { + override public init() { super.init() self.buttonPressedWrapper = { [weak self] button in @@ -342,7 +392,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } } - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absolutePosition = (rect, containerSize) for button in buttonNodes { @@ -353,7 +403,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) { + public class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) { let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] return { context, theme, chatBubbleCorners, strings, backgroundNode, replyMarkup, message, constrainedWidth in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD new file mode 100644 index 00000000000..4d008815291 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD @@ -0,0 +1,60 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageAnimatedStickerItemNode", + module_name = "ChatMessageAnimatedStickerItemNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/MediaResources", + "//submodules/StickerResources", + "//submodules/ContextUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/Emoji", + "//submodules/Markdown", + "//submodules/ManagedAnimationNode", + "//submodules/SlotMachineAnimationNode", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/ShimmerEffect", + "//submodules/WallpaperBackgroundNode", + "//submodules/LocalMediaResources", + "//submodules/AppBundle", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageShareButton", + "//submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode", + "//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer", + "//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode", + "//submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode", + "//submodules/TelegramUI/Components/Chat/MessageHaptics", + "//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift similarity index 87% rename from submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index cea9f42e6fb..f75ac88511e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -29,12 +29,29 @@ import ChatPresentationInterfaceState import TextNodeWithEntities import ChatControllerInteraction import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageBubbleContentNode +import ChatMessageReplyInfoNode +import ChatMessageItem +import ChatMessageItemView +import ChatMessageSwipeToReplyNode +import ChatMessageSelectionNode +import ChatMessageDeliveryFailedNode +import ChatMessageShareButton +import ChatMessageThreadInfoNode +import ChatMessageActionButtonsNode +import ChatSwipeToReplyRecognizer +import ChatMessageReactionsFooterContentNode +import ManagedDiceAnimationNode +import MessageHaptics +import ChatMessageTransitionNode private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont -protocol GenericAnimatedStickerNode: ASDisplayNode { +public protocol GenericAnimatedStickerNode: ASDisplayNode { func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) var currentFrameIndex: Int { get } @@ -42,201 +59,32 @@ protocol GenericAnimatedStickerNode: ASDisplayNode { } extension DefaultAnimatedStickerNodeImpl: GenericAnimatedStickerNode { - func setFrameIndex(_ frameIndex: Int) { + public func setFrameIndex(_ frameIndex: Int) { self.stop() self.play(fromIndex: frameIndex) } } extension SlotMachineAnimationNode: GenericAnimatedStickerNode { - var currentFrameIndex: Int { + public var currentFrameIndex: Int { return 0 } - func setFrameIndex(_ frameIndex: Int) { + public func setFrameIndex(_ frameIndex: Int) { } } -class ChatMessageShareButton: HighlightableButtonNode { - private var backgroundContent: WallpaperBubbleBackgroundNode? - //private let backgroundNode: NavigationBackgroundNode - private var backgroundBlurView: PortalView? - - private let iconNode: ASImageNode - private var iconOffset = CGPoint() - - private var theme: PresentationTheme? - private var isReplies: Bool = false - - private var textNode: ImmediateTextNode? - - private var absolutePosition: (CGRect, CGSize)? - - init() { - //self.backgroundNode = NavigationBackgroundNode(color: .clear) - self.iconNode = ASImageNode() - - super.init(pointerStyle: nil) - - self.allowsGroupOpacity = true - - //self.addSubnode(self.backgroundNode) - self.addSubnode(self.iconNode) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Nicegram (translateButton) - func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false, translateButton: Bool = false) -> CGSize { - var isReplies = false - var replyCount = 0 - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { - for attribute in message.attributes { - if let attribute = attribute as? ReplyThreadMessageAttribute { - replyCount = Int(attribute.count) - isReplies = true - break - } - } - } - if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.effectiveTopId == message.id { - replyCount = 0 - isReplies = false - } - if disableComments { - replyCount = 0 - isReplies = false - } - - if self.theme !== presentationData.theme.theme || self.isReplies != isReplies { - self.theme = presentationData.theme.theme - self.isReplies = isReplies - - var updatedIconImage: UIImage? - var updatedIconOffset = CGPoint() - // MARK: Nicegram (if translateButton) - if translateButton { - updatedIconImage = PresentationResourcesChat.chatTranslateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - } else if case .pinnedMessages = subject { - updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - updatedIconOffset = CGPoint(x: UIScreenPixel, y: 1.0) - } else if isReplies { - updatedIconImage = PresentationResourcesChat.chatFreeCommentButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - } else if message.id.peerId.isRepliesOrSavedMessages(accountPeerId: account.peerId) { - updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - updatedIconOffset = CGPoint(x: UIScreenPixel, y: 1.0) - } else { - updatedIconImage = PresentationResourcesChat.chatFreeShareButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - } - //self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) - self.iconNode.image = updatedIconImage - self.iconOffset = updatedIconOffset - } - var size = CGSize(width: 30.0, height: 30.0) - var offsetIcon = false - if isReplies, replyCount > 0 { - offsetIcon = true - - let textNode: ImmediateTextNode - if let current = self.textNode { - textNode = current - } else { - textNode = ImmediateTextNode() - self.textNode = textNode - self.addSubnode(textNode) - } - - let textColor = bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: presentationData.theme.wallpaper) - - let countString: String - if replyCount >= 1000 * 1000 { - countString = "\(replyCount / 1000_000)M" - } else if replyCount >= 1000 { - countString = "\(replyCount / 1000)K" - } else { - countString = "\(replyCount)" - } - - textNode.attributedText = NSAttributedString(string: countString, font: Font.regular(11.0), textColor: textColor) - let textSize = textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) - size.height += textSize.height - 1.0 - textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: size.height - textSize.height - 4.0), size: textSize) - } else if let textNode = self.textNode { - self.textNode = nil - textNode.removeFromSupernode() - } - - if self.backgroundBlurView == nil { - if let backgroundBlurView = controllerInteraction.presentationContext.backgroundNode?.makeFreeBackground() { - self.backgroundBlurView = backgroundBlurView - self.view.insertSubview(backgroundBlurView.view, at: 0) - - backgroundBlurView.view.clipsToBounds = true - } - } - if let backgroundBlurView = self.backgroundBlurView { - backgroundBlurView.view.frame = CGRect(origin: CGPoint(), size: size) - backgroundBlurView.view.layer.cornerRadius = min(size.width, size.height) / 2.0 - } - - //self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) - //self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(self.backgroundNode.bounds.width, self.backgroundNode.bounds.height) / 2.0, transition: .immediate) - if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0) + self.iconOffset.x, y: floor((size.width - image.size.width) / 2.0) - (offsetIcon ? 1.0 : 0.0) + self.iconOffset.y), size: image.size) - } - - - if controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { - if self.backgroundContent == nil, let backgroundContent = controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { - backgroundContent.clipsToBounds = true - self.backgroundContent = backgroundContent - self.insertSubnode(backgroundContent, at: 0) - } - } else { - self.backgroundContent?.removeFromSupernode() - self.backgroundContent = nil - } - - if let backgroundContent = self.backgroundContent { - //self.backgroundNode.isHidden = true - self.backgroundBlurView?.view.isHidden = true - backgroundContent.cornerRadius = min(size.width, size.height) / 2.0 - backgroundContent.frame = CGRect(origin: CGPoint(), size: size) - if let (rect, containerSize) = self.absolutePosition { - var backgroundFrame = backgroundContent.frame - backgroundFrame.origin.x += rect.minX - backgroundFrame.origin.y += rect.minY - backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) - } - } else { - //self.backgroundNode.isHidden = false - self.backgroundBlurView?.view.isHidden = false - } - - return size - } - - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - self.absolutePosition = (rect, containerSize) - if let backgroundContent = self.backgroundContent { - var backgroundFrame = backgroundContent.frame - backgroundFrame.origin.x += rect.minX - backgroundFrame.origin.y += rect.minY - backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) - } - } +extension ManagedDiceAnimationNode: GenericAnimatedStickerNode { } -class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { - let contextSourceNode: ContextExtractedContentContainingNode +public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { + public let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode - let imageNode: TransformImageNode + public let imageNode: TransformImageNode private var enableSynchronousImageApply: Bool = false private var backgroundNode: WallpaperBubbleBackgroundNode? - private(set) var placeholderNode: StickerShimmerEffectNode - private(set) var animationNode: GenericAnimatedStickerNode? + public private(set) var placeholderNode: StickerShimmerEffectNode + public private(set) var animationNode: GenericAnimatedStickerNode? private var animationSize: CGSize? private var didSetUpAnimationNode = false private var isPlaying = false @@ -244,7 +92,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private let textNode: TextNodeWithEntities private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = [] - private var overlayMeshAnimationNode: ChatMessageTransitionNode.DecorationItemNode? private var enqueuedAdditionalAnimations: [(Int, Double)] = [] private var additionalAnimationsCommitTimer: SwiftSignalKit.Timer? @@ -255,10 +102,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var deliveryFailedNode: ChatMessageDeliveryFailedNode? private var shareButtonNode: ChatMessageShareButton? - var telegramFile: TelegramMediaFile? - var emojiFile: TelegramMediaFile? - var telegramDice: TelegramMediaDice? - var emojiString: String? + public var telegramFile: TelegramMediaFile? + public var emojiFile: TelegramMediaFile? + public var telegramDice: TelegramMediaDice? + public var emojiString: String? private let disposable = MetaDisposable() private let disposables = DisposableSet() @@ -267,8 +114,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var threadInfoNode: ChatMessageThreadInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundContent: WallpaperBubbleBackgroundNode? - private var replyBackgroundNode: NavigationBackgroundNode? private var forwardInfoNode: ChatMessageForwardInfoNode? + private var forwardBackgroundContent: WallpaperBubbleBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? private var reactionButtonsNode: ChatMessageReactionButtonsNode? @@ -288,12 +135,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var appliedForwardInfo: (Peer?, String?)? + private var replyRecognizer: ChatSwipeToReplyRecognizer? private var currentSwipeAction: ChatControllerInteractionSwipeAction? private var wasPending: Bool = false private var didChangeFromPendingToSent: Bool = false - required init() { + required public init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() @@ -342,8 +190,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { switch action { case .action, .optionalAction: break - case let .openContextMenu(tapMessage, selectAll, subFrame): - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, strongSelf, subFrame, gesture, nil) + case let .openContextMenu(openContextMenu): + item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, strongSelf, openContextMenu.subFrame, gesture, nil) } } } @@ -390,7 +238,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.additionalAnimationsCommitTimer?.invalidate() } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -405,7 +253,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func didLoad() { + override public func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) @@ -441,7 +289,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let action = strongSelf.gestureRecognized(gesture: .longTap, location: point, recognizer: recognizer) { switch action { case let .action(f): - f() + f.action() recognizer.cancel() case let .optionalAction(f): f() @@ -454,6 +302,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.view.addGestureRecognizer(recognizer) let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) + if let item = self.item { + let _ = item + replyRecognizer.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + self.view.disablesInteractiveTransitionGestureRecognizer = false + } replyRecognizer.shouldBegin = { [weak self] in if let strongSelf = self, let item = strongSelf.item { if strongSelf.selectionNode != nil { @@ -474,10 +327,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } return false } + self.replyRecognizer = replyRecognizer self.view.addGestureRecognizer(replyRecognizer) } - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = oldValue != .none let isVisible = self.visibility != .none @@ -514,6 +368,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var setupTimestamp: Double? private func setupNode(item: ChatMessageItem) { + self.replyRecognizer?.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + if self.isNodeLoaded { + self.view.disablesInteractiveTransitionGestureRecognizer = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + } + guard self.animationNode == nil else { return } @@ -572,7 +431,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { + override public func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { super.setupItem(item, synchronousLoad: synchronousLoad) if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal { @@ -734,13 +593,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations if !isPlaying { self.removeAdditionalAnimations() - - if let overlayMeshAnimationNode = self.overlayMeshAnimationNode { - self.overlayMeshAnimationNode = nil - if let transitionNode = item.controllerInteraction.getMessageTransitionNode() as? ChatMessageTransitionNode { - transitionNode.remove(decorationNode: overlayMeshAnimationNode) - } - } } if let animationNode = self.animationNode as? AnimatedStickerNode { if self.isPlaying != isPlaying || (isPlaying && !self.didSetUpAnimationNode) { @@ -810,13 +662,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func updateStickerSettings(forceStopAnimations: Bool) { + override public func updateStickerSettings(forceStopAnimations: Bool) { self.forceStopAnimations = forceStopAnimations self.updateVisibility() } private var absoluteRect: (CGRect, CGSize)? - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if !self.contextSourceNode.isExtractedToContextPreview { var rect = rect @@ -870,7 +722,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } @@ -880,7 +732,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { super.updateAccessibilityData(accessibilityData) self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label @@ -911,7 +763,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { var displaySize = CGSize(width: 180.0, height: 180.0) let telegramFile = self.telegramFile let emojiFile = self.emojiFile @@ -1054,7 +906,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject { needsShareButton = false } @@ -1108,7 +960,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let font = Font.regular(fontSizeForEmojiString(item.message.text)) let textColor = item.presentationData.theme.theme.list.itemPrimaryTextColor - let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: textColor, linkColor: textColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message) + let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: textColor, linkColor: textColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message, adjustQuoteFontSize: true) textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural)) imageSize = CGSize(width: textLayoutAndApply!.0.size.width, height: textLayoutAndApply!.0.size.height) @@ -1205,11 +1057,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var viaBotApply: (TextNodeLayout, () -> TextNode)? var threadInfoApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? - var needsReplyBackground = false + var replyInfoApply: (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode)? var replyMarkup: ReplyMarkupMessageAttribute? - let availableContentWidth = min(120.0, max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)) + var availableContentWidth = min(200.0, max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)) + availableContentWidth -= 20.0 var ignoreForward = false if let forwardInfo = item.message.forwardInfo { @@ -1226,6 +1078,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } var replyMessage: Message? + var replyForward: QuotedReplyMessageAttribute? + var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -1252,6 +1106,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } + } else if let quoteReplyAttribute = attribute as? QuotedReplyMessageAttribute { + replyForward = quoteReplyAttribute } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { @@ -1259,7 +1116,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - var hasReply = replyMessage != nil || replyStory != nil + var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil if case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId { hasReply = false @@ -1279,13 +1136,15 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { )) } - if hasReply, (replyMessage != nil || replyStory != nil) { + if hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, strings: item.presentationData.strings, context: item.context, type: .standalone, message: replyMessage, + replyForward: replyForward, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude), @@ -1353,13 +1212,19 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } let availableWidth = max(60.0, availableContentWidth + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) } - if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil { + var needsReplyBackground = false + if replyInfoApply != nil { needsReplyBackground = true } + var needsForwardBackground = false + if viaBotApply != nil || forwardInfoSizeApply != nil { + needsForwardBackground = true + } + var maxContentWidth = imageSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { @@ -1412,6 +1277,53 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height } + var headersOffset: CGFloat = 0.0 + if let (threadInfoSize, _) = threadInfoApply { + headersOffset += threadInfoSize.height + 10.0 + } + + var viaBotFrame: CGRect? + if let (viaBotLayout, _) = viaBotApply { + viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 15.0) : (params.width - params.rightInset - viaBotLayout.size.width - layoutConstants.bubble.edgeInset - 14.0)), y: headersOffset + 8.0), size: viaBotLayout.size) + } + + var replyInfoFrame: CGRect? + if let (replyInfoSize, _) = replyInfoApply { + var viaBotSize = CGSize() + if let viaBotFrame = viaBotFrame { + viaBotSize = viaBotFrame.size + } + let replyInfoFrameValue = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - max(replyInfoSize.width, viaBotSize.width) - layoutConstants.bubble.edgeInset - 10.0)), y: headersOffset + 8.0 + viaBotSize.height), size: replyInfoSize) + replyInfoFrame = replyInfoFrameValue + if let viaBotFrameValue = viaBotFrame { + if replyInfoFrameValue.minX < replyInfoFrameValue.minX { + viaBotFrame = viaBotFrameValue.offsetBy(dx: replyInfoFrameValue.minX - viaBotFrameValue.minX, dy: 0.0) + } + } + } + + var replyBackgroundFrame: CGRect? + if let replyInfoFrame = replyInfoFrame { + var viaBotSize = CGSize() + if let viaBotFrame = viaBotFrame { + viaBotSize = viaBotFrame.size + } + + replyBackgroundFrame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: headersOffset + replyInfoFrame.minY - viaBotSize.height - 2.0), size: CGSize(width: max(replyInfoFrame.size.width, viaBotSize.width) + 8.0, height: replyInfoFrame.size.height + viaBotSize.height + 5.0)) + } + let _ = replyBackgroundFrame + + /*if let replyBackgroundFrameValue = replyBackgroundFrame { + if replyBackgroundFrameValue.insetBy(dx: -2.0, dy: -2.0).intersects(baseShareButtonFrame) { + let offset: CGFloat = 25.0 + + layoutSize.height += offset + updatedImageFrame.origin.y += offset + dateAndStatusFrame.origin.y += offset + baseShareButtonFrame.origin.y += offset + } + }*/ + func finishLayout(_ animation: ListViewItemUpdateAnimation, _ apply: ListViewItemApply, _ synchronousLoads: Bool) { if let strongSelf = weakSelf.value { strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) @@ -1424,7 +1336,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var transition: ContainedViewLayoutTransition = .immediate if case let .System(duration, _) = animation { - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject { transition = .animated(duration: duration, curve: .linear) } else { transition = .animated(duration: duration, curve: .spring) @@ -1531,34 +1443,31 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { dateAndStatusApply(animation) if needsReplyBackground { - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) - } else { - let replyBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)) - strongSelf.replyBackgroundNode = replyBackgroundNode - strongSelf.contextSourceNode.contentNode.addSubnode(replyBackgroundNode) - } - - if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { - if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { - backgroundContent.clipsToBounds = true - strongSelf.replyBackgroundContent = backgroundContent - strongSelf.insertSubnode(backgroundContent, at: 0) - } - } else { - strongSelf.replyBackgroundContent?.removeFromSupernode() - strongSelf.replyBackgroundContent = nil + if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.replyBackgroundContent = backgroundContent + strongSelf.contextSourceNode.contentNode.insertSubnode(backgroundContent, at: 0) } - } else if let replyBackgroundNode = strongSelf.replyBackgroundNode { - strongSelf.replyBackgroundNode = nil - replyBackgroundNode.removeFromSupernode() - + } else { if let replyBackgroundContent = strongSelf.replyBackgroundContent { replyBackgroundContent.removeFromSupernode() strongSelf.replyBackgroundContent = nil } } + if needsForwardBackground { + if strongSelf.forwardBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.forwardBackgroundContent = backgroundContent + strongSelf.contextSourceNode.contentNode.insertSubnode(backgroundContent, at: 0) + } + } else { + if let forwardBackgroundContent = strongSelf.forwardBackgroundContent { + forwardBackgroundContent.removeFromSupernode() + strongSelf.forwardBackgroundContent = nil + } + } + var headersOffset: CGFloat = 0.0 if let (threadInfoSize, threadInfoApply) = threadInfoApply { let threadInfoNode = threadInfoApply(synchronousLoads) @@ -1586,16 +1495,24 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: 0.0) } + var forwardAreaFrame: CGRect? if let (viaBotLayout, viaBotApply) = viaBotApply, forwardInfoSizeApply == nil { let viaBotNode = viaBotApply() if strongSelf.viaBotNode == nil { strongSelf.viaBotNode = viaBotNode strongSelf.contextSourceNode.contentNode.addSubnode(viaBotNode) } - let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0), size: viaBotLayout.size) + let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0 + 5.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0 - 5.0)), y: headersOffset + 8.0), size: viaBotLayout.size) + viaBotNode.frame = viaBotFrame messageInfoSize = CGSize(width: messageInfoSize.width, height: viaBotLayout.size.height) + + if let forwardAreaFrameValue = forwardAreaFrame { + forwardAreaFrame = forwardAreaFrameValue.union(viaBotFrame) + } else { + forwardAreaFrame = viaBotFrame + } } else if let viaBotNode = strongSelf.viaBotNode { viaBotNode.removeFromSupernode() strongSelf.viaBotNode = nil @@ -1611,10 +1528,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { forwardInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize) + let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0 + 5.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0 - 5.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize) forwardInfoNode.frame = forwardInfoFrame - messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height - 1.0) + messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height + 8.0) + + if let forwardAreaFrameValue = forwardAreaFrame { + forwardAreaFrame = forwardAreaFrameValue.union(forwardInfoFrame) + } else { + forwardAreaFrame = forwardInfoFrame + } } else if let forwardInfoNode = strongSelf.forwardInfoNode { if animation.isAnimated { if let forwardInfoNode = strongSelf.forwardInfoNode { @@ -1629,13 +1552,25 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + var forwardBackgroundFrame: CGRect? + if let forwardAreaFrame { + forwardBackgroundFrame = forwardAreaFrame.insetBy(dx: -6.0, dy: -3.0) + } + + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(synchronousLoads) + if headersOffset != 0.0 { + headersOffset += 6.0 + } + + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - replyInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) + replyBackgroundFrame = replyInfoFrame + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads, animation) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) @@ -1644,26 +1579,25 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode = nil } - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.frame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)) - 4.0, y: headersOffset + 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) - - let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 - replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) - - if let backgroundContent = strongSelf.replyBackgroundContent { - let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 - - replyBackgroundNode.isHidden = true - backgroundContent.cornerRadius = cornerRadius - backgroundContent.frame = replyBackgroundNode.frame - if let (rect, containerSize) = strongSelf.absoluteRect { - var backgroundFrame = backgroundContent.frame - backgroundFrame.origin.x += rect.minX - backgroundFrame.origin.y += rect.minY - backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) - } - } else { - replyBackgroundNode.isHidden = false + if let backgroundContent = strongSelf.replyBackgroundContent, let replyBackgroundFrame { + backgroundContent.cornerRadius = 4.0 + backgroundContent.frame = replyBackgroundFrame + if let (rect, containerSize) = strongSelf.absoluteRect { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + if let backgroundContent = strongSelf.forwardBackgroundContent, let forwardBackgroundFrame { + backgroundContent.cornerRadius = 4.0 + backgroundContent.frame = forwardBackgroundFrame + if let (rect, containerSize) = strongSelf.absoluteRect { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } } @@ -1672,7 +1606,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode?.alpha = panelsAlpha strongSelf.viaBotNode?.alpha = panelsAlpha strongSelf.forwardInfoNode?.alpha = panelsAlpha - strongSelf.replyBackgroundNode?.alpha = panelsAlpha + strongSelf.replyBackgroundContent?.alpha = panelsAlpha + strongSelf.forwardBackgroundContent?.alpha = panelsAlpha if isFailed { let deliveryFailedNode: ChatMessageDeliveryFailedNode @@ -1836,14 +1771,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } switch action { case let .action(f): - f() + f.action() case let .optionalAction(f): f() - case let .openContextMenu(tapMessage, selectAll, subFrame): + case let .openContextMenu(openContextMenu): if canAddMessageReactions(message: item.message) { item.controllerInteraction.updateMessageReaction(item.message, .default) } else { - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil, nil) + item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil) } } } else if case .tap = gesture { @@ -1900,7 +1835,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { item.controllerInteraction.commitEmojiInteraction(item.message.id, item.message.text.strippedEmoji, EmojiInteraction(animations: animations), file) } - func playEmojiInteraction(_ interaction: EmojiInteraction) { + public func playEmojiInteraction(_ interaction: EmojiInteraction) { guard interaction.animations.count <= 7 else { return } @@ -1946,7 +1881,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - func playAdditionalEmojiAnimation(index: Int) { + public func playAdditionalEmojiAnimation(index: Int) { guard let item = self.item else { return } @@ -1985,7 +1920,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.playEffectAnimation(resource: effect.resource, isStickerEffect: true) } - func playEffectAnimation(resource: MediaResource, isStickerEffect: Bool = false) { + public func playEffectAnimation(resource: MediaResource, isStickerEffect: Bool = false) { guard let item = self.item else { return } @@ -2108,12 +2043,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) }) } else if let attribute = attribute as? ReplyStoryAttribute { return .optionalAction({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .action(InternalBubbleTapAction.Action { + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }) } } } @@ -2131,7 +2070,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -2140,7 +2079,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if forwardInfoNode.hasAction(at: self.view.convert(location, to: forwardInfoNode.view)) { - return .action({}) + return .action(InternalBubbleTapAction.Action {}) } else { return .optionalAction(performAction) } @@ -2381,7 +2320,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return nil case .longTap, .doubleTap, .secondaryTap: if let item = self.item, self.imageNode.frame.contains(location) { - return .openContextMenu(tapMessage: item.message, selectAll: false, subFrame: self.imageNode.frame) + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.message, selectAll: false, subFrame: self.imageNode.frame)) } case .hold: break @@ -2410,7 +2349,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } @@ -2421,13 +2360,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } private var playedSwipeToReplyHaptic = false - @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 + var leftOffset: CGFloat = 0.0 var swipeOffset: CGFloat = 45.0 if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { offset = -24.0 + leftOffset = -10.0 } else { offset = 10.0 + leftOffset = -10.0 swipeOffset = 60.0 } @@ -2455,7 +2397,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if translation.x < 0.0 { translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) } else { - translation.x = 0.0 + if recognizer.allowBothDirections { + translation.x = -max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) + } else { + translation.x = 0.0 + } } if let item = self.item, self.swipeToReplyNode == nil { @@ -2473,7 +2419,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let swipeToReplyNode = self.swipeToReplyNode { swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) - swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + if translation.x < 0.0 { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + } else { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0) + } if let (rect, containerSize) = self.absoluteRect { let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) @@ -2492,7 +2444,15 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.swipeToReplyFeedback = nil let translation = recognizer.translation(in: self.view) - if case .ended = recognizer.state, translation.x < -swipeOffset { + + let gestureRecognized: Bool + if recognizer.allowBothDirections { + gestureRecognized = abs(translation.x) > swipeOffset + } else { + gestureRecognized = translation.x < -swipeOffset + } + + if case .ended = recognizer.state, gestureRecognized { if let item = self.item { if let currentSwipeAction = currentSwipeAction { switch currentSwipeAction { @@ -2524,7 +2484,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } @@ -2539,7 +2499,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return super.hitTest(point, with: event) } - override func updateSelectionState(animated: Bool) { + override public func updateSelectionState(animated: Bool) { guard let item = self.item else { return } @@ -2559,8 +2519,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let forwardInfoNode = self.forwardInfoNode { transition.updateAlpha(node: forwardInfoNode, alpha: panelsAlpha) } - if let replyBackgroundNode = self.replyBackgroundNode { - transition.updateAlpha(node: replyBackgroundNode, alpha: panelsAlpha) + if let replyBackgroundContent = self.replyBackgroundContent { + transition.updateAlpha(node: replyBackgroundContent, alpha: panelsAlpha) + } + if let forwardBackgroundContent = self.forwardBackgroundContent { + transition.updateAlpha(node: forwardBackgroundContent, alpha: panelsAlpha) } if let selectionState = item.controllerInteraction.selectionState { @@ -2621,7 +2584,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func updateHighlightedState(animated: Bool) { + override public func updateHighlightedState(animated: Bool) { super.updateHighlightedState(animated: animated) if let item = self.item { @@ -2646,11 +2609,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func cancelInsertionAnimations() { + override public func cancelInsertionAnimations() { self.layer.removeAllAnimations() } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -2668,27 +2631,41 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { + override public func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { return self.contextSourceNode } - override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + override public func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } + + public final class AnimationTransitionTextInput { + public let backgroundView: UIView + public let contentView: UIView + public let sourceRect: CGRect + public let scrollOffset: CGFloat - func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { + public init(backgroundView: UIView, contentView: UIView, sourceRect: CGRect, scrollOffset: CGFloat) { + self.backgroundView = backgroundView + self.contentView = contentView + self.sourceRect = sourceRect + self.scrollOffset = scrollOffset + } + } + + public func animateContentFromTextInputField(textInput: AnimationTransitionTextInput, transition: CombinedTransition) { guard let _ = self.item else { return } @@ -2747,8 +2724,56 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) } + + public final class AnimationTransitionSticker { + public let imageNode: TransformImageNode? + public let animationNode: ASDisplayNode? + public let placeholderNode: ASDisplayNode? + public let imageLayer: CALayer? + public let relativeSourceRect: CGRect + + var sourceFrame: CGRect { + if let imageNode = self.imageNode { + return imageNode.frame + } else if let imageLayer = self.imageLayer { + return imageLayer.bounds + } else { + return CGRect(origin: CGPoint(), size: relativeSourceRect.size) + } + } + + var sourceLayer: CALayer? { + if let imageNode = self.imageNode { + return imageNode.layer + } else if let imageLayer = self.imageLayer { + return imageLayer + } else { + return nil + } + } + + func snapshotContentTree() -> UIView? { + if let animationNode = self.animationNode { + return animationNode.view.snapshotContentTree() + } else if let imageNode = self.imageNode { + return imageNode.view.snapshotContentTree() + } else if let sourceLayer = self.imageLayer { + return sourceLayer.snapshotContentTreeAsView() + } else { + return nil + } + } + + public init(imageNode: TransformImageNode?, animationNode: ASDisplayNode?, placeholderNode: ASDisplayNode?, imageLayer: CALayer?, relativeSourceRect: CGRect) { + self.imageNode = imageNode + self.animationNode = animationNode + self.placeholderNode = placeholderNode + self.imageLayer = imageLayer + self.relativeSourceRect = relativeSourceRect + } + } - func animateContentFromStickerGridItem(stickerSource: ChatMessageTransitionNode.Sticker, transition: CombinedTransition) { + public func animateContentFromStickerGridItem(stickerSource: AnimationTransitionSticker, transition: CombinedTransition) { guard let _ = self.item else { return } @@ -2834,20 +2859,50 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { placeholderNode.layer.animateAlpha(from: 0.0, to: placeholderNode.alpha, duration: 0.4) } } + + public final class AnimationTransitionReplyPanel { + public let titleNode: ASDisplayNode + public let textNode: ASDisplayNode + public let lineNode: ASDisplayNode + public let imageNode: ASDisplayNode + public let relativeSourceRect: CGRect + public let relativeTargetRect: CGRect - func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { + public init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) { + self.titleNode = titleNode + self.textNode = textNode + self.lineNode = lineNode + self.imageNode = imageNode + self.relativeSourceRect = relativeSourceRect + self.relativeTargetRect = relativeTargetRect + } + } + + public func animateReplyPanel(sourceReplyPanel: AnimationTransitionReplyPanel, transition: CombinedTransition) { if let replyInfoNode = self.replyInfoNode { let localRect = self.contextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) - let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, localRect: localRect, transition: transition) - if let replyBackgroundNode = self.replyBackgroundNode { - transition.animatePositionAdditive(layer: replyBackgroundNode.layer, offset: offset) - replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + let mappedPanel = ChatMessageReplyInfoNode.TransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ) + let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: mappedPanel, localRect: localRect, transition: transition) + if let replyBackgroundContent = self.replyBackgroundContent { + transition.animatePositionAdditive(layer: replyBackgroundContent.layer, offset: offset) + replyBackgroundContent.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + if let forwardBackgroundContent = self.forwardBackgroundContent { + transition.animatePositionAdditive(layer: forwardBackgroundContent.layer, offset: offset) + forwardBackgroundContent.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } } } - func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + public func animateFromLoadingPlaceholder(delay: Double, transition: ContainedViewLayoutTransition) { guard let item = self.item else { return } @@ -2857,14 +2912,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) } - override func openMessageContextMenu() { + override public func openMessageContextMenu() { guard let item = self.item else { return } item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil, nil) } - override func targetReactionView(value: MessageReaction.Reaction) -> UIView? { + override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result } @@ -2874,7 +2929,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return nil } - override func targetForStoryTransition(id: StoryId) -> UIView? { + override public func targetForStoryTransition(id: StoryId) -> UIView? { guard let item = self.item else { return nil } @@ -2890,17 +2945,17 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return nil } - override func unreadMessageRangeUpdated() { + override public func unreadMessageRangeUpdated() { self.updateVisibility() } - override func contentFrame() -> CGRect { + override public func contentFrame() -> CGRect { return self.imageNode.frame } } -struct AnimatedEmojiSoundsConfiguration { - static var defaultValue: AnimatedEmojiSoundsConfiguration { +public struct AnimatedEmojiSoundsConfiguration { + public static var defaultValue: AnimatedEmojiSoundsConfiguration { return AnimatedEmojiSoundsConfiguration(sounds: [:]) } @@ -2910,7 +2965,7 @@ struct AnimatedEmojiSoundsConfiguration { self.sounds = sounds } - static func with(appConfiguration: AppConfiguration, account: Account) -> AnimatedEmojiSoundsConfiguration { + public static func with(appConfiguration: AppConfiguration, account: Account) -> AnimatedEmojiSoundsConfiguration { if let data = appConfiguration.data, let values = data["emojies_sounds"] as? [String: Any] { var sounds: [String: TelegramMediaFile] = [:] for (key, value) in values { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/BUILD new file mode 100644 index 00000000000..9bbaaa0da4e --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageAttachedContentButtonNode", + module_name = "ChatMessageAttachedContentButtonNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ChatPresentationInterfaceState", + "//submodules/ShimmerEffect", + ], + visibility = [ + "//visibility:public", + ], +) + diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift new file mode 100644 index 00000000000..1904ea87468 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode/Sources/ChatMessageAttachedContentButtonNode.swift @@ -0,0 +1,181 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState +import ShimmerEffect + +private let buttonFont = Font.semibold(14.0) +private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate) + +public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { + private let textNode: TextNode + private var iconView: UIImageView? + private let shimmerEffectNode: ShimmerEffectForegroundNode + + private var backgroundView: UIImageView? + + private var regularIconImage: UIImage? + + public var pressed: (() -> Void)? + + private var titleColor: UIColor? + + public init() { + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + + self.shimmerEffectNode = ShimmerEffectForegroundNode() + self.shimmerEffectNode.cornerRadius = 5.0 + + super.init() + + self.addSubnode(self.shimmerEffectNode) + self.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width + strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false) + } else { + if let presentationLayer = strongSelf.layer.presentation() { + strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) + } + } + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.pressed?() + } + + public func startShimmering() { + guard let titleColor = self.titleColor else { + return + } + self.shimmerEffectNode.isHidden = false + self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let backgroundFrame = self.bounds + self.shimmerEffectNode.frame = backgroundFrame + self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size) + self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: titleColor.withAlphaComponent(0.3), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) + } + + public func stopShimmering() { + self.shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.shimmerEffectNode.isHidden = true + }) + } + + public typealias AsyncLayout = (_ width: CGFloat, _ iconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ inProgress: Bool, _ drawBackground: Bool) -> (CGFloat, (CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode)) + public static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> AsyncLayout { + let previousRegularIconImage = current?.regularIconImage + + let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) + + return { width, iconImage, cornerIcon, title, titleColor, inProgress, drawBackground in + let targetNode: ChatMessageAttachedContentButtonNode + if let current = current { + targetNode = current + } else { + targetNode = ChatMessageAttachedContentButtonNode() + } + + let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) + if let maybeMakeTextLayout = maybeMakeTextLayout { + makeTextLayout = maybeMakeTextLayout + } else { + makeTextLayout = TextNode.asyncLayout(targetNode.textNode) + } + + var updatedRegularIconImage: UIImage? + if iconImage !== previousRegularIconImage { + updatedRegularIconImage = iconImage + } + + var iconWidth: CGFloat = 0.0 + if let iconImage = iconImage { + iconWidth = iconImage.size.width + 5.0 + } + + let labelInset: CGFloat = 8.0 + + let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) + + return (textSize.size.width + labelInset * 2.0, { refinedWidth, refinedHeight in + let size = CGSize(width: refinedWidth, height: refinedHeight) + return (size, { animation in + targetNode.accessibilityLabel = title + + targetNode.titleColor = titleColor + + let iconView: UIImageView + if let current = targetNode.iconView { + iconView = current + } else { + iconView = UIImageView() + targetNode.iconView = iconView + targetNode.view.addSubview(iconView) + } + iconView.tintColor = titleColor + + if let updatedRegularIconImage = updatedRegularIconImage { + targetNode.regularIconImage = updatedRegularIconImage + if !targetNode.textNode.isHidden { + iconView.image = updatedRegularIconImage.withRenderingMode(.alwaysTemplate) + } + } + + let _ = textApply() + + let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: size.height)) + + var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size) + if drawBackground { + textFrame.origin.y += 1.0 + } + if let image = iconView.image { + let iconFrame: CGRect + if cornerIcon { + iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size) + } else { + textFrame.origin.x += floor(image.size.width / 2.0) + iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - image.size.height) * 0.5)), size: image.size) + } + + animation.animator.updateFrame(layer: iconView.layer, frame: iconFrame, completion: nil) + } + + targetNode.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + animation.animator.updatePosition(layer: targetNode.textNode.layer, position: textFrame.center, completion: nil) + + if drawBackground { + let backgroundView: UIImageView + if let current = targetNode.backgroundView { + backgroundView = current + animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) + } else { + backgroundView = UIImageView() + backgroundView.image = sharedBackgroundImage + targetNode.backgroundView = backgroundView + targetNode.view.insertSubview(backgroundView, at: 0) + backgroundView.frame = backgroundFrame + } + backgroundView.tintColor = titleColor.withMultipliedAlpha(0.1) + } else if let backgroundView = targetNode.backgroundView { + targetNode.backgroundView = nil + backgroundView.removeFromSuperview() + } + + return targetNode + }) + }) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/BUILD new file mode 100644 index 00000000000..4204788d7a4 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/BUILD @@ -0,0 +1,47 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageAttachedContentNode", + module_name = "ChatMessageAttachedContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/UrlEscaping", + "//submodules/PhotoResources", + "//submodules/WebsiteType", + "//submodules/ChatMessageInteractiveMediaBadge", + "//submodules/GalleryData", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/ShimmerEffect", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode", + "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode", + "//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift new file mode 100644 index 00000000000..67455efa73c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -0,0 +1,2289 @@ +import Foundation +import UIKit +import Postbox +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TextFormat +import AccountContext +import UrlEscaping +import PhotoResources +import WebsiteType +import ChatMessageInteractiveMediaBadge +import GalleryData +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import ChatControllerInteraction +import ShimmerEffect +import ChatMessageDateAndStatusNode +import ChatHistoryEntry +import ChatMessageItemCommon +import ChatMessageBubbleContentNode +import ChatMessageInteractiveInstantVideoNode +import ChatMessageInteractiveFileNode +import ChatMessageInteractiveMediaNode +import WallpaperPreviewMedia +import ChatMessageAttachedContentButtonNode +import MessageInlineBlockBackgroundView + +public enum ChatMessageAttachedContentActionIcon { + case instant + case link +} + +public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let preferMediaInline = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 0) + public static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1) + public static let preferMediaAspectFilled = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 2) + public static let titleBeforeMedia = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 3) +} + +public final class ChatMessageAttachedContentNode: ASDisplayNode { + private var backgroundView: MessageInlineBlockBackgroundView? + + private let transformContainer: ASDisplayNode + private var title: TextNodeWithEntities? + private var subtitle: TextNodeWithEntities? + private var text: TextNodeWithEntities? + private var inlineMedia: TransformImageNode? + private var contentMedia: ChatMessageInteractiveMediaNode? + private var contentInstantVideo: ChatMessageInteractiveInstantVideoNode? + private var contentFile: ChatMessageInteractiveFileNode? + private var actionButton: ChatMessageAttachedContentButtonNode? + private var actionButtonSeparator: SimpleLayer? + public var statusNode: ChatMessageDateAndStatusNode? + + private var inlineMediaValue: Media? + + //private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge? + private var linkHighlightingNode: LinkHighlightingNode? + + private var context: AccountContext? + private var message: Message? + private var media: Media? + private var theme: ChatPresentationThemeData? + + private var isHighlighted: Bool = false + private var highlightTimer: Foundation.Timer? + + public var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)? + public var activateAction: (() -> Void)? + public var requestUpdateLayout: (() -> Void)? + + private var currentProgressDisposable: Disposable? + + public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) } + + public var visibility: ListViewItemNodeVisibility = .none { + didSet { + if oldValue != self.visibility { + self.contentMedia?.visibility = self.visibility != .none + self.contentInstantVideo?.visibility = self.visibility != .none + + switch self.visibility { + case .none: + self.text?.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.text?.visibilityRect = subRect + } + } + } + } + + override public init() { + self.transformContainer = ASDisplayNode() + + super.init() + + self.addSubnode(self.transformContainer) + } + + deinit { + self.highlightTimer?.invalidate() + } + + @objc private func pressed() { + self.activateAction?() + } + + public typealias AsyncLayout = (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) + + public func makeProgress() -> Promise { + let progress = Promise() + self.currentProgressDisposable?.dispose() + self.currentProgressDisposable = (progress.get() + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] hasProgress in + guard let self else { + return + } + self.backgroundView?.displayProgress = hasProgress + }) + return progress + } + + public func asyncLayout() -> AsyncLayout { + let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.title) + let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitle) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.text) + let makeContentMedia = ChatMessageInteractiveMediaNode.asyncLayout(self.contentMedia) + let makeContentFile = ChatMessageInteractiveFileNode.asyncLayout(self.contentFile) + let makeActionButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.actionButton) + let makeStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) + + return { [weak self] presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in + let isPreview = presentationData.isPreview + let fontSize: CGFloat + if message.adAttribute != nil { + fontSize = floor(presentationData.fontSize.baseDisplaySize) + } else { + fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) + } + + let titleFont = Font.semibold(fontSize) + let textFont = Font.regular(fontSize) + let textBoldFont = Font.semibold(fontSize) + let textItalicFont = Font.italic(fontSize) + let textBoldItalicFont = Font.semiboldItalic(fontSize) + let textFixedFont = Font.regular(fontSize) + let textBlockQuoteFont = Font.regular(fontSize) + + var incoming = message.effectivelyIncoming(context.account.peerId) + if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { + incoming = false + } + + var isReplyThread = false + if case .replyThread = chatLocation { + isReplyThread = true + } + + let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing + let author = message.author + let nameColors = author?.nameColor.flatMap { context.peerNameColors.get($0, dark: presentationData.theme.theme.overallDarkAppearance) } + + let mainColor: UIColor + var secondaryColor: UIColor? + var tertiaryColor: UIColor? + if !incoming { + mainColor = messageTheme.accentTextColor + if let _ = nameColors?.secondary { + secondaryColor = .clear + } + if let _ = nameColors?.tertiary { + tertiaryColor = .clear + } + } else { + var authorNameColor: UIColor? + authorNameColor = nameColors?.main + secondaryColor = nameColors?.secondary + tertiaryColor = nameColors?.tertiary + + if let authorNameColor { + mainColor = authorNameColor + } else { + mainColor = messageTheme.accentTextColor + } + } + + let textTopSpacing: CGFloat + let textBottomSpacing: CGFloat + + if displayLine { + textTopSpacing = 3.0 + textBottomSpacing = 3.0 + } else { + textTopSpacing = -2.0 + textBottomSpacing = 0.0 + } + + let textLineSpacing: CGFloat = 0.09 + let titleTextSpacing: CGFloat = 0.0 + let textContentMediaSpacing: CGFloat = 6.0 + let contentMediaTopSpacing: CGFloat = 6.0 + let contentMediaBottomSpacing: CGFloat = 6.0 + let contentMediaButtonSpacing: CGFloat = 7.0 + let textButtonSpacing: CGFloat = 7.0 + let buttonBottomSpacing: CGFloat = 0.0 + let statusBackgroundSpacing: CGFloat = 9.0 + let inlineMediaEdgeInset: CGFloat = 6.0 + + var insets = UIEdgeInsets() + insets.left = layoutConstants.text.bubbleInsets.left + insets.right = layoutConstants.text.bubbleInsets.right + + if case let .linear(top, _) = preparePosition { + switch top { + case .None: + break + default: + break + } + } + + if displayLine { + insets.left += 9.0 + insets.right += 6.0 + } + + var contentMediaValue: Media? + var contentFileValue: TelegramMediaFile? + + var contentMediaAutomaticPlayback: Bool = false + var contentMediaAutomaticDownload: InteractiveMediaNodeAutodownloadMode = .none + + var mediaAndFlags = mediaAndFlags + if let mediaAndFlagsValue = mediaAndFlags { + if mediaAndFlagsValue.0 is TelegramMediaStory || mediaAndFlagsValue.0 is WallpaperPreviewMedia { + var flags = mediaAndFlagsValue.1 + flags.remove(.preferMediaInline) + mediaAndFlags = (mediaAndFlagsValue.0, flags) + } + } + + var contentMediaAspectFilled = false + if let (_, flags) = mediaAndFlags { + contentMediaAspectFilled = flags.contains(.preferMediaAspectFilled) + } + var contentMediaInline = false + + if let (media, flags) = mediaAndFlags { + contentMediaInline = flags.contains(.preferMediaInline) + + if let file = media as? TelegramMediaFile { + if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { + contentMediaValue = file + } else if file.isInstantVideo { + contentMediaValue = file + } else if file.isVideo { + contentMediaValue = file + } else if file.isSticker || file.isAnimatedSticker { + contentMediaValue = file + } else { + contentFileValue = file + } + + if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { + contentMediaAutomaticDownload = .full + } else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) { + contentMediaAutomaticDownload = .prefetch + } + + if file.isAnimated { + contentMediaAutomaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif + } else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo { + var willDownloadOrLocal = false + if case .full = contentMediaAutomaticDownload { + willDownloadOrLocal = true + } else { + willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil + } + if willDownloadOrLocal { + contentMediaAutomaticPlayback = true + contentMediaAspectFilled = true + } + } + } else if let _ = media as? TelegramMediaImage { + contentMediaValue = media + } else if let _ = media as? TelegramMediaWebFile { + contentMediaValue = media + } else if let _ = media as? WallpaperPreviewMedia { + contentMediaValue = media + } else if let _ = media as? TelegramMediaStory { + contentMediaValue = media + } + } + + var maxWidth: CGFloat = .greatestFiniteMagnitude + + let contentMediaContinueLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? + let inlineMediaAndSize: (Media, CGSize)? + + if let contentMediaValue { + if contentMediaInline { + contentMediaContinueLayout = nil + + if let image = contentMediaValue as? TelegramMediaImage { + inlineMediaAndSize = (image, CGSize(width: 54.0, height: 54.0)) + } else if let file = contentMediaValue as? TelegramMediaFile, !file.previewRepresentations.isEmpty { + inlineMediaAndSize = (file, CGSize(width: 54.0, height: 54.0)) + } else { + inlineMediaAndSize = nil + } + } else { + let contentMode: InteractiveMediaNodeContentMode = contentMediaAspectFilled ? .aspectFill : .aspectFit + + let (_, initialImageWidth, refineLayout) = makeContentMedia( + context, + presentationData, + presentationData.dateTimeFormat, + message, associatedData, + attributes, + contentMediaValue, + nil, + .full, + associatedData.automaticDownloadPeerType, + associatedData.automaticDownloadPeerId, + .constrained(CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)), + layoutConstants, + contentMode, + controllerInteraction.presentationContext + ) + contentMediaContinueLayout = refineLayout + maxWidth = initialImageWidth + insets.left + insets.right + + inlineMediaAndSize = nil + } + } else { + contentMediaContinueLayout = nil + inlineMediaAndSize = nil + } + + let contentFileContinueLayout: ChatMessageInteractiveFileNode.ContinueLayout? + if let contentFileValue { + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentFileValue) + + let (_, refineLayout) = makeContentFile(ChatMessageInteractiveFileNode.Arguments( + context: context, + presentationData: presentationData, + customTintColor: incoming ? mainColor : nil, + message: message, + topMessage: message, + associatedData: associatedData, + chatLocation: chatLocation, + attributes: attributes, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + forcedIsEdited: false, + file: contentFileValue, + automaticDownload: automaticDownload, + incoming: incoming, + isRecentActions: false, + forcedResourceStatus: associatedData.forcedResourceStatus, + dateAndStatusType: nil, + displayReactions: false, + messageSelection: nil, + isAttachedContentBlock: true, + layoutConstants: layoutConstants, + constrainedSize: CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), + controllerInteraction: controllerInteraction + )) + contentFileContinueLayout = refineLayout + } else { + contentFileContinueLayout = nil + } + + return (maxWidth, { constrainedSize, position in + enum ContentLayoutOrderItem { + case title + case subtitle + case text + case media + case file + case actionButton + } + var contentLayoutOrder: [ContentLayoutOrderItem] = [] + + if let title = title, !title.isEmpty { + contentLayoutOrder.append(.title) + } + if let subtitle = subtitle, !subtitle.string.isEmpty { + contentLayoutOrder.append(.subtitle) + } + if let text = text, !text.isEmpty { + contentLayoutOrder.append(.text) + } + if contentMediaContinueLayout != nil { + if let (_, flags) = mediaAndFlags { + if flags.contains(.titleBeforeMedia) { + if let index = contentLayoutOrder.firstIndex(of: .title) { + contentLayoutOrder.insert(.media, at: index + 1) + } else { + contentLayoutOrder.insert(.media, at: 0) + } + } else if flags.contains(.preferMediaBeforeText) { + contentLayoutOrder.insert(.media, at: 0) + } else { + contentLayoutOrder.append(.media) + } + } else { + contentLayoutOrder.append(.media) + } + } + if contentFileContinueLayout != nil { + contentLayoutOrder.append(.file) + } + if !isPreview, actionTitle != nil { + contentLayoutOrder.append(.actionButton) + } + + var actualWidth: CGFloat = 0.0 + + let maxContentsWidth: CGFloat = constrainedSize.width - insets.left - insets.right + + var titleLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + var subtitleLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + var textLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? + + var remainingCutoutHeight: CGFloat = 0.0 + var cutoutWidth: CGFloat = 0.0 + if let (_, inlineMediaSize) = inlineMediaAndSize { + remainingCutoutHeight = inlineMediaSize.height + cutoutWidth = inlineMediaSize.width + inlineMediaEdgeInset + } + for item in contentLayoutOrder { + switch item { + case .title: + if let title = title, !title.isEmpty { + var cutout: TextNodeCutout? + if remainingCutoutHeight > 0.0 { + cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) + } + + let titleString = NSAttributedString(string: title, font: titleFont, textColor: mainColor) + let titleLayoutAndApplyValue = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) + titleLayoutAndApply = titleLayoutAndApplyValue + + remainingCutoutHeight -= titleLayoutAndApplyValue.0.size.height + } + case .subtitle: + if let subtitle = subtitle, !subtitle.string.isEmpty { + var cutout: TextNodeCutout? + if remainingCutoutHeight > 0.0 { + cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) + } + + let subtitleString = NSMutableAttributedString(attributedString: subtitle) + subtitleString.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length)) + subtitleString.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length)) + + let subtitleLayoutAndApplyValue = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) + subtitleLayoutAndApply = subtitleLayoutAndApplyValue + + remainingCutoutHeight -= subtitleLayoutAndApplyValue.0.size.height + } + case .text: + if let text = text, !text.isEmpty { + var cutout: TextNodeCutout? + if remainingCutoutHeight > 0.0 { + cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) + } + + 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())) + textLayoutAndApply = textLayoutAndApplyValue + + remainingCutoutHeight -= textLayoutAndApplyValue.0.size.height + } + case .media, .file, .actionButton: + break + } + } + + if let (titleLayout, _) = titleLayoutAndApply { + actualWidth = max(actualWidth, titleLayout.size.width) + } + if let (subtitleLayout, _) = subtitleLayoutAndApply { + actualWidth = max(actualWidth, subtitleLayout.size.width) + } + if let (textLayout, _) = textLayoutAndApply { + actualWidth = max(actualWidth, textLayout.size.width) + } + + let actionButtonMinWidthAndFinalizeLayout: (CGFloat, ((CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode)))? + if !isPreview, let actionTitle { + var buttonIconImage: UIImage? + var cornerIcon = false + + if incoming { + if let actionIcon { + switch actionIcon { + case .instant: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)! + case .link: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)! + cornerIcon = true + } + } + } else { + if let actionIcon { + switch actionIcon { + case .instant: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)! + case .link: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)! + cornerIcon = true + } + } + } + + let (buttonWidth, continueLayout) = makeActionButtonLayout( + maxContentsWidth, + buttonIconImage, + cornerIcon, + actionTitle, + mainColor, + false, + message.adAttribute != nil + ) + actionButtonMinWidthAndFinalizeLayout = (buttonWidth, continueLayout) + actualWidth = max(actualWidth, buttonWidth) + } else { + actionButtonMinWidthAndFinalizeLayout = nil + } + + let contentMediaFinalizeLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))? + if let contentMediaContinueLayout { + let (refinedWidth, finalizeImageLayout) = contentMediaContinueLayout(CGSize(width: constrainedSize.width, height: constrainedSize.height), contentMediaAutomaticPlayback, true, ImageCorners(radius: 4.0)) + actualWidth = max(actualWidth, refinedWidth) + contentMediaFinalizeLayout = finalizeImageLayout + } else { + contentMediaFinalizeLayout = nil + } + + let contentFileFinalizeLayout: ChatMessageInteractiveFileNode.FinalizeLayout? + if let contentFileContinueLayout { + let (refinedWidth, finalizeFileLayout) = contentFileContinueLayout(CGSize(width: constrainedSize.width, height: constrainedSize.height)) + actualWidth = max(actualWidth, refinedWidth) + contentFileFinalizeLayout = finalizeFileLayout + } else { + contentFileFinalizeLayout = nil + } + + var edited = false + if attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message) + if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) { + dateReactionsAndPeers = ([], []) + } + for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = !attribute.isHidden + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData) + + let statusType: ChatMessageDateAndStatusType + if incoming { + statusType = .BubbleIncoming + } else { + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + + let maxStatusContentWidth: CGFloat = constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right + + var trailingContentWidth: CGFloat? + if let _ = message.adAttribute, let (textLayout, _) = textLayoutAndApply { + if textLayout.hasRTL { + trailingContentWidth = 10000.0 + } else { + trailingContentWidth = textLayout.trailingLineWidth + } + } else { + if !displayLine, let (actionButtonMinWidth, _) = actionButtonMinWidthAndFinalizeLayout { + trailingContentWidth = actionButtonMinWidth + } + } + + var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? + if case let .linear(_, bottom) = position { + switch bottom { + case .None, .Neighbour(_, .footer, _): + let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments( + context: context, + presentationData: presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent( + contentWidth: trailingContentWidth, + reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions), preferAdditionalInset: false) + ), + constrainedSize: CGSize(width: maxStatusContentWidth, height: CGFloat.greatestFiniteMagnitude), + availableReactions: associatedData.availableReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, + displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser, + replyCount: dateReplies, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message), + animationCache: controllerInteraction.presentationContext.animationCache, + animationRenderer: controllerInteraction.presentationContext.animationRenderer + )) + statusLayoutAndContinue = statusLayoutAndContinueValue + actualWidth = max(actualWidth, statusLayoutAndContinueValue.0) + default: + break + } + } + + actualWidth += insets.left + insets.right + + return (actualWidth, { resultingWidth in + let statusSizeAndApply = statusLayoutAndContinue?.1(resultingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0) + + let contentMediaSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)? + if let contentMediaFinalizeLayout { + let (size, apply) = contentMediaFinalizeLayout(resultingWidth - insets.left - insets.right) + contentMediaSizeAndApply = (size, apply) + } else { + contentMediaSizeAndApply = nil + } + + let contentFileSizeAndApply: (CGSize, ChatMessageInteractiveFileNode.Apply)? + if let contentFileFinalizeLayout { + let (size, apply) = contentFileFinalizeLayout(resultingWidth - insets.left - insets.right) + contentFileSizeAndApply = (size, apply) + } else { + contentFileSizeAndApply = nil + } + + let actionButtonSizeAndApply: ((CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))? + if let (_, actionButtonFinalizeLayout) = actionButtonMinWidthAndFinalizeLayout { + let (size, apply) = actionButtonFinalizeLayout(resultingWidth - insets.left - insets.right, 36.0) + actionButtonSizeAndApply = (size, apply) + } else { + actionButtonSizeAndApply = nil + } + + var actualSize = CGSize() + + var backgroundInsets = UIEdgeInsets() + backgroundInsets.left += layoutConstants.text.bubbleInsets.left + backgroundInsets.right += layoutConstants.text.bubbleInsets.right + + if case let .linear(top, _) = position { + switch top { + case .None: + actualSize.height += 11.0 + backgroundInsets.top = actualSize.height + default: + break + } + } + + actualSize.width = resultingWidth + + struct ContentDisplayOrderItem { + let item: ContentLayoutOrderItem + let offsetY: CGFloat + } + var contentDisplayOrder: [ContentDisplayOrderItem] = [] + + for i in 0 ..< contentLayoutOrder.count { + let item = contentLayoutOrder[i] + switch item { + case .title: + if let (titleLayout, _) = titleLayoutAndApply { + if i == 0 { + actualSize.height += textTopSpacing + } else if contentLayoutOrder[i - 1] == .media || contentLayoutOrder[i - 1] == .file { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += titleLayout.size.height - titleLayout.insets.top - titleLayout.insets.bottom + } + case .subtitle: + if let (subtitleLayout, _) = subtitleLayoutAndApply { + if i == 0 { + actualSize.height += textTopSpacing + } else if contentLayoutOrder[i - 1] == .title { + actualSize.height += titleTextSpacing + } else if contentLayoutOrder[i - 1] == .media || contentLayoutOrder[i - 1] == .file { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += subtitleLayout.size.height - subtitleLayout.insets.top - subtitleLayout.insets.bottom + } + case .text: + if let (textLayout, _) = textLayoutAndApply { + if i == 0 { + actualSize.height += textTopSpacing + } else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle { + actualSize.height += titleTextSpacing + } else if contentLayoutOrder[i - 1] == .media || contentLayoutOrder[i - 1] == .file { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += textLayout.size.height - textLayout.insets.top - textLayout.insets.bottom + } + case .media: + if let (contentMediaSize, _) = contentMediaSizeAndApply { + if i == 0 { + actualSize.height += contentMediaTopSpacing + } else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle || contentLayoutOrder[i - 1] == .text { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += contentMediaSize.height + } + case .file: + if let (contentFileSize, _) = contentFileSizeAndApply { + if i == 0 { + actualSize.height += contentMediaTopSpacing + } else if contentLayoutOrder[i - 1] == .title || contentLayoutOrder[i - 1] == .subtitle || contentLayoutOrder[i - 1] == .text { + actualSize.height += textContentMediaSpacing + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += contentFileSize.height + } + case .actionButton: + if let (actionButtonSize, _) = actionButtonSizeAndApply { + if i != 0 { + switch contentLayoutOrder[i - 1] { + case .title, .subtitle, .text: + actualSize.height += textButtonSpacing + case .media, .file: + actualSize.height += contentMediaButtonSpacing + default: + break + } + } + + if let (_, inlineMediaSize) = inlineMediaAndSize { + if actualSize.height < insets.top + inlineMediaEdgeInset + inlineMediaSize.height + contentMediaButtonSpacing { + actualSize.height = insets.top + inlineMediaEdgeInset + inlineMediaSize.height + contentMediaButtonSpacing + } + } + + contentDisplayOrder.append(ContentDisplayOrderItem( + item: item, + offsetY: actualSize.height + )) + + actualSize.height += actionButtonSize.height + } + } + } + + if !contentLayoutOrder.isEmpty { + switch contentLayoutOrder[contentLayoutOrder.count - 1] { + case .title, .subtitle, .text: + actualSize.height += textBottomSpacing + + if let (_, inlineMediaSize) = inlineMediaAndSize { + if actualSize.height < backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset { + actualSize.height = backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset + } + } + case .media, .file: + actualSize.height += contentMediaBottomSpacing + case .actionButton: + actualSize.height += buttonBottomSpacing + } + } else { + if let (_, inlineMediaSize) = inlineMediaAndSize { + if actualSize.height < backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset { + actualSize.height = backgroundInsets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset + } + } + } + + if case let .linear(_, bottom) = position, let statusSizeAndApply { + switch bottom { + case .None, .Neighbour(_, .footer, _): + let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height + actualSize.height += bottomStatusContentHeight + backgroundInsets.bottom += bottomStatusContentHeight + default: + break + } + } + + return (actualSize, { animation, synchronousLoads, applyInfo in + guard let self else { + return + } + + self.context = context + self.message = message + self.media = mediaAndFlags?.0 + self.theme = presentationData.theme + + animation.animator.updateFrame(layer: self.transformContainer.layer, frame: CGRect(origin: CGPoint(), size: actualSize), completion: nil) + + if displayLine { + 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)) + + 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, 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, pattern: nil, animation: .None) + } + } else { + if let backgroundView = self.backgroundView { + self.backgroundView = nil + backgroundView.removeFromSuperview() + } + } + + if let (inlineMediaValue, inlineMediaSize) = inlineMediaAndSize { + var inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMediaSize.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMediaSize) + if contentLayoutOrder.isEmpty { + inlineMediaFrame.origin.x = insets.left + } + + let inlineMedia: TransformImageNode + var updateMedia = false + if let current = self.inlineMedia { + inlineMedia = current + + if let curentInlineMediaValue = self.inlineMediaValue { + updateMedia = !curentInlineMediaValue.isSemanticallyEqual(to: inlineMediaValue) + } else { + updateMedia = true + } + + animation.animator.updateFrame(layer: inlineMedia.layer, frame: inlineMediaFrame, completion: nil) + } else { + inlineMedia = TransformImageNode() + inlineMedia.contentAnimations = .subsequentUpdates + self.inlineMedia = inlineMedia + self.transformContainer.addSubnode(inlineMedia) + + inlineMedia.frame = inlineMediaFrame + + updateMedia = true + + inlineMedia.alpha = 0.0 + animation.animator.updateAlpha(layer: inlineMedia.layer, alpha: 1.0, completion: nil) + animation.animator.animateScale(layer: inlineMedia.layer, from: 0.01, to: 1.0, completion: nil) + } + self.inlineMediaValue = inlineMediaValue + + var fittedImageSize = inlineMediaSize + if let image = inlineMediaValue as? TelegramMediaImage { + if let dimensions = image.representations.last?.dimensions.cgSize { + fittedImageSize = dimensions.aspectFilled(inlineMediaSize) + } + } else if let file = inlineMediaValue as? TelegramMediaFile { + if let dimensions = file.dimensions?.cgSize { + fittedImageSize = dimensions.aspectFilled(inlineMediaSize) + } + } + + if updateMedia { + let resolvedInlineMediaValue = inlineMediaValue + + if let image = resolvedInlineMediaValue as? TelegramMediaImage { + let updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), placeholderColor: mainColor.withMultipliedAlpha(0.1)) + inlineMedia.setSignal(updateInlineImageSignal) + } else if let file = resolvedInlineMediaValue as? TelegramMediaFile, let representation = file.previewRepresentations.last { + let updateInlineImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file), representation: representation) + inlineMedia.setSignal(updateInlineImageSignal) + } + } + + inlineMedia.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: fittedImageSize, boundingSize: inlineMediaSize, intrinsicInsets: UIEdgeInsets(), emptyColor: mainColor.withMultipliedAlpha(0.1)))() + } else { + if let inlineMedia = self.inlineMedia { + self.inlineMedia = nil + + let inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMedia.bounds.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMedia.bounds.size) + animation.animator.updateFrame(layer: inlineMedia.layer, frame: inlineMediaFrame, completion: nil) + animation.animator.updateAlpha(layer: inlineMedia.layer, alpha: 0.0, completion: nil) + animation.animator.updateScale(layer: inlineMedia.layer, scale: 0.01, completion: { [weak inlineMedia] _ in + inlineMedia?.removeFromSupernode() + }) + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .title }), let (titleLayout, titleApply) = titleLayoutAndApply { + let title = titleApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + + let titleFrame = CGRect(origin: CGPoint(x: -titleLayout.insets.left + insets.left, y: -titleLayout.insets.top + item.offsetY), size: titleLayout.size) + + if self.title !== title { + self.title?.textNode.removeFromSupernode() + self.title = title + title.textNode.layer.anchorPoint = CGPoint() + self.transformContainer.addSubnode(title.textNode) + + title.textNode.frame = titleFrame + } else { + title.textNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + animation.animator.updatePosition(layer: title.textNode.layer, position: titleFrame.origin, completion: nil) + } + } else { + if let title = self.title { + self.title = nil + title.textNode.removeFromSupernode() + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .subtitle }), let (subtitleLayout, subtitleApply) = subtitleLayoutAndApply { + let subtitle = subtitleApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + + let subtitleFrame = CGRect(origin: CGPoint(x: -subtitleLayout.insets.left + insets.left, y: -subtitleLayout.insets.top + item.offsetY), size: subtitleLayout.size) + + if self.subtitle !== subtitle { + self.subtitle?.textNode.removeFromSupernode() + self.subtitle = subtitle + subtitle.textNode.layer.anchorPoint = CGPoint() + self.transformContainer.addSubnode(subtitle.textNode) + + subtitle.textNode.frame = subtitleFrame + } else { + subtitle.textNode.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + animation.animator.updatePosition(layer: subtitle.textNode.layer, position: subtitleFrame.origin, completion: nil) + } + } else { + if let subtitle = self.subtitle { + self.subtitle = nil + subtitle.textNode.removeFromSupernode() + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .text }), let (textLayout, textApply) = textLayoutAndApply { + let text = textApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + + let textFrame = CGRect(origin: CGPoint(x: -textLayout.insets.left + insets.left, y: -textLayout.insets.top + item.offsetY), size: textLayout.size) + + if self.text !== text { + self.text?.textNode.removeFromSupernode() + self.text = text + text.textNode.layer.anchorPoint = CGPoint() + self.transformContainer.addSubnode(text.textNode) + + text.textNode.frame = textFrame + } else { + text.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + animation.animator.updatePosition(layer: text.textNode.layer, position: textFrame.origin, completion: nil) + } + } else { + if let text = self.text { + self.text = nil + text.textNode.removeFromSupernode() + } + } + + 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) + + let contentMedia = contentMediaApply(animation, synchronousLoads) + if self.contentMedia !== contentMedia { + self.contentMedia?.removeFromSupernode() + self.contentMedia = contentMedia + + contentMedia.activateLocalContent = { [weak self] mode in + guard let self else { + return + } + self.openMedia?(mode) + } + contentMedia.updateMessageReaction = { [weak controllerInteraction] message, value in + guard let controllerInteraction else { + return + } + controllerInteraction.updateMessageReaction(message, value) + } + contentMedia.visibility = self.visibility != .none + + self.transformContainer.addSubnode(contentMedia) + + contentMedia.frame = contentMediaFrame + + contentMedia.alpha = 0.0 + animation.animator.updateAlpha(layer: contentMedia.layer, alpha: 1.0, completion: nil) + animation.animator.animateScale(layer: contentMedia.layer, from: 0.01, to: 1.0, completion: nil) + } else { + animation.animator.updateFrame(layer: contentMedia.layer, frame: contentMediaFrame, completion: nil) + } + } else { + if let contentMedia = self.contentMedia { + self.contentMedia = nil + + animation.animator.updateAlpha(layer: contentMedia.layer, alpha: 0.0, completion: nil) + animation.animator.updateScale(layer: contentMedia.layer, scale: 0.01, completion: { [weak contentMedia] _ in + contentMedia?.removeFromSupernode() + }) + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .file }), let (contentFileSize, contentFileApply) = contentFileSizeAndApply { + let contentFileFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: contentFileSize) + + let contentFile = contentFileApply(synchronousLoads, animation, applyInfo) + if self.contentFile !== contentFile { + self.contentFile?.removeFromSupernode() + self.contentFile = contentFile + + contentFile.activateLocalContent = { [weak self] in + guard let self else { + return + } + self.openMedia?(.default) + } + contentFile.visibility = self.visibility != .none + + self.transformContainer.addSubnode(contentFile) + + contentFile.frame = contentFileFrame + + contentFile.alpha = 0.0 + animation.animator.updateAlpha(layer: contentFile.layer, alpha: 1.0, completion: nil) + animation.animator.animateScale(layer: contentFile.layer, from: 0.01, to: 1.0, completion: nil) + } else { + animation.animator.updateFrame(layer: contentFile.layer, frame: contentFileFrame, completion: nil) + } + } else { + if let contentFile = self.contentFile { + self.contentFile = nil + + animation.animator.updateAlpha(layer: contentFile.layer, alpha: 0.0, completion: nil) + animation.animator.updateScale(layer: contentFile.layer, scale: 0.01, completion: { [weak contentFile] _ in + contentFile?.removeFromSupernode() + }) + } + } + + if let item = contentDisplayOrder.first(where: { $0.item == .actionButton }), let (actionButtonSize, actionButtonApply) = actionButtonSizeAndApply { + var actionButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: actionButtonSize) + if let _ = message.adAttribute, let statusSizeAndApply { + actionButtonFrame.origin.y += statusSizeAndApply.0.height + } + + let actionButton = actionButtonApply(animation) + + if self.actionButton !== actionButton { + self.actionButton?.removeFromSupernode() + self.actionButton = actionButton + self.transformContainer.addSubnode(actionButton) + actionButton.frame = actionButtonFrame + + actionButton.pressed = { [weak self] in + guard let self else { + return + } + self.activateAction?() + } + } else { + animation.animator.updateFrame(layer: actionButton.layer, frame: actionButtonFrame, completion: nil) + } + + if let _ = message.adAttribute { + + } else { + let separatorFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX, y: actionButtonFrame.minY - 1.0), size: CGSize(width: actionButtonFrame.width, height: UIScreenPixel)) + + let actionButtonSeparator: SimpleLayer + if let current = self.actionButtonSeparator { + actionButtonSeparator = current + animation.animator.updateFrame(layer: actionButtonSeparator, frame: separatorFrame, completion: nil) + } else { + actionButtonSeparator = SimpleLayer() + self.actionButtonSeparator = actionButtonSeparator + self.layer.addSublayer(actionButtonSeparator) + actionButtonSeparator.frame = separatorFrame + } + + actionButtonSeparator.backgroundColor = mainColor.withMultipliedAlpha(0.2).cgColor + } + } else { + if let actionButton = self.actionButton { + self.actionButton = nil + actionButton.removeFromSupernode() + } + } + + if self.actionButton == nil, let actionButtonSeparator = self.actionButtonSeparator { + self.actionButtonSeparator = nil + actionButtonSeparator.removeFromSuperlayer() + } + + if let statusSizeAndApply { + var statusFrame = CGRect(origin: CGPoint(x: actualSize.width - backgroundInsets.right - statusSizeAndApply.0.width, y: actualSize.height - layoutConstants.text.bubbleInsets.bottom - statusSizeAndApply.0.height), size: statusSizeAndApply.0) + if let _ = message.adAttribute, let (actionButtonSize, _) = actionButtonSizeAndApply { + statusFrame.origin.y -= actionButtonSize.height + statusBackgroundSpacing + } + + let statusNode = statusSizeAndApply.1(self.statusNode == nil ? .None : animation) + if self.statusNode !== statusNode { + self.statusNode?.removeFromSupernode() + self.statusNode = statusNode + self.addSubnode(statusNode) + + statusNode.reactionSelected = { [weak self] value in + guard let self, let message = self.message else { + return + } + controllerInteraction.updateMessageReaction(message, .reaction(value)) + } + + statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let self, let message = self.message else { + gesture?.cancel() + return + } + controllerInteraction.openMessageReactionContextMenu(message, sourceNode, gesture, value) + } + + statusNode.frame = statusFrame + } else { + animation.animator.updateFrame(layer: statusNode.layer, frame: statusFrame, completion: nil) + } + } else if let statusNode = self.statusNode { + self.statusNode = nil + statusNode.removeFromSupernode() + } + }) + }) + }) + + /*var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0) + if displayLine { + horizontalInsets.left += 10.0 + horizontalInsets.right += 9.0 + } + + var titleBeforeMedia = false + var preferMediaBeforeText = false + var preferMediaAspectFilled = false + if let (_, flags) = mediaAndFlags { + preferMediaBeforeText = flags.contains(.preferMediaBeforeText) + preferMediaAspectFilled = flags.contains(.preferMediaAspectFilled) + titleBeforeMedia = flags.contains(.titleBeforeMedia) + } + + var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit + + var edited = false + if attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message) + if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) { + dateReactionsAndPeers = ([], []) + } + for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = !attribute.isHidden + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData) + + var webpageGalleryMediaCount: Int? + for media in message.media { + if let media = media as? TelegramMediaWebpage { + if case let .Loaded(content) = media.content, let instantPage = content.instantPage, let image = content.image { + switch instantPageType(of: content) { + case .album: + let count = instantPageGalleryMedia(webpageId: media.webpageId, page: instantPage, galleryMedia: image).count + if count > 1 { + webpageGalleryMediaCount = count + } + default: + break + } + } + } + } + + var textString: NSAttributedString? + var inlineImageDimensions: CGSize? + var inlineImageSize: CGSize? + var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var textCutout = TextNodeCutout() + var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude + var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? + var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)))? + + var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)? + + let topTitleString = NSMutableAttributedString() + + let string = NSMutableAttributedString() + var notEmpty = false + + let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing + + if let title = title, !title.isEmpty { + if titleBeforeMedia { + topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) + } else { + string.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) + notEmpty = true + } + } + + if let subtitle = subtitle, subtitle.length > 0 { + if notEmpty { + string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) + } + let updatedSubtitle = NSMutableAttributedString() + updatedSubtitle.append(subtitle) + updatedSubtitle.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length)) + updatedSubtitle.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length)) + string.append(updatedSubtitle) + notEmpty = true + } + + if let text = text, !text.isEmpty { + if notEmpty { + string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) + } + if let entities = entities { + string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true)) + } else { + string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: messageTheme.primaryTextColor)) + } + notEmpty = true + } + + textString = string + if string.length > 1000 { + textString = string.attributedSubstring(from: NSMakeRange(0, 1000)) + } + + var isReplyThread = false + if case .replyThread = chatLocation { + isReplyThread = true + } + + var skipStandardStatus = false + var isImage = false + var isFile = false + + var automaticPlayback = false + + var textStatusType: ChatMessageDateAndStatusType? + var imageStatusType: ChatMessageDateAndStatusType? + var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? + + if let (media, flags) = mediaAndFlags { + if let file = media as? TelegramMediaFile { + if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { + isImage = true + } else if file.isInstantVideo { + isImage = true + } else if file.isVideo { + isImage = true + } else if file.isSticker || file.isAnimatedSticker { + isImage = true + } else { + isFile = true + } + } else if let _ = media as? TelegramMediaImage { + if !flags.contains(.preferMediaInline) { + isImage = true + } + } else if let _ = media as? TelegramMediaWebFile { + isImage = true + } else if let _ = media as? WallpaperPreviewMedia { + isImage = true + } else if let _ = media as? TelegramMediaStory { + isImage = true + } + } + + if preferMediaBeforeText, let textString, textString.length != 0 { + isImage = false + } + + var statusInText = !isImage + if let textString { + if textString.length == 0 { + statusInText = false + } + } else { + statusInText = false + } + + switch preparePosition { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if let count = webpageGalleryMediaCount { + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil) + skipStandardStatus = isImage + } else if let mediaBadge = mediaBadge { + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil) + } else { + skipStandardStatus = isFile + } + + if !skipStandardStatus { + if incoming { + if isImage { + imageStatusType = .ImageIncoming + } else { + textStatusType = .BubbleIncoming + } + } else { + if message.flags.contains(.Failed) { + if isImage { + imageStatusType = .ImageOutgoing(.Failed) + } else { + textStatusType = .BubbleOutgoing(.Failed) + } + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { + if isImage { + imageStatusType = .ImageOutgoing(.Sending) + } else { + textStatusType = .BubbleOutgoing(.Sending) + } + } else { + if isImage { + imageStatusType = .ImageOutgoing(.Sent(read: messageRead)) + } else { + textStatusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + } + } + default: + break + } + + let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in + ChatMessageDateAndStatus( + type: statusType, + edited: edited, + viewCount: viewCount, + dateReactions: dateReactionsAndPeers.reactions, + dateReactionPeers: dateReactionsAndPeers.peers, + dateReplies: dateReplies, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + dateText: dateText + ) + } + + if let (media, flags) = mediaAndFlags { + if let file = media as? TelegramMediaFile { + if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout + } else if file.isInstantVideo { + let displaySize = CGSize(width: 212.0, height: 212.0) + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) + let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, topMessage: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload, 0.0) + initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight + contentInstantVideoSizeAndApply = (videoLayout, apply) + } else if file.isVideo { + var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none + + if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { + automaticDownload = .full + } else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) { + automaticDownload = .prefetch + } + if file.isAnimated { + automaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif + } else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo { + var willDownloadOrLocal = false + if case .full = automaticDownload { + willDownloadOrLocal = true + } else { + willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil + } + if willDownloadOrLocal { + automaticPlayback = true + contentMode = .aspectFill + } + } + + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout + } else if file.isSticker || file.isAnimatedSticker { + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout + } else { + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) + + let statusType: ChatMessageDateAndStatusType + if incoming { + statusType = .BubbleIncoming + } else { + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + + let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments( + context: context, + presentationData: presentationData, + message: message, + topMessage: message, + associatedData: associatedData, + chatLocation: chatLocation, + attributes: attributes, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + forcedIsEdited: false, + file: file, + automaticDownload: automaticDownload, + incoming: incoming, + isRecentActions: false, + forcedResourceStatus: associatedData.forcedResourceStatus, + dateAndStatusType: statusType, + displayReactions: false, + messageSelection: nil, + layoutConstants: layoutConstants, + constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height), + controllerInteraction: controllerInteraction + )) + refineContentFileLayout = refineLayout + } + } else if let image = media as? TelegramMediaImage { + if !flags.contains(.preferMediaInline) { + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout + } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { + inlineImageDimensions = dimensions.cgSize + + if image != currentImage || !currentMediaIsInline { + updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) + } + } + } else if let image = media as? TelegramMediaWebFile { + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout + } else if let wallpaper = media as? WallpaperPreviewMedia { + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout + if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { + skipStandardStatus = true + } + } else if let story = media as? TelegramMediaStory { + var media: Media? + if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue { + media = item.media + } + + var automaticDownload = false + if let media { + automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media) + } + + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout + } + } + + if let _ = inlineImageDimensions { + inlineImageSize = CGSize(width: 53.0, height: 53.0) + + if let inlineImageSize = inlineImageSize { + textCutout.topRight = CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0) + } + } + + return (initialWidth, { constrainedSize, position in + var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 0.0, right: horizontalInsets.right) + + switch position { + case let .linear(topNeighbor, bottomNeighbor): + switch topNeighbor { + case .None: + insets.top += 10.0 + default: + break + } + switch bottomNeighbor { + case .None: + insets.bottom += 12.0 + default: + insets.bottom += 0.0 + } + default: + break + } + + let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) + + var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? + if let _ = additionalImageBadgeContent { + updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge() + } + + let upatedTextCutout = textCutout + + + let (topTitleLayout, topTitleApply) = topTitleAsyncLayout(TextNodeLayoutArguments(attributedString: topTitleString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets())) + + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? + if statusInText, let textStatusType = textStatusType { + let trailingContentWidth: CGFloat + if textLayout.hasRTL { + trailingContentWidth = 10000.0 + } else { + trailingContentWidth = textLayout.trailingLineWidth + } + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: context, + presentationData: presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: textStatusType, + layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), + constrainedSize: textConstrainedSize, + availableReactions: associatedData.availableReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, + displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser, + replyCount: dateReplies, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message), + animationCache: controllerInteraction.presentationContext.animationCache, + animationRenderer: controllerInteraction.presentationContext.animationRenderer + )) + } + let _ = statusSuggestedWidthAndContinue + + var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) + + textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) + + let mainColor: UIColor + if !incoming { + mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor + } else { + var authorNameColor: UIColor? + let author = message.author + if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { + authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } + if let rawAuthorNameColor = authorNameColor { + var dimColors = false + switch presentationData.theme.theme.name { + case .builtin(.nightAccent), .builtin(.night): + dimColors = true + default: + break + } + if dimColors { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) + authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) + } + } + } + + if let authorNameColor { + mainColor = authorNameColor + } else { + mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor + } + } + + var boundingSize = textFrame.size + if titleBeforeMedia { + boundingSize.height += topTitleLayout.size.height + 4.0 + boundingSize.width = max(boundingSize.width, topTitleLayout.size.width) + } + if let inlineImageSize = inlineImageSize { + if boundingSize.height < inlineImageSize.height { + boundingSize.height = inlineImageSize.height + } + } + + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) + } + + var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))? + if let refineContentImageLayout = refineContentImageLayout { + let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0)) + finalizeContentImageLayout = finalizeImageLayout + + boundingSize.width = max(boundingSize.width, refinedWidth) + } + var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode))? + if let refineContentFileLayout = refineContentFileLayout { + let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize) + finalizeContentFileLayout = finalizeFileLayout + + boundingSize.width = max(boundingSize.width, refinedWidth) + } + + if let (videoLayout, _) = contentInstantVideoSizeAndApply { + boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight) + } + + var imageApply: (() -> Void)? + if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions { + let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor) + imageApply = imageLayout(arguments) + } + + var continueActionButtonLayout: ((CGFloat, CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))? + if let actionTitle = actionTitle, !isPreview { + var buttonIconImage: UIImage? + var buttonHighlightedIconImage: UIImage? + var cornerIcon = false + let titleColor: UIColor + let titleHighlightedColor: UIColor + if incoming { + if let actionIcon { + switch actionIcon { + case .instant: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)! + buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! + case .link: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)! + buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! + cornerIcon = true + } + } + titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor + let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) + titleHighlightedColor = bubbleColor.fill[0] + } else { + if let actionIcon { + switch actionIcon { + case .instant: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)! + buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! + case .link: + buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)! + buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! + cornerIcon = true + } + } + titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor + let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) + titleHighlightedColor = bubbleColor.fill[0] + } + let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, nil, buttonIconImage, buttonHighlightedIconImage, cornerIcon, actionTitle, titleColor, titleHighlightedColor, false) + boundingSize.width = max(buttonWidth, boundingSize.width) + continueActionButtonLayout = continueLayout + } + + boundingSize.width += insets.left + insets.right + boundingSize.height += insets.top + insets.bottom + + return (boundingSize.width, { boundingWidth in + var adjustedBoundingSize = boundingSize + + var imageFrame: CGRect? + if let inlineImageSize = inlineImageSize { + imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right + 4.0, y: 0.0), size: inlineImageSize) + } + + var contentImageSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)? + if let finalizeContentImageLayout = finalizeContentImageLayout { + let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right) + contentImageSizeAndApply = (size, apply) + + var imageHeightAddition = size.height + if textFrame.size.height > CGFloat.ulpOfOne { + imageHeightAddition += 2.0 + } + + adjustedBoundingSize.height += imageHeightAddition + 7.0 + } + + var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)? + if let finalizeContentFileLayout = finalizeContentFileLayout { + let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right) + contentFileSizeAndApply = (size, apply) + + var imageHeightAddition = size.height + 6.0 + if textFrame.size.height > CGFloat.ulpOfOne { + imageHeightAddition += 6.0 + } else { + imageHeightAddition += 7.0 + } + + adjustedBoundingSize.height += imageHeightAddition + 5.0 + } + + if let (videoLayout, _) = contentInstantVideoSizeAndApply { + let imageHeightAddition = videoLayout.contentSize.height + 6.0 + + adjustedBoundingSize.height += imageHeightAddition// + 5.0 + } + + var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))? + if let continueActionButtonLayout = continueActionButtonLayout { + let (size, apply) = continueActionButtonLayout(boundingWidth - 5.0 - insets.right, 38.0) + actionButtonSizeAndApply = (size, apply) + adjustedBoundingSize.height += 4.0 + size.height + if let text, !text.isEmpty { + if contentImageSizeAndApply == nil { + adjustedBoundingSize.height += 5.0 + } else if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + adjustedBoundingSize.height += 5.0 + } + } + } + + var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)? + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right) + } + if let statusSizeAndApply = statusSizeAndApply { + adjustedBoundingSize.height += statusSizeAndApply.0.height + + if let imageFrame = imageFrame, statusSizeAndApply.0.height == 0.0 { + if statusInText { + adjustedBoundingSize.height = max(adjustedBoundingSize.height, imageFrame.maxY + 8.0 + 15.0) + } + } + } + + adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width) + + var contentMediaHeight: CGFloat? + if let (contentImageSize, _) = contentImageSizeAndApply { + contentMediaHeight = contentImageSize.height + } + + if let (contentFileSize, _) = contentFileSizeAndApply { + contentMediaHeight = contentFileSize.height + } + + if let (videoLayout, _) = contentInstantVideoSizeAndApply { + contentMediaHeight = videoLayout.contentSize.height + } + + var textVerticalOffset: CGFloat = 0.0 + if titleBeforeMedia { + textVerticalOffset += topTitleLayout.size.height + 4.0 + } + if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + textVerticalOffset += contentMediaHeight + 7.0 + } + let adjustedTextFrame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) + + var statusFrame: CGRect? + if let statusSizeAndApply = statusSizeAndApply { + var finalStatusFrame = CGRect(origin: CGPoint(x: adjustedTextFrame.minX, y: adjustedTextFrame.maxY), size: statusSizeAndApply.0) + if let imageFrame = imageFrame { + if finalStatusFrame.maxY < imageFrame.maxY + 10.0 { + finalStatusFrame.origin.y = max(finalStatusFrame.minY, imageFrame.maxY + 2.0) + if finalStatusFrame.height == 0.0 { + finalStatusFrame.origin.y += 14.0 + + adjustedBoundingSize.height += 14.0 + } + } + } + statusFrame = finalStatusFrame + } + + return (adjustedBoundingSize, { [weak self] animation, synchronousLoads, applyInfo in + if let strongSelf = self { + strongSelf.context = context + strongSelf.message = message + strongSelf.media = mediaAndFlags?.0 + strongSelf.theme = presentationData.theme + + let backgroundView: UIImageView + if let current = strongSelf.backgroundView { + backgroundView = current + } else { + backgroundView = UIImageView() + strongSelf.backgroundView = backgroundView + strongSelf.view.insertSubview(backgroundView, at: 0) + } + + if backgroundView.image == nil { + backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme) + } + backgroundView.tintColor = mainColor + + animation.animator.updateFrame(layer: backgroundView.layer, frame: CGRect(origin: CGPoint(x: 11.0, y: insets.top - 3.0), size: CGSize(width: adjustedBoundingSize.width - 4.0 - insets.right, height: adjustedBoundingSize.height - insets.top - insets.bottom + 4.0)), completion: nil) + backgroundView.isHidden = !displayLine + + //strongSelf.borderColor = UIColor.red.cgColor + //strongSelf.borderWidth = 2.0 + + strongSelf.textNode.textNode.displaysAsynchronously = !isPreview + + let _ = topTitleApply() + strongSelf.topTitleNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: insets.top), size: topTitleLayout.size) + + let _ = textApply(TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: messageTheme.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads + )) + switch strongSelf.visibility { + case .none: + strongSelf.textNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + strongSelf.textNode.visibilityRect = subRect + } + + if let imageFrame = imageFrame { + if let updateImageSignal = updateInlineImageSignal { + strongSelf.inlineImageNode.setSignal(updateImageSignal) + } + animation.animator.updateFrame(layer: strongSelf.inlineImageNode.layer, frame: imageFrame, completion: nil) + if strongSelf.inlineImageNode.supernode == nil { + strongSelf.addSubnode(strongSelf.inlineImageNode) + } + + if let imageApply = imageApply { + imageApply() + } + } else if strongSelf.inlineImageNode.supernode != nil { + strongSelf.inlineImageNode.removeFromSupernode() + } + + if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { + let contentImageNode = contentImageApply(animation, synchronousLoads) + if strongSelf.contentImageNode !== contentImageNode { + strongSelf.contentImageNode = contentImageNode + contentImageNode.activatePinch = { sourceNode in + controllerInteraction.activateMessagePinch(sourceNode) + } + strongSelf.addSubnode(contentImageNode) + contentImageNode.activateLocalContent = { [weak strongSelf] mode in + if let strongSelf = strongSelf { + strongSelf.openMedia?(mode) + } + } + contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in + guard let controllerInteraction = controllerInteraction else { + return + } + controllerInteraction.updateMessageReaction(message, value) + } + contentImageNode.visibility = strongSelf.visibility != .none + } + let _ = contentImageApply(animation, synchronousLoads) + var contentImageFrame: CGRect + if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) + if titleBeforeMedia { + contentImageFrame.origin.y += topTitleLayout.size.height + 4.0 + } + } else { + contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize) + } + + contentImageNode.frame = contentImageFrame + } else if let contentImageNode = strongSelf.contentImageNode { + contentImageNode.visibility = false + contentImageNode.removeFromSupernode() + strongSelf.contentImageNode = nil + } + + if let updatedAdditionalImageBadge = updatedAdditionalImageBadge, let contentImageNode = strongSelf.contentImageNode, let contentImageSize = contentImageSizeAndApply?.0 { + if strongSelf.additionalImageBadgeNode != updatedAdditionalImageBadge { + strongSelf.additionalImageBadgeNode?.removeFromSupernode() + } + strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge + contentImageNode.addSubnode(updatedAdditionalImageBadge) + if mediaBadge != nil { + updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, animated: false) + updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 0.0, height: 0.0)) + } else { + updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, alignment: .right, animated: false) + updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 6.0, y: contentImageSize.height - 18.0 - 6.0), size: CGSize(width: 0.0, height: 0.0)) + } + } else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode { + strongSelf.additionalImageBadgeNode = nil + additionalImageBadgeNode.removeFromSupernode() + } + + if let (contentFileSize, contentFileApply) = contentFileSizeAndApply { + let contentFileNode = contentFileApply(synchronousLoads, animation, applyInfo) + if strongSelf.contentFileNode !== contentFileNode { + strongSelf.contentFileNode = contentFileNode + strongSelf.addSubnode(contentFileNode) + contentFileNode.activateLocalContent = { [weak strongSelf] in + if let strongSelf = strongSelf { + strongSelf.openMedia?(.default) + } + } + contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in + if let strongSelf = strongSelf { + strongSelf.requestUpdateLayout?() + } + } + } + if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize) + } else { + contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 8.0 : 7.0)), size: contentFileSize) + } + } else if let contentFileNode = strongSelf.contentFileNode { + contentFileNode.removeFromSupernode() + strongSelf.contentFileNode = nil + } + + if let (videoLayout, apply) = contentInstantVideoSizeAndApply { + let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation) + if strongSelf.contentInstantVideoNode !== contentInstantVideoNode { + strongSelf.contentInstantVideoNode = contentInstantVideoNode + strongSelf.addSubnode(contentInstantVideoNode) + } + if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: videoLayout.contentSize) + } else { + contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: videoLayout.contentSize) + } + } else if let contentInstantVideoNode = strongSelf.contentInstantVideoNode { + contentInstantVideoNode.removeFromSupernode() + strongSelf.contentInstantVideoNode = nil + } + + strongSelf.textNode.textNode.frame = adjustedTextFrame + if let statusSizeAndApply = statusSizeAndApply, let statusFrame = statusFrame { + if strongSelf.statusNode.supernode == nil { + strongSelf.addSubnode(strongSelf.statusNode) + strongSelf.statusNode.frame = statusFrame + statusSizeAndApply.1(.None) + } else { + animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil) + statusSizeAndApply.1(animation) + } + } else if strongSelf.statusNode.supernode != nil { + strongSelf.statusNode.removeFromSupernode() + } + + if let (size, apply) = actionButtonSizeAndApply { + let buttonNode = apply() + + let buttonFrame = CGRect(origin: CGPoint(x: 12.0, y: adjustedBoundingSize.height - insets.bottom - size.height), size: size) + if buttonNode !== strongSelf.buttonNode { + strongSelf.buttonNode?.removeFromSupernode() + strongSelf.buttonNode = buttonNode + buttonNode.isUserInteractionEnabled = false + strongSelf.addSubnode(buttonNode) + buttonNode.pressed = { + if let strongSelf = self { + strongSelf.activateAction?() + } + } + buttonNode.frame = buttonFrame + } else { + animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil) + } + + let buttonSeparatorFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + 8.0, y: buttonFrame.minY - 2.0), size: CGSize(width: buttonFrame.width - 8.0 - 8.0, height: UIScreenPixel)) + + let buttonSeparatorLayer: SimpleLayer + if let current = strongSelf.buttonSeparatorLayer { + buttonSeparatorLayer = current + animation.animator.updateFrame(layer: buttonSeparatorLayer, frame: buttonSeparatorFrame, completion: nil) + } else { + buttonSeparatorLayer = SimpleLayer() + strongSelf.buttonSeparatorLayer = buttonSeparatorLayer + strongSelf.layer.addSublayer(buttonSeparatorLayer) + buttonSeparatorLayer.frame = buttonSeparatorFrame + } + + buttonSeparatorLayer.backgroundColor = mainColor.withMultipliedAlpha(0.5).cgColor + } else { + if let buttonNode = strongSelf.buttonNode { + strongSelf.buttonNode = nil + buttonNode.removeFromSupernode() + } + + if let buttonSeparatorLayer = strongSelf.buttonSeparatorLayer { + strongSelf.buttonSeparatorLayer = nil + buttonSeparatorLayer.removeFromSuperlayer() + } + } + } + }) + }) + })*/ + } + } + + public func updateHiddenMedia(_ media: [Media]?) -> Bool { + if let currentMedia = self.media { + if let media = media { + var found = false + for m in media { + if currentMedia.isEqual(to: m) { + found = true + break + } + } + if let contentImageNode = self.contentMedia { + contentImageNode.isHidden = found + contentImageNode.updateIsHidden(found) + return found + } + } else if let contentImageNode = self.contentMedia { + contentImageNode.isHidden = false + contentImageNode.updateIsHidden(false) + } + } + return false + } + + public func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + if let contentImageNode = self.contentMedia, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) { + return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in + return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) + }) + } else if let contentImageNode = self.contentMedia, let file = self.media as? TelegramMediaFile, file.isEqual(to: media) { + return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in + return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) + }) + } else if let contentImageNode = self.contentMedia, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) { + return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in + return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) + }) + } + return nil + } + + public func hasActionAtPoint(_ point: CGPoint) -> Bool { + if let buttonNode = self.actionButton, buttonNode.frame.contains(point) { + return true + } + return false + } + + public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if let text = self.text { + let textNodeFrame = text.textNode.frame + if let (index, attributes) = text.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = text.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) + } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) + } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) + } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) + } + } + } + + if let actionButton = self.actionButton, actionButton.frame.contains(point) { + return ChatMessageBubbleContentTapAction(content: .ignore) + } + + if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) { + return self.defaultContentAction() + } else { + return .init(content: .none) + } + } + + public func updateTouchesAtPoint(_ point: CGPoint?) { + guard let context = self.context, let message = self.message, let theme = self.theme else { + return + } + var rects: [CGRect]? + if let point = point { + if let text = self.text { + let textNodeFrame = text.textNode.frame + if let (index, attributes) = text.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag, + TelegramTextAttributes.BankCard + ] + for name in possibleNames { + if let _ = attributes[NSAttributedString.Key(rawValue: name)] { + rects = text.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + } + + if let rects = rects, let text = self.text { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor) + self.linkHighlightingNode = linkHighlightingNode + self.transformContainer.insertSubnode(linkHighlightingNode, belowSubnode: text.textNode) + } + linkHighlightingNode.frame = text.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + + var isHighlighted = false + if rects == nil, let point { + if let actionButton = self.actionButton, actionButton.frame.contains(point) { + } else if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) { + isHighlighted = true + } + } + + if self.isHighlighted != isHighlighted { + self.isHighlighted = isHighlighted + + if isHighlighted { + /*self.highlightTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false, block: { [weak self] timer in + guard let self else { + return + } + if self.highlightTimer === timer { + self.highlightTimer = nil + } + self.applyIsHighlighted() + })*/ + self.applyIsHighlighted() + } else { + self.applyIsHighlighted() + } + } + } + + private func applyIsHighlighted() { + if let highlightTimer = self.highlightTimer { + self.highlightTimer = nil + highlightTimer.invalidate() + } + + let transition: ContainedViewLayoutTransition = .animated(duration: self.isHighlighted ? 0.3 : 0.2, curve: .easeInOut) + let scale: CGFloat = self.isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0 + transition.updateSublayerTransformScale(node: self.transformContainer, scale: scale, beginWithCurrentState: true) + } + + public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + if let statusNode = self.statusNode, !statusNode.isHidden { + if let result = statusNode.reactionView(value: value) { + return result + } + } + if let result = self.contentFile?.dateAndStatusNode.reactionView(value: value) { + return result + } + if let result = self.contentMedia?.dateAndStatusNode.reactionView(value: value) { + return result + } + if let result = self.contentInstantVideo?.dateAndStatusNode.reactionView(value: value) { + return result + } + return nil + } + + public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + return self.contentMedia?.playMediaWithSound() + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD new file mode 100644 index 00000000000..56a646c36b8 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageBubbleContentNode", + module_name = "ChatMessageBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ChatMessageBackground", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift similarity index 96% rename from submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift index 63bd5c68c56..77bf4fe086f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift @@ -2,8 +2,9 @@ import Foundation import UIKit import Display import TelegramPresentationData +import ChatMessageItemCommon -func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat, layoutConstants: ChatMessageItemLayoutConstants, chatPresentationData: ChatPresentationData) -> ImageCorners { +public func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat, layoutConstants: ChatMessageItemLayoutConstants, chatPresentationData: ChatPresentationData) -> ImageCorners { let topLeftCorner: ImageCorner let topRightCorner: ImageCorner diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift new file mode 100644 index 00000000000..59a6d7a543b --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -0,0 +1,305 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramUIPreferences +import TelegramPresentationData +import AccountContext +import ChatMessageBackground +import ChatControllerInteraction +import ChatHistoryEntry +import ChatMessageItemCommon +import SwiftSignalKit + +public enum ChatMessageBubbleContentBackgroundHiding { + case never + case emptyWallpaper + case always +} + +public enum ChatMessageBubbleContentAlignment { + case none + case center +} + +public struct ChatMessageBubbleContentProperties { + public let hidesSimpleAuthorHeader: Bool + public let headerSpacing: CGFloat + public let hidesBackground: ChatMessageBubbleContentBackgroundHiding + public let forceFullCorners: Bool + public let forceAlignment: ChatMessageBubbleContentAlignment + public let shareButtonOffset: CGPoint? + public let hidesHeaders: Bool + public let avatarOffset: CGFloat? + public let isDetached: Bool + + public init( + hidesSimpleAuthorHeader: Bool, + headerSpacing: CGFloat, + hidesBackground: ChatMessageBubbleContentBackgroundHiding, + forceFullCorners: Bool, + forceAlignment: ChatMessageBubbleContentAlignment, + shareButtonOffset: CGPoint? = nil, + hidesHeaders: Bool = false, + avatarOffset: CGFloat? = nil, + isDetached: Bool = false + ) { + self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader + self.headerSpacing = headerSpacing + self.hidesBackground = hidesBackground + self.forceFullCorners = forceFullCorners + self.forceAlignment = forceAlignment + self.shareButtonOffset = shareButtonOffset + self.hidesHeaders = hidesHeaders + self.avatarOffset = avatarOffset + self.isDetached = isDetached + } +} + +public enum ChatMessageBubbleNoneMergeStatus { + case Incoming + case Outgoing + case None +} + +public enum ChatMessageBubbleMergeStatus { + case None(ChatMessageBubbleNoneMergeStatus) + case Left + case Right + case Both +} + +public enum ChatMessageBubbleRelativePosition { + public enum NeighbourType { + case media + case header + case footer + case text + case reactions + } + + public enum NeighbourSpacing { + case `default` + case condensed + case overlap(CGFloat) + } + + case None(ChatMessageBubbleMergeStatus) + case BubbleNeighbour + case Neighbour(Bool, NeighbourType, NeighbourSpacing) +} + +public enum ChatMessageBubbleContentMosaicNeighbor { + case merged + case mergedBubble + case none(tail: Bool) +} + +public struct ChatMessageBubbleContentMosaicPosition { + public let topLeft: ChatMessageBubbleContentMosaicNeighbor + public let topRight: ChatMessageBubbleContentMosaicNeighbor + public let bottomLeft: ChatMessageBubbleContentMosaicNeighbor + public let bottomRight: ChatMessageBubbleContentMosaicNeighbor + + public init(topLeft: ChatMessageBubbleContentMosaicNeighbor, topRight: ChatMessageBubbleContentMosaicNeighbor, bottomLeft: ChatMessageBubbleContentMosaicNeighbor, bottomRight: ChatMessageBubbleContentMosaicNeighbor) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } +} + +public enum ChatMessageBubbleContentPosition { + case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) + case mosaic(position: ChatMessageBubbleContentMosaicPosition, wide: Bool) +} + +public enum ChatMessageBubblePreparePosition { + case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) + case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) +} + +public struct ChatMessageBubbleContentTapAction { + public struct Url { + public var url: String + public var concealed: Bool + public var allowInlineWebpageResolution: Bool + + public init( + url: String, + concealed: Bool, + allowInlineWebpageResolution: Bool = false + ) { + self.url = url + self.concealed = concealed + self.allowInlineWebpageResolution = allowInlineWebpageResolution + } + } + + public enum Content { + case none + case url(Url) + case textMention(String) + case peerMention(peerId: PeerId, mention: String, openProfile: Bool) + case botCommand(String) + case hashtag(String?, String) + case instantPage + case wallpaper + case theme + case call(peerId: PeerId, isVideo: Bool) + case openMessage + case timecode(Double, String) + case tooltip(String, ASDisplayNode?, CGRect?) + case bankCard(String) + case ignore + case openPollResults(Data) + case copy(String) + case largeEmoji(String, String?, TelegramMediaFile) + case customEmoji(TelegramMediaFile) + } + + public var content: Content + public var hasLongTapAction: Bool + public var activate: (() -> Promise?)? + + public init(content: Content, hasLongTapAction: Bool = true, activate: (() -> Promise?)? = nil) { + self.content = content + self.hasLongTapAction = hasLongTapAction + self.activate = activate + } +} + +public final class ChatMessageBubbleContentItem { + public let context: AccountContext + public let controllerInteraction: ChatControllerInteraction + public let message: Message + public let topMessage: Message + public let read: Bool + public let chatLocation: ChatLocation + public let presentationData: ChatPresentationData + public let associatedData: ChatMessageItemAssociatedData + public let attributes: ChatMessageEntryAttributes + public let isItemPinned: Bool + public let isItemEdited: Bool + + public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) { + self.context = context + self.controllerInteraction = controllerInteraction + self.message = message + self.topMessage = topMessage + self.read = read + self.chatLocation = chatLocation + self.presentationData = presentationData + self.associatedData = associatedData + self.attributes = attributes + self.isItemPinned = isItemPinned + self.isItemEdited = isItemEdited + } +} + +open class ChatMessageBubbleContentNode: ASDisplayNode { + open var supportsMosaic: Bool { + return false + } + + public weak var itemNode: ChatMessageItemNodeProtocol? + public weak var bubbleBackgroundNode: ChatMessageBackground? + public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop? + + open var visibility: ListViewItemNodeVisibility = .none + + public var item: ChatMessageBubbleContentItem? + + public var updateIsTextSelectionActive: ((Bool) -> Void)? + + open var disablesClipping: Bool { + return false + } + + required public override init() { + super.init() + } + + open func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + preconditionFailure() + } + + open func animateInsertion(_ currentTimestamp: Double, duration: Double) { + } + + open func animateAdded(_ currentTimestamp: Double, duration: Double) { + } + + open func animateRemoved(_ currentTimestamp: Double, duration: Double) { + } + + open func animateInsertionIntoBubble(_ duration: Double) { + } + + open func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + open func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + open func updateHiddenMedia(_ media: [Media]?) -> Bool { + return false + } + + open func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { + } + + open func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) { + } + + open func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + return nil + } + + open func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + return ChatMessageBubbleContentTapAction(content: .none) + } + + open func updateTouchesAtPoint(_ point: CGPoint?) { + } + + open func updateHighlightedState(animated: Bool) -> Bool { + return false + } + + open func willUpdateIsExtractedToContextPreview(_ value: Bool) { + } + + open func updateIsExtractedToContextPreview(_ value: Bool) { + } + + open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + } + + open func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + } + + open func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + } + + open func unreadMessageRangeUpdated() { + } + + open func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + return nil + } + + open func targetForStoryTransition(id: StoryId) -> UIView? { + return nil + } + + open func getStatusNode() -> ASDisplayNode? { + return nil + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD new file mode 100644 index 00000000000..5b8b444a8d7 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -0,0 +1,86 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageBubbleItemNode", + module_name = "ChatMessageBubbleItemNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/TemporaryCachedPeerDataManager", + "//submodules/LocalizedPeerData", + "//submodules/ContextUI", + "//submodules/TelegramUniversalVideoContent", + "//submodules/MosaicLayout", + "//submodules/TextSelectionNode", + "//submodules/PlatformRestrictionMatching", + "//submodules/Emoji", + "//submodules/PersistentStringHash", + "//submodules/GridMessageSelectionNode", + "//submodules/AppBundle", + "//submodules/Markdown", + "//submodules/WallpaperBackgroundNode", + "//submodules/ChatPresentationInterfaceState", + "//submodules/ChatMessageBackground", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageShareButton", + "//submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode", + "//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer", + "//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift similarity index 82% rename from submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 46c140fa747..a20a5a21705 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -30,7 +30,6 @@ import NGTranslate import NGStrings import NGUI import NGWebUtils -import SettingsUI import TelegramCore import NaturalLanguage // @@ -50,12 +49,45 @@ import ComponentFlow import EmojiStatusComponent import ChatControllerInteraction import ChatMessageForwardInfoNode - -enum InternalBubbleTapAction { - case action(() -> Void) - case optionalAction(() -> Void) - case openContextMenu(tapMessage: Message, selectAll: Bool, subFrame: CGRect) -} +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatHistoryEntry +import ChatMessageTextBubbleContentNode +import ChatMessageItemCommon +import ChatMessageReplyInfoNode +import ChatMessageCallBubbleContentNode +import ChatMessageInteractiveFileNode +import ChatMessageFileBubbleContentNode +import ChatMessageWebpageBubbleContentNode +import ChatMessagePollBubbleContentNode +import ChatMessageItem +import ChatMessageItemView +import ChatMessageSwipeToReplyNode +import ChatMessageSelectionNode +import ChatMessageDeliveryFailedNode +import ChatMessageShareButton +import ChatMessageThreadInfoNode +import ChatMessageActionButtonsNode +import ChatSwipeToReplyRecognizer +import ChatMessageReactionsFooterContentNode +import ChatMessageInstantVideoBubbleContentNode +import ChatMessageCommentFooterContentNode +import ChatMessageActionBubbleContentNode +import ChatMessageContactBubbleContentNode +import ChatMessageEventLogPreviousDescriptionContentNode +import ChatMessageEventLogPreviousLinkContentNode +import ChatMessageEventLogPreviousMessageContentNode +import ChatMessageGameBubbleContentNode +import ChatMessageInvoiceBubbleContentNode +import ChatMessageMapBubbleContentNode +import ChatMessageMediaBubbleContentNode +import ChatMessageProfilePhotoSuggestionContentNode +import ChatMessageRestrictedBubbleContentNode +import ChatMessageStoryMentionContentNode +import ChatMessageUnsupportedBubbleContentNode +import ChatMessageWallpaperBubbleContentNode +import ChatMessageGiftBubbleContentNode +import ChatMessageGiveawayBubbleContentNode private struct BubbleItemAttributes { var isAttachment: Bool @@ -74,50 +106,6 @@ private final class ChatMessageBubbleClippingNode: ASDisplayNode { } } -func hasCommentButton(item: ChatMessageItem) -> Bool { - let firstMessage = item.content.firstMessage - - var hasDiscussion = false - if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { - hasDiscussion = true - } - if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == firstMessage.id { - hasDiscussion = false - } - - if firstMessage.adAttribute != nil { - hasDiscussion = false - } - - if hasDiscussion { - var canComment = false - if case .pinnedMessages = item.associatedData.subject { - canComment = false - } else if firstMessage.id.namespace == Namespaces.Message.Local { - canComment = true - } else { - for attribute in firstMessage.attributes { - if let attribute = attribute as? ReplyThreadMessageAttribute, let commentsPeerId = attribute.commentsPeerId { - switch item.associatedData.channelDiscussionGroup { - case .unknown: - canComment = true - case let .known(groupId): - canComment = groupId == commentsPeerId - } - break - } - } - } - - if canComment { - return true - } - } else if firstMessage.id.peerId.isReplies { - return true - } - return false -} - private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], Bool, Bool) { var result: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)] = [] var skipText = false @@ -139,7 +127,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ // MARK: Nicegram if isAllowedMessage(restrictionReason: attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, extractReason: true), contentSettings: item.context.currentContentSettings.with { $0 }) { } else { - result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break outer } @@ -156,9 +144,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else if let story = media as? TelegramMediaStory { if story.isMention { if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty { - result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { - result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } } else { var hideStory = false @@ -189,7 +177,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if isVideo { if file.isInstantVideo { hasSeparateCommentsButton = true - result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty { messageWithCaptionToAdd = (message, itemAttributes) @@ -203,28 +191,30 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } isFile = true hasFiles = true - result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing))) + result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: neighborSpacing))) needReactions = false } } else if let action = media as? TelegramMediaAction { isAction = true if case .phoneCall = action.action { - result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .giftPremium = action.action { - result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .suggestedProfilePhoto = action.action { - result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .setChatWallpaper = action.action { - result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else if case .giftCode = action.action { + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { - result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } needReactions = false } else if let _ = media as? TelegramMediaMap { - result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } else if let _ = media as? TelegramMediaGame { skipText = true - result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break inner } else if let invoice = media as? TelegramMediaInvoice { @@ -232,20 +222,23 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } else { skipText = true - result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } needReactions = false break inner } else if let _ = media as? TelegramMediaContact { - result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } else if let _ = media as? TelegramMediaExpiredContent { result.removeAll() - result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false return (result, false, false) } else if let _ = media as? TelegramMediaPoll { - result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + needReactions = false + } 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? TelegramMediaUnsupported { isUnsupportedMedia = true @@ -265,7 +258,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ messageWithCaptionToAdd = (message, itemAttributes) skipText = true } else { - result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: isFile ? .condensed : .default))) + result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) needReactions = false } } else { @@ -285,7 +278,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } } - result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview { + result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + } else { + result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } needReactions = false } break inner @@ -295,31 +292,31 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if message.adAttribute != nil { result.removeAll() - result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } if isUnsupportedMedia { - result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } } if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd { - result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } if let additionalContent = item.additionalContent { switch additionalContent { case let .eventLogPreviousMessage(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false case let .eventLogPreviousDescription(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false case let .eventLogPreviousLink(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false } } @@ -334,26 +331,26 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) { if hasCommentButton(item: item) { - result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) + 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 result.last?.1 == ChatMessageTextBubbleContentNode.self { } else { - if result.last?.1 == ChatMessageWebpageBubbleContentNode.self || - result.last?.1 == ChatMessagePollBubbleContentNode.self || + if result.last?.1 == ChatMessagePollBubbleContentNode.self || result.last?.1 == ChatMessageContactBubbleContentNode.self || result.last?.1 == ChatMessageGameBubbleContentNode.self || - result.last?.1 == ChatMessageInvoiceBubbleContentNode.self { - result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) + result.last?.1 == ChatMessageInvoiceBubbleContentNode.self || + result.last?.1 == ChatMessageGiveawayBubbleContentNode.self { + result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .reactions, neighborSpacing: .default))) needReactions = false } else if result.last?.1 == ChatMessageCommentFooterContentNode.self { if result.count >= 2 { - if result[result.count - 2].1 == ChatMessageWebpageBubbleContentNode.self || - result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self || - result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self { - result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) + if result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self || + result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self || + result[result.count - 2].1 == ChatMessageGiveawayBubbleContentNode.self { + result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .reactions, neighborSpacing: .default)), at: result.count - 1) } } } @@ -369,16 +366,6 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ return (result, needSeparateContainers, needReactions) } -let chatMessagePeerIdColors: [UIColor] = [ - UIColor(rgb: 0xfc5c51), - UIColor(rgb: 0xfa790f), - UIColor(rgb: 0x895dd5), - UIColor(rgb: 0x0fb297), - UIColor(rgb: 0x00c0c2), - UIColor(rgb: 0x3ca5ec), - UIColor(rgb: 0x3d72ed) -] - private enum ContentNodeOperation { case remove(index: Int) case insert(index: Int, node: ChatMessageBubbleContentNode) @@ -405,18 +392,18 @@ private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize: } } -class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode { - class ContentContainer { - let contentMessageStableId: UInt32 - let sourceNode: ContextExtractedContentContainingNode - let containerNode: ContextControllerSourceNode - var backgroundWallpaperNode: ChatMessageBubbleBackdrop? - var backgroundNode: ChatMessageBackground? - var selectionBackgroundNode: ASDisplayNode? +public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode { + public class ContentContainer { + public let contentMessageStableId: UInt32 + public let sourceNode: ContextExtractedContentContainingNode + public let containerNode: ContextControllerSourceNode + public var backgroundWallpaperNode: ChatMessageBubbleBackdrop? + public var backgroundNode: ChatMessageBackground? + public var selectionBackgroundNode: ASDisplayNode? private var currentParams: (size: CGSize, contentOrigin: CGPoint, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, presentationContext: ChatPresentationContext, mediaBox: MediaBox, messageSelection: Bool?, selectionInsets: UIEdgeInsets)? - init(contentMessageStableId: UInt32) { + public init(contentMessageStableId: UInt32) { self.contentMessageStableId = contentMessageStableId self.sourceNode = ContextExtractedContentContainingNode() @@ -553,14 +540,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - let mainContextSourceNode: ContextExtractedContentContainingNode + public let mainContextSourceNode: ContextExtractedContentContainingNode private let mainContainerNode: ContextControllerSourceNode private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground + private var backgroundHighlightNode: ChatMessageBackground? private let shadowNode: ChatMessageShadowNode private var clippingNode: ChatMessageBubbleClippingNode - override var extractedBackgroundNode: ASDisplayNode? { + override public var extractedBackgroundNode: ASDisplayNode? { return self.shadowNode } @@ -573,8 +561,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private var adminBadgeNode: TextNode? private var credibilityIconView: ComponentHostView? private var credibilityIconComponent: EmojiStatusComponent? + private var closeButtonNode: HighlightTrackingButtonNode? + private var closeIconNode: ASImageNode? + private var forwardInfoNode: ChatMessageForwardInfoNode? - var forwardInfoReferenceNode: ASDisplayNode? { + public var forwardInfoReferenceNode: ASDisplayNode? { return self.forwardInfoNode } @@ -583,7 +574,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private var contentContainersWrapperNode: ASDisplayNode private var contentContainers: [ContentContainer] = [] - private(set) var contentNodes: [ChatMessageBubbleContentNode] = [] + public private(set) var contentNodes: [ChatMessageBubbleContentNode] = [] private var mosaicStatusNode: ChatMessageDateAndStatusNode? private var actionButtonsNode: ChatMessageActionButtonsNode? private var reactionButtonsNode: ChatMessageReactionButtonsNode? @@ -594,7 +585,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private let messageAccessibilityArea: AccessibilityAreaNode private var backgroundType: ChatMessageBackgroundType? - private var highlightedState: Bool = false + + private struct HighlightedState: Equatable { + var quote: String? + } + private var highlightedState: HighlightedState? private var backgroundFrameTransition: (CGRect, CGRect)? @@ -604,13 +599,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private var appliedForwardInfo: (Peer?, String?)? private var disablesComments = true + private var authorNameColor: UIColor? + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + private var replyRecognizer: ChatSwipeToReplyRecognizer? private var currentSwipeAction: ChatControllerInteractionSwipeAction? //private let debugNode: ASDisplayNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { for contentNode in self.contentNodes { @@ -645,7 +643,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - required init() { + required public init() { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() @@ -667,30 +665,40 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode //self.addSubnode(self.debugNode) - self.mainContainerNode.shouldBegin = { [weak self] location in + self.mainContainerNode.shouldBeginWithCustomActivationProcess = { [weak self] location in guard let strongSelf = self else { - return false + return .none } if !strongSelf.backgroundNode.frame.contains(location) { - return false + return .none } if strongSelf.selectionNode != nil { - return false + return .none } if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { - if case .action = action { - return false + if case let .action(action) = action, !action.contextMenuOnLongPress { + return .none } } if let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { switch action { - case .action, .optionalAction: - return false - case let .openContextMenu(_, selectAll, _): - return selectAll || strongSelf.contentContainers.count < 2 + case .action: + return .none + case .optionalAction: + return .none + case let .openContextMenu(openContextMenu): + if openContextMenu.selectAll || strongSelf.contentContainers.count < 2 { + if openContextMenu.disableDefaultPressAnimation { + return .customActivationProcess + } else { + return .default + } + } else { + return .none + } } } - return true + return .default } self.mainContainerNode.activated = { [weak self] gesture, location in @@ -702,9 +710,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode switch action { case .action, .optionalAction: break - case let .openContextMenu(tapMessage, selectAll, subFrame): - var tapMessage = tapMessage - if selectAll, case let .group(messages) = item.content, tapMessage.text.isEmpty { + case let .openContextMenu(openContextMenu): + var tapMessage = openContextMenu.tapMessage + if openContextMenu.selectAll, case let .group(messages) = item.content, tapMessage.text.isEmpty { for message in messages { if !message.0.text.isEmpty { tapMessage = message.0 @@ -712,7 +720,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } } - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, strongSelf, subFrame, gesture, nil) + item.controllerInteraction.openMessageContextMenu(tapMessage, openContextMenu.selectAll, strongSelf, openContextMenu.subFrame, gesture, nil) } } } @@ -739,7 +747,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } if let singleUrl = accessibilityData.singleUrl { - strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage) + strongSelf.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: singleUrl, concealed: false, external: false, message: strongSelf.item?.content.firstMessage)) return true } @@ -793,11 +801,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func cancelInsertionAnimations() { + override public func cancelInsertionAnimations() { self.shadowNode.layer.removeAllAnimations() func process(node: ASDisplayNode) { @@ -831,7 +839,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode process(node: self) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -866,7 +874,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode process(node: self) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) self.allowsGroupOpacity = true @@ -878,7 +886,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.bounds.width / 2.0 - self.backgroundNode.frame.midX, y: self.backgroundNode.frame.midY), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) if let subnodes = self.subnodes { @@ -892,7 +900,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + public func animateFromLoadingPlaceholder(delay: Double, transition: ContainedViewLayoutTransition) { guard let item = self.item else { return } @@ -901,8 +909,22 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode transition.animatePositionAdditive(node: self, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0), delay: delay) transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) } + + public final class AnimationTransitionTextInput { + let backgroundView: UIView + let contentView: UIView + let sourceRect: CGRect + let scrollOffset: CGFloat - func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { + public init(backgroundView: UIView, contentView: UIView, sourceRect: CGRect, scrollOffset: CGFloat) { + self.backgroundView = backgroundView + self.contentView = contentView + self.sourceRect = sourceRect + self.scrollOffset = scrollOffset + } + } + + public func animateContentFromTextInputField(textInput: AnimationTransitionTextInput, transition: CombinedTransition) { let widthDifference = self.backgroundNode.frame.width - textInput.backgroundView.frame.width let heightDifference = self.backgroundNode.frame.height - textInput.backgroundView.frame.height @@ -934,15 +956,41 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } } + + public final class AnimationTransitionReplyPanel { + public let titleNode: ASDisplayNode + public let textNode: ASDisplayNode + public let lineNode: ASDisplayNode + public let imageNode: ASDisplayNode + public let relativeSourceRect: CGRect + public let relativeTargetRect: CGRect + + public init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) { + self.titleNode = titleNode + self.textNode = textNode + self.lineNode = lineNode + self.imageNode = imageNode + self.relativeSourceRect = relativeSourceRect + self.relativeTargetRect = relativeTargetRect + } + } - func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { + public func animateReplyPanel(sourceReplyPanel: AnimationTransitionReplyPanel, transition: CombinedTransition) { if let replyInfoNode = self.replyInfoNode { let localRect = self.mainContextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) - let _ = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, unclippedTransitionNode: self.mainContextSourceNode.contentNode, localRect: localRect, transition: transition) + let mappedPanel = ChatMessageReplyInfoNode.TransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ) + let _ = replyInfoNode.animateFromInputPanel(sourceReplyPanel: mappedPanel, unclippedTransitionNode: self.mainContextSourceNode.contentNode, localRect: localRect, transition: transition) } } - func animateFromMicInput(micInputNode: UIView, transition: CombinedTransition) -> ContextExtractedContentContainingNode? { + public func animateFromMicInput(micInputNode: UIView, transition: CombinedTransition) -> ContextExtractedContentContainingNode? { for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageFileBubbleContentNode { let statusContainerNode = contentNode.interactiveFileNode.statusContainerNode @@ -965,11 +1013,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - func animateContentFromMediaInput(snapshotView: UIView, transition: CombinedTransition) { + public func animateContentFromMediaInput(snapshotView: UIView, transition: CombinedTransition) { self.mainContextSourceNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } - func animateContentFromGroupedMediaInput(transition: CombinedTransition) -> [CGRect] { + public func animateContentFromGroupedMediaInput(transition: CombinedTransition) -> [CGRect] { self.mainContextSourceNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) var rects: [CGRect] = [] @@ -981,12 +1029,50 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return rects } - override func didLoad() { + override public func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) recognizer.tapActionAtPoint = { [weak self] point in if let strongSelf = self { + if let item = strongSelf.item, let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject { + if case let .link(link) = info { + let options = Atomic(value: nil) + link.options.start(next: { value in + let _ = options.swap(value) + }).dispose() + guard let options = options.with({ $0 }) else { + return .fail + } + if !options.hasAlternativeLinks { + return .fail + } + + for contentNode in strongSelf.contentNodes { + let contentNodePoint = strongSelf.view.convert(point, to: contentNode.view) + let tapAction = contentNode.tapActionAtPoint(contentNodePoint, gesture: .tap, isEstimating: true) + switch tapAction.content { + case .none: + break + case .ignore: + return .fail + case .url: + return .waitForSingleTap + default: + break + } + } + } + + return .fail + } + + if let closeButtonNode = strongSelf.closeButtonNode { + if let _ = closeButtonNode.hitTest(strongSelf.view.convert(point, to: closeButtonNode.view), with: nil) { + return .fail + } + } + if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) { return .fail } @@ -1036,7 +1122,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode for contentNode in strongSelf.contentNodes { let contentNodePoint = strongSelf.view.convert(point, to: contentNode.view) let tapAction = contentNode.tapActionAtPoint(contentNodePoint, gesture: .tap, isEstimating: true) - switch tapAction { + switch tapAction.content { case .none: if let _ = strongSelf.item?.controllerInteraction.tapMessage { return .waitForSingleTap @@ -1063,7 +1149,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if let action = strongSelf.gestureRecognized(gesture: .longTap, location: point, recognizer: recognizer) { switch action { case let .action(f): - f() + f.action() recognizer.cancel() case let .optionalAction(f): f() @@ -1082,13 +1168,21 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode switch action { case .action, .optionalAction: break - case let .openContextMenu(tapMessage, selectAll, subFrame): - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, strongSelf, subFrame, nil, point) + case let .openContextMenu(openContextMenu): + item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, strongSelf, openContextMenu.subFrame, nil, point) } } } recognizer.highlight = { [weak self] point in if let strongSelf = self { + if let replyInfoNode = strongSelf.replyInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) + } + replyInfoNode.updateTouchesAtPoint(translatedPoint) + } for contentNode in strongSelf.contentNodes { var translatedPoint: CGPoint? let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: strongSelf.view) @@ -1104,6 +1198,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.view.isExclusiveTouch = true let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) + if let item = self.item { + let _ = item + replyRecognizer.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + self.view.disablesInteractiveTransitionGestureRecognizer = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + } replyRecognizer.shouldBegin = { [weak self] in if let strongSelf = self, let item = strongSelf.item { if strongSelf.selectionNode != nil { @@ -1135,10 +1234,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } return false } + self.replyRecognizer = replyRecognizer self.view.addGestureRecognizer(replyRecognizer) + + if let item = self.item, let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject { + if case .link = info { + } else { + self.tapRecognizer?.isEnabled = false + } + self.replyRecognizer?.isEnabled = false + } } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] for contentNode in self.contentNodes { if let message = contentNode.item?.message { @@ -1193,8 +1301,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), threadInfoLayout: (ChatMessageThreadInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode), - forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, ChatMessageForwardInfoNode.StoryData?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), - replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode), + forwardInfoLayout: (AccountContext, ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, ChatMessageForwardInfoNode.StoryData?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), + replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)), @@ -1208,7 +1316,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: isSelected) let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) - let nameFont = Font.medium(fontSize) + let nameFont = Font.semibold(fontSize) let inlineBotPrefixFont = Font.regular(fontSize) @@ -1245,18 +1353,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var allowFullWidth = false let chatLocationPeerId: PeerId = item.chatLocation.peerId ?? item.content.firstMessage.id.peerId - + do { let peerId = chatLocationPeerId - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { displayAuthorInfo = false } else if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { if let forwardInfo = item.content.firstMessage.forwardInfo { ignoreForward = true effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), 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) } } displayAuthorInfo = !mergedTop.merged && incoming && effectiveAuthor != nil @@ -1272,7 +1380,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode displayAuthorInfo = !mergedTop.merged && incoming } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { ignoreForward = true - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), 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) displayAuthorInfo = !mergedTop.merged && incoming } else if let adAttribute = item.content.firstMessage.adAttribute, let author = item.content.firstMessage.author { ignoreForward = true @@ -1470,7 +1578,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject { needsShareButton = false } @@ -1505,6 +1613,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item) var maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset * 3.0 - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) + if needsShareButton { + maximumContentWidth -= 10.0 + } var hasInstantVideo = false for contentNodeItemValue in contentNodeMessagesAndClasses { @@ -1583,6 +1694,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var inlineBotNameString: String? var replyMessage: Message? + var replyForward: QuotedReplyMessageAttribute? + var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? var replyStory: StoryId? var replyMarkup: ReplyMarkupMessageAttribute? var authorNameColor: UIColor? @@ -1599,6 +1712,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } else { replyMessage = firstMessage.associatedMessages[attribute.messageId] } + replyQuote = attribute.quote.flatMap { ($0, attribute.isQuote) } + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty && !isPreview { @@ -1720,8 +1836,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let topPosition: ChatMessageBubbleRelativePosition let bottomPosition: ChatMessageBubbleRelativePosition - var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) - var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) + var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) + var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) if index != 0 { topBubbleAttributes = contentPropertiesAndPrepareLayouts[index - 1].3 } @@ -1834,7 +1950,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode hasForwardLikeContent = true } - if inlineBotNameString == nil && (ignoreForward || !hasForwardLikeContent) && replyMessage == nil && replyStory == nil { + if inlineBotNameString == nil && (ignoreForward || !hasForwardLikeContent) && replyMessage == nil && replyForward == nil && replyStory == nil { if let first = contentPropertiesAndLayouts.first, first.1.hidesSimpleAuthorHeader && !ignoreNameHiding { if let author = firstMessage.author as? TelegramChannel, case .group = author.info, author.id == firstMessage.id.peerId, !incoming { } else { @@ -1844,20 +1960,41 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } + if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { + let peer = (peer as Peer) + let nameColors = peer.nameColor.flatMap { item.context.peerNameColors.get($0, dark: item.presentationData.theme.theme.overallDarkAppearance) } + authorNameColor = nameColors?.main + } else if let effectiveAuthor = effectiveAuthor { + let nameColor = effectiveAuthor.nameColor ?? .blue + let nameColors = item.context.peerNameColors.get(nameColor, dark: item.presentationData.theme.theme.overallDarkAppearance) + let color: UIColor + if incoming { + color = nameColors.main + } else { + color = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor + } + authorNameColor = color + } + if initialDisplayHeader && displayAuthorInfo { if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { authorNameString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - authorNameColor = chatMessagePeerIdColors[Int(clamping: peer.id.id._internalGetInt64Value() % 7)] + + let peer = (peer as Peer) + let nameColors = peer.nameColor.flatMap { item.context.peerNameColors.get($0, dark: item.presentationData.theme.theme.overallDarkAppearance) } + authorNameColor = nameColors?.main } else if let effectiveAuthor = effectiveAuthor { authorNameString = EnginePeer(effectiveAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - let nameColor: UIColor + let nameColor = effectiveAuthor.nameColor ?? .blue + let nameColors = item.context.peerNameColors.get(nameColor, dark: item.presentationData.theme.theme.overallDarkAppearance) + let color: UIColor if incoming { - nameColor = chatMessagePeerIdColors[Int(clamping: effectiveAuthor.id.id._internalGetInt64Value() % 7)] + color = nameColors.main } else { - nameColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor + color = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor } - authorNameColor = nameColor + authorNameColor = color if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId { } else if effectiveAuthor.isScam { @@ -1865,11 +2002,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } 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 { - 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: nameColor.withMultipliedAlpha(0.4), loopMode: .count(2)) + 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) } else if effectiveAuthor.isPremium { - currentCredibilityIcon = .premium(color: nameColor.withMultipliedAlpha(0.4)) + currentCredibilityIcon = .premium(color: color.withMultipliedAlpha(0.4)) } } if let rawAuthorNameColor = authorNameColor { @@ -1904,7 +2041,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if firstMessage.media.contains(where: { $0 is TelegramMediaStory }) { displayHeader = true } - if replyMessage != nil || replyStory != nil { + if replyMessage != nil || replyForward != nil || replyStory != nil { displayHeader = true } if !displayHeader, case .peer = item.chatLocation, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { @@ -1914,7 +2051,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let firstNodeTopPosition: ChatMessageBubbleRelativePosition if displayHeader { - firstNodeTopPosition = .Neighbour(false, .freeform, .default) + firstNodeTopPosition = .Neighbour(false, .header, .default) } else { firstNodeTopPosition = .None(topNodeMergeStatus) } @@ -1966,7 +2103,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } let dateFormat: MessageTimestampStatusFormat - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { dateFormat = .minimal } else { dateFormat = .regular @@ -2026,7 +2163,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var threadInfoSizeApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode?) = (CGSize(), { _ in nil }) var replyInfoOriginY: CGFloat = 0.0 - var replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?) = (CGSize(), { _ in nil }) + var replyInfoSizeApply: (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode?) = (CGSize(), { _, _, _ in nil }) var forwardInfoOriginY: CGFloat = 0.0 var forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?) = (CGSize(), { _ in nil }) @@ -2038,7 +2175,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0 if authorNameString != nil || inlineBotNameString != nil { if headerSize.height.isZero { - headerSize.height += 5.0 + headerSize.height += 7.0 } let inlineBotNameColor = messageTheme.accentTextColor @@ -2093,13 +2230,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return adminBadgeSizeAndApply.1() }) - let sizeAndApply = authorNameLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - credibilityIconWidth - adminBadgeSizeAndApply.0.size.width), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let closeButtonWidth: CGFloat = item.message.adAttribute != nil ? 18.0 : 0.0 + + let sizeAndApply = authorNameLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - credibilityIconWidth - adminBadgeSizeAndApply.0.size.width - closeButtonWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) nameNodeSizeApply = (sizeAndApply.0.size, { return sizeAndApply.1() }) nameNodeOriginY = headerSize.height - headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + adminBadgeSizeAndApply.0.size.width + credibilityIconWidth + bubbleWidthInsets) + headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + adminBadgeSizeAndApply.0.size.width + credibilityIconWidth + closeButtonWidth + bubbleWidthInsets) headerSize.height += nameNodeSizeApply.0.height } @@ -2128,7 +2267,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode forwardAuthorSignature = forwardInfo.authorSignature } } - let sizeAndApply = forwardInfoLayout(item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) + let sizeAndApply = forwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) }) forwardInfoOriginY = headerSize.height @@ -2155,7 +2294,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - let sizeAndApply = forwardInfoLayout(item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, nil, nil, ChatMessageForwardInfoNode.StoryData(storyType: storyType), CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) + let sizeAndApply = forwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, nil, nil, ChatMessageForwardInfoNode.StoryData(storyType: storyType), CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) }) if storyType != .regular { @@ -2171,7 +2310,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - var hasReply = replyMessage != nil || replyStory != nil + var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil if !isInstantVideo, case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId { hasReply = false @@ -2203,9 +2342,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if !isInstantVideo, hasReply, (replyMessage != nil || replyStory != nil) { + if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) { if headerSize.height.isZero { - headerSize.height += 6.0 + headerSize.height += 11.0 } else { headerSize.height += 2.0 } @@ -2215,22 +2354,28 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode context: item.context, type: .bubble(incoming: incoming), message: replyMessage, + replyForward: replyForward, + quote: replyQuote, story: replyStory, parentMessage: item.message, - constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), + constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: CGFloat.greatestFiniteMagnitude), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer, associatedData: item.associatedData )) - replyInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) }) + replyInfoSizeApply = (sizeAndApply.0, { realSize, synchronousLoads, animation in sizeAndApply.1(realSize, synchronousLoads, animation) }) replyInfoOriginY = headerSize.height headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + bubbleWidthInsets) - headerSize.height += replyInfoSizeApply.0.height + 2.0 - } - - if !headerSize.height.isZero { - headerSize.height -= 5.0 + headerSize.height += replyInfoSizeApply.0.height + 7.0 + + if !headerSize.height.isZero { + headerSize.height -= 7.0 + } + } else { + if !headerSize.height.isZero { + headerSize.height -= 5.0 + } } } @@ -2266,6 +2411,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } + var updatedContentNodeOrder = false + if currentContentClassesPropertiesAndLayouts.count == contentNodeMessagesAndClasses.count { + for i in 0 ..< currentContentClassesPropertiesAndLayouts.count { + let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].1 + let contentItem = contentNodeMessagesAndClasses[i] as (message: Message, type: AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes) + if currentClass != contentItem.type { + updatedContentNodeOrder = true + break + } + } + } + var contentNodePropertiesAndFinalize: [(ChatMessageBubbleContentProperties, ChatMessageBubbleContentPosition?, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void), UInt32?, Bool?)] = [] var maxContentWidth: CGFloat = headerSize.width @@ -2350,7 +2507,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if mosaicRange.upperBound - 1 == contentNodeCount - 1 { lastMosaicBottomPosition = lastNodeTopPosition } else { - lastMosaicBottomPosition = .Neighbour(false, .freeform, .default) + lastMosaicBottomPosition = .Neighbour(false, .text, .default) } if position.contains(.bottom), case .Neighbour = lastMosaicBottomPosition { @@ -2418,8 +2575,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let topPosition: ChatMessageBubbleRelativePosition let bottomPosition: ChatMessageBubbleRelativePosition - var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) - var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default) + var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) + var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default) if i != 0 { topBubbleAttributes = contentPropertiesAndLayouts[i - 1].3 } @@ -2442,7 +2599,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contentPosition = .linear(top: topPosition, bottom: bottomPosition) case .mosaic: assertionFailure() - contentPosition = .linear(top: .Neighbour(false, .freeform, .default), bottom: .Neighbour(false, .freeform, .default)) + contentPosition = .linear(top: .Neighbour(false, .text, .default), bottom: .Neighbour(false, .text, .default)) } let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), contentPosition) #if DEBUG @@ -2450,7 +2607,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode print("contentNodeWidth \(contentNodeWidth) > \(maximumNodeWidth)") } #endif - maxContentWidth = max(maxContentWidth, contentNodeWidth) + + if contentNodeProperties.isDetached { + + } else { + maxContentWidth = max(maxContentWidth, contentNodeWidth) + } contentNodePropertiesAndFinalize.append((contentNodeProperties, contentPosition, contentNodeFinalize, contentGroupId, itemSelection)) } @@ -2465,6 +2627,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var contentNodesHeight: CGFloat = 0.0 var totalContentNodesHeight: CGFloat = 0.0 var currentContainerGroupOverlap: CGFloat = 0.0 + var detachedContentNodesHeight: CGFloat = 0.0 var mosaicStatusOrigin: CGPoint? for i in 0 ..< contentNodePropertiesAndFinalize.count { @@ -2506,7 +2669,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } totalContentNodesHeight += properties.headerSpacing } - + if currentContainerGroupId != contentGroupId { if let containerGroupId = currentContainerGroupId { var overlapOffset: CGFloat = 0.0 @@ -2526,11 +2689,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode currentItemSelection = itemSelection } + let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight let (size, apply) = finalize(maxContentWidth) - contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodesHeight), size: size), properties, contentGroupId == nil, apply)) + contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size), properties, contentGroupId == nil, apply)) contentNodesHeight += size.height totalContentNodesHeight += size.height + + if properties.isDetached { + detachedContentNodesHeight += size.height + } } } @@ -2560,7 +2728,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth) } - + let minimalContentSize: CGSize if hideBackground { minimalContentSize = CGSize(width: 1.0, height: 1.0) @@ -2568,24 +2736,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode minimalContentSize = layoutConstants.bubble.minimumSize } let calculatedBubbleHeight = headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom - let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(minimalContentSize.height, calculatedBubbleHeight)) - + let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(minimalContentSize.height, calculatedBubbleHeight - detachedContentNodesHeight)) var contentVerticalOffset: CGFloat = 0.0 if minimalContentSize.height > calculatedBubbleHeight + 2.0 { contentVerticalOffset = floorToScreenPixels((minimalContentSize.height - calculatedBubbleHeight) / 2.0) } + let availableWidth = params.width - params.leftInset - params.rightInset let backgroundFrame: CGRect let contentOrigin: CGPoint let contentUpperRightCorner: CGPoint switch alignment { case .none: - backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset - deliveryFailedInset), y: 0.0), size: layoutBubbleSize) + backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset - deliveryFailedInset), y: detachedContentNodesHeight), size: layoutBubbleSize) contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height + contentVerticalOffset) contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) case .center: - let availableWidth = params.width - params.leftInset - params.rightInset - backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: 0.0), size: layoutBubbleSize) + backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: detachedContentNodesHeight), size: layoutBubbleSize) let contentOriginX: CGFloat if !hideBackground { contentOriginX = (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right) @@ -2598,7 +2765,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let bubbleContentWidth = maxContentWidth - layoutConstants.bubble.edgeInset * 2.0 - (layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left) - var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height) + var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height + detachedContentNodesHeight) if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height } @@ -2657,6 +2824,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode nameNodeSizeApply: nameNodeSizeApply, contentOrigin: contentOrigin, nameNodeOriginY: nameNodeOriginY, + authorNameColor: authorNameColor, layoutConstants: layoutConstants, currentCredibilityIcon: currentCredibilityIcon, adminNodeSizeApply: adminNodeSizeApply, @@ -2668,6 +2836,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode replyInfoSizeApply: replyInfoSizeApply, replyInfoOriginY: replyInfoOriginY, removedContentNodeIndices: removedContentNodeIndices, + updatedContentNodeOrder: updatedContentNodeOrder, addedContentNodes: addedContentNodes, contentNodeMessagesAndClasses: contentNodeMessagesAndClasses, contentNodeFramesPropertiesAndApply: contentNodeFramesPropertiesAndApply, @@ -2708,6 +2877,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode nameNodeSizeApply: (CGSize, () -> TextNode?), contentOrigin: CGPoint, nameNodeOriginY: CGFloat, + authorNameColor: UIColor?, layoutConstants: ChatMessageItemLayoutConstants, currentCredibilityIcon: EmojiStatusComponent.Content?, adminNodeSizeApply: (CGSize, () -> TextNode?), @@ -2716,9 +2886,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode threadInfoOriginY: CGFloat, forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?), forwardInfoOriginY: CGFloat, - replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?), + replyInfoSizeApply: (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode?), replyInfoOriginY: CGFloat, removedContentNodeIndices: [Int]?, + updatedContentNodeOrder: Bool, addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?, contentNodeMessagesAndClasses: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, Bool, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)], @@ -2747,6 +2918,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.updateAccessibilityData(accessibilityData) strongSelf.disablesComments = disablesComments + strongSelf.authorNameColor = authorNameColor + + strongSelf.replyRecognizer?.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + strongSelf.view.disablesInteractiveTransitionGestureRecognizer = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + var animation = animation if strongSelf.mainContextSourceNode.isExtractedToContextPreview { animation = .System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .easeInOut, interactive: false)) @@ -2757,7 +2933,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if case let .System(duration, _) = animation { legacyTransition = .animated(duration: duration, curve: .spring) - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject, !"".isEmpty { useDisplayLinkAnimations = true } } @@ -2781,7 +2957,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if item.presentationData.theme.theme.forceSync { legacyTransition = .immediate } - strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: legacyTransition, backgroundNode: presentationContext.backgroundNode) + strongSelf.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: legacyTransition, backgroundNode: presentationContext.backgroundNode) strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode, backgroundNode: presentationContext.backgroundNode) strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics) @@ -2904,6 +3080,55 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.adminBadgeNode?.removeFromSupernode() strongSelf.adminBadgeNode = nil } + + if let _ = item.message.adAttribute { + let buttonNode: HighlightTrackingButtonNode + let iconNode: ASImageNode + if let currentButton = strongSelf.closeButtonNode, let currentIcon = strongSelf.closeIconNode { + buttonNode = currentButton + iconNode = currentIcon + } else { + buttonNode = HighlightTrackingButtonNode() + iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + iconNode.isUserInteractionEnabled = false + + buttonNode.addTarget(strongSelf, action: #selector(strongSelf.closeButtonPressed), forControlEvents: .touchUpInside) + buttonNode.highligthedChanged = { [weak iconNode] highlighted in + guard let iconNode else { + return + } + if highlighted { + iconNode.layer.removeAnimation(forKey: "opacity") + iconNode.alpha = 0.4 + } else { + iconNode.alpha = 1.0 + iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + + strongSelf.clippingNode.addSubnode(buttonNode) + strongSelf.clippingNode.addSubnode(iconNode) + + strongSelf.closeButtonNode = buttonNode + strongSelf.closeIconNode = iconNode + } + + iconNode.image = PresentationResourcesChat.chatBubbleCloseIcon(item.presentationData.theme.theme) + + let closeButtonSize = CGSize(width: 32.0, height: 32.0) + let closeIconSize = CGSize(width: 12.0, height: 12.0) + let closeButtonFrame = CGRect(origin: CGPoint(x: contentUpperRightCorner.x - closeButtonSize.width, y: layoutConstants.bubble.contentInsets.top), size: closeButtonSize) + let closeButtonIconFrame = CGRect(origin: CGPoint(x: contentUpperRightCorner.x - layoutConstants.text.bubbleInsets.left - closeIconSize.width + 1.0, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY + 2.0), size: closeIconSize) + + animation.animator.updateFrame(layer: buttonNode.layer, frame: closeButtonFrame, completion: nil) + animation.animator.updateFrame(layer: iconNode.layer, frame: closeButtonIconFrame, completion: nil) + } else { + strongSelf.closeButtonNode?.removeFromSupernode() + strongSelf.closeButtonNode = nil + strongSelf.closeIconNode?.removeFromSupernode() + strongSelf.closeIconNode = nil + } } else { if animation.isAnimated { if let nameNode = strongSelf.nameNode { @@ -3021,7 +3246,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if let replyInfoNode = replyInfoSizeApply.1(synchronousLoads) { + let replyInfoFrame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: CGSize(width: backgroundFrame.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: replyInfoSizeApply.0.height)) + if let replyInfoNode = replyInfoSizeApply.1(replyInfoFrame.size, synchronousLoads, animation) { strongSelf.replyInfoNode = replyInfoNode var animateFrame = true if replyInfoNode.supernode == nil { @@ -3035,7 +3261,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } let previousReplyInfoNodeFrame = replyInfoNode.frame - replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: CGSize(width: backgroundFrame.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: replyInfoSizeApply.0.height)) + replyInfoNode.frame = replyInfoFrame if case let .System(duration, _) = animation { if animateFrame { replyInfoNode.layer.animateFrame(from: previousReplyInfoNodeFrame, to: replyInfoNode.frame, duration: duration, timingFunction: timingFunction) @@ -3104,8 +3330,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode switch action { case .action, .optionalAction: return false - case let .openContextMenu(_, selectAll, _): - return !selectAll + case let .openContextMenu(openContextMenu): + return !openContextMenu.selectAll } } return true @@ -3218,7 +3444,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.contentContainersWrapperNode.view.mask = nil } - if removedContentNodeIndices?.count ?? 0 != 0 || addedContentNodes?.count ?? 0 != 0 { + var animateTextAndWebpagePositionSwap: Bool? + var bottomStatusNodeAnimationSourcePosition: CGPoint? + + if removedContentNodeIndices?.count ?? 0 != 0 || addedContentNodes?.count ?? 0 != 0 || updatedContentNodeOrder { var updatedContentNodes = strongSelf.contentNodes if let removedContentNodeIndices = removedContentNodeIndices { @@ -3250,8 +3479,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode } + + #if DEBUG && false + contentNode.layer.borderColor = UIColor(white: 0.0, alpha: 0.2).cgColor + contentNode.layer.borderWidth = 1.0 + #endif + containerSupernode.addSubnode(contentNode) + contentNode.itemNode = strongSelf contentNode.bubbleBackgroundNode = strongSelf.backgroundNode contentNode.bubbleBackdropNode = strongSelf.backgroundWallpaperNode contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in @@ -3283,12 +3519,32 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode assert(sortedContentNodes.count == updatedContentNodes.count) + if animation.isAnimated, let fromTextIndex = strongSelf.contentNodes.firstIndex(where: { $0 is ChatMessageTextBubbleContentNode }), let fromWebpageIndex = strongSelf.contentNodes.firstIndex(where: { $0 is ChatMessageWebpageBubbleContentNode }) { + if let toTextIndex = sortedContentNodes.firstIndex(where: { $0 is ChatMessageTextBubbleContentNode }), let toWebpageIndex = sortedContentNodes.firstIndex(where: { $0 is ChatMessageWebpageBubbleContentNode }) { + if fromTextIndex == toWebpageIndex && fromWebpageIndex == toTextIndex { + animateTextAndWebpagePositionSwap = fromTextIndex < toTextIndex + + if let textNode = strongSelf.contentNodes[fromTextIndex] as? ChatMessageTextBubbleContentNode, let webpageNode = strongSelf.contentNodes[fromWebpageIndex] as? ChatMessageWebpageBubbleContentNode { + if fromTextIndex > toTextIndex { + if let statusNode = textNode.statusNode, let contentSuperview = textNode.view.superview, statusNode.view.isDescendant(of: contentSuperview) { + bottomStatusNodeAnimationSourcePosition = statusNode.view.convert(CGPoint(x: statusNode.bounds.width, y: statusNode.bounds.height), to: contentSuperview) + } + } else { + if let statusNode = webpageNode.contentNode.statusNode, let contentSuperview = webpageNode.view.superview, statusNode.view.isDescendant(of: contentSuperview) { + bottomStatusNodeAnimationSourcePosition = statusNode.view.convert(CGPoint(x: statusNode.bounds.width, y: statusNode.bounds.height), to: contentSuperview) + } + } + } + } + } + } + strongSelf.contentNodes = sortedContentNodes } var shouldClipOnTransitions = true var contentNodeIndex = 0 - for (relativeFrame, _, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply { + for (relativeFrame, properties, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply { apply(animation, synchronousLoads, applyInfo) if contentNodeIndex >= strongSelf.contentNodes.count { @@ -3301,7 +3557,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode shouldClipOnTransitions = false } - let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: useContentOrigin ? contentOrigin.y : 0.0) + var effectiveContentOriginX = contentOrigin.x + var effectiveContentOriginY = useContentOrigin ? contentOrigin.y : 0.0 + if properties.isDetached { + effectiveContentOriginX = floorToScreenPixels((layout.size.width - relativeFrame.width) / 2.0) + effectiveContentOriginY = 0.0 + } + + let contentNodeFrame = relativeFrame.offsetBy(dx: effectiveContentOriginX, dy: effectiveContentOriginY) let previousContentNodeFrame = contentNode.frame if case let .System(duration, _) = animation { @@ -3324,7 +3587,52 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode }) strongSelf.setAnimationForKey("contentNode\(contentNodeIndex)Frame", animation: animation) } else { - animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil) + var useExpensiveSnapshot = false + if case .messageOptions = item.associatedData.subject { + useExpensiveSnapshot = true + } + + if let animateTextAndWebpagePositionSwap, let contentNode = contentNode as? ChatMessageTextBubbleContentNode, let snapshotView = useExpensiveSnapshot ? contentNode.view.snapshotView(afterScreenUpdates: false) : contentNode.layer.snapshotContentTreeAsView() { + let clippingView = UIView() + clippingView.clipsToBounds = true + clippingView.frame = contentNode.frame + + clippingView.addSubview(snapshotView) + snapshotView.frame = CGRect(origin: CGPoint(), size: contentNode.bounds.size) + + contentNode.view.superview?.insertSubview(clippingView, belowSubview: contentNode.view) + + animation.animator.updateAlpha(layer: clippingView.layer, alpha: 0.0, completion: { [weak clippingView] _ in + clippingView?.removeFromSuperview() + }) + + let positionOffset: CGFloat = animateTextAndWebpagePositionSwap ? -1.0 : 1.0 + + animation.animator.updatePosition(layer: snapshotView.layer, position: CGPoint(x: snapshotView.center.x, y: snapshotView.center.y + positionOffset * contentNode.frame.height), completion: nil) + + contentNode.frame = contentNodeFrame + + if let statusNode = contentNode.statusNode, let contentSuperview = contentNode.view.superview, statusNode.view.isDescendant(of: contentSuperview), let bottomStatusNodeAnimationSourcePosition { + let localSourcePosition = statusNode.view.convert(bottomStatusNodeAnimationSourcePosition, from: contentSuperview) + let offset = CGPoint(x: statusNode.bounds.width - localSourcePosition.x, y: statusNode.bounds.height - localSourcePosition.y) + animation.animator.animatePosition(layer: statusNode.layer, from: statusNode.layer.position.offsetBy(dx: -offset.x, dy: -offset.y), to: statusNode.layer.position, completion: nil) + } + + contentNode.animateClippingTransition(offset: positionOffset * contentNodeFrame.height, animation: animation) + + contentNode.alpha = 0.0 + animation.animator.updateAlpha(layer: contentNode.layer, alpha: 1.0, completion: nil) + } else if animateTextAndWebpagePositionSwap != nil, let contentNode = contentNode as? ChatMessageWebpageBubbleContentNode { + if let statusNode = contentNode.contentNode.statusNode, let contentSuperview = contentNode.view.superview, statusNode.view.isDescendant(of: contentSuperview), let bottomStatusNodeAnimationSourcePosition { + let localSourcePosition = statusNode.view.convert(bottomStatusNodeAnimationSourcePosition, from: contentSuperview) + let offset = CGPoint(x: statusNode.bounds.width - localSourcePosition.x, y: statusNode.bounds.height - localSourcePosition.y) + animation.animator.animatePosition(layer: statusNode.layer, from: statusNode.layer.position.offsetBy(dx: -offset.x, dy: -offset.y), to: statusNode.layer.position, completion: nil) + } + + animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil) + } else { + animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil) + } } } else if animateAlpha { contentNode.frame = contentNodeFrame @@ -3633,9 +3941,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if case let .System(duration, _) = animation/*, !strongSelf.mainContextSourceNode.isExtractedToContextPreview*/ { if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) { if useDisplayLinkAnimations { + strongSelf.clippingNode.clipsToBounds = shouldClipOnTransitions let backgroundAnimation = ListViewAnimation(from: strongSelf.backgroundNode.frame, to: backgroundFrame, duration: duration * UIView.animationDurationFactor(), curve: strongSelf.preferredAnimationCurve, beginAt: beginAt, update: { [weak strongSelf] _, frame in if let strongSelf = strongSelf { strongSelf.backgroundNode.frame = frame + if let backgroundHighlightNode = strongSelf.backgroundHighlightNode { + backgroundHighlightNode.frame = frame + backgroundHighlightNode.updateLayout(size: frame.size, transition: .immediate) + } strongSelf.clippingNode.position = CGPoint(x: frame.midX, y: frame.midY) strongSelf.clippingNode.bounds = CGRect(origin: CGPoint(x: frame.minX, y: frame.minY), size: frame.size) @@ -3643,10 +3956,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.backgroundWallpaperNode.updateFrame(frame, transition: .immediate) strongSelf.shadowNode.updateLayout(backgroundFrame: frame, transition: .immediate) } + }, completed: { [weak strongSelf] _ in + guard let strongSelf else { + return + } + strongSelf.clippingNode.clipsToBounds = false }) strongSelf.setAnimationForKey("backgroundNodeFrame", animation: backgroundAnimation) } else { animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil) + if let backgroundHighlightNode = strongSelf.backgroundHighlightNode { + animation.animator.updateFrame(layer: backgroundHighlightNode.layer, frame: backgroundFrame, completion: nil) + backgroundHighlightNode.updateLayout(size: backgroundFrame.size, transition: animation) + } animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil) strongSelf.clippingNode.clipsToBounds = shouldClipOnTransitions animation.animator.updateBounds(layer: strongSelf.clippingNode.layer, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size), completion: { [weak strongSelf] _ in @@ -3709,6 +4031,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview { legacyTransition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame) + if let backgroundHighlightNode = strongSelf.backgroundHighlightNode { + legacyTransition.updateFrame(node: backgroundHighlightNode, frame: backgroundFrame, completion: nil) + backgroundHighlightNode.updateLayout(size: backgroundFrame.size, transition: legacyTransition) + } legacyTransition.updateFrame(node: strongSelf.clippingNode, frame: backgroundFrame) legacyTransition.updateBounds(node: strongSelf.clippingNode, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size)) @@ -3718,6 +4044,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: legacyTransition) } else { strongSelf.backgroundNode.frame = backgroundFrame + if let backgroundHighlightNode = strongSelf.backgroundHighlightNode { + backgroundHighlightNode.frame = backgroundFrame + backgroundHighlightNode.updateLayout(size: backgroundFrame.size, transition: .immediate) + } + strongSelf.clippingNode.frame = backgroundFrame strongSelf.clippingNode.bounds = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size) strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate) @@ -3737,6 +4068,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.mainContextSourceNode.layoutUpdated?(strongSelf.mainContextSourceNode.bounds.size, animation) } + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject { + if case .link = info { + } else { + strongSelf.tapRecognizer?.isEnabled = false + } + strongSelf.replyRecognizer?.isEnabled = false + strongSelf.mainContainerNode.isGestureEnabled = false + for contentContainer in strongSelf.contentContainers { + contentContainer.containerNode.isGestureEnabled = false + } + } + strongSelf.updateSearchTextHighlightState() if let (_, f) = strongSelf.awaitingAppliedReaction { @@ -3744,10 +4087,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode f() } - } - override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { super.updateAccessibilityData(accessibilityData) self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label @@ -3787,73 +4129,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - override func shouldAnimateHorizontalFrameTransition() -> Bool { + override public func shouldAnimateHorizontalFrameTransition() -> Bool { return false - /*if let _ = self.backgroundFrameTransition { - return true - } else { - return false - }*/ } - override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { + override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { super.animateFrameTransition(progress, currentValue) - - /*if let backgroundFrameTransition = self.backgroundFrameTransition { - let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect - self.backgroundNode.frame = backgroundFrame - - self.clippingNode.frame = backgroundFrame - self.clippingNode.bounds = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size) - - self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate) - self.backgroundWallpaperNode.frame = backgroundFrame - self.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: .immediate) - - if let type = self.backgroundNode.type { - var incomingOffset: CGFloat = 0.0 - switch type { - case .incoming: - incomingOffset = 5.0 - default: - break - } - self.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0) - self.mainContainerNode.targetNodeForActivationProgressContentRect = self.mainContextSourceNode.contentRect - if !self.mainContextSourceNode.isExtractedToContextPreview { - if let (rect, size) = self.absoluteRect { - self.updateAbsoluteRect(rect, within: size) - } - } - } - self.messageAccessibilityArea.frame = backgroundFrame - - if let item = self.item, let shareButtonNode = self.shareButtonNode { - let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true) - shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) - } - - - if let item = self.item, let trButtonNode = self.trButtonNode { - let buttonSize = trButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true, translateButton: true) - - var totalShareButtonOffset: CGFloat = 0.0 - if hasShareButton { - totalShareButtonOffset = shareButtonWidth + shareButtonOffset - } - - trButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0 - totalShareButtonOffset), size: buttonSize) - } - - if CGFloat(1.0).isLessThanOrEqualTo(progress) { - self.backgroundFrameTransition = nil - - self.clippingNode.clipsToBounds = false - } - }*/ } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let item = self.item { @@ -3863,14 +4147,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } switch action { case let .action(f): - f() + f.action() case let .optionalAction(f): f() - case let .openContextMenu(tapMessage, selectAll, subFrame): - if canAddMessageReactions(message: tapMessage) { - item.controllerInteraction.updateMessageReaction(tapMessage, .default) + case let .openContextMenu(openContextMenu): + if canAddMessageReactions(message: openContextMenu.tapMessage) { + item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default) } else { - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil, nil) + item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil) } } } else if case .tap = gesture { @@ -3984,13 +4268,30 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) - }) + if let threadId = item.message.threadId, makeThreadIdMessageId(peerId: item.message.id.peerId, threadId: threadId) == attribute.messageId, let quotedReply = item.message.attributes.first(where: { $0 is QuotedReplyMessageAttribute }) as? QuotedReplyMessageAttribute { + return .action(InternalBubbleTapAction.Action({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(quotedReply.peerId.flatMap { item.message.peers[$0] }) + }, contextMenuOnLongPress: true)) + } + + return .action(InternalBubbleTapAction.Action({ [weak self] in + guard let self else { + return + } + var progress: Promise? + if let replyInfoNode = self.replyInfoNode { + progress = replyInfoNode.makeProgress() + } + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil, progress: progress)) + }, contextMenuOnLongPress: true)) } else if let attribute = attribute as? ReplyStoryAttribute { - return .optionalAction({ + return .action(InternalBubbleTapAction.Action({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) - }) + }, contextMenuOnLongPress: true)) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .action(InternalBubbleTapAction.Action({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }, contextMenuOnLongPress: true)) } } } @@ -4018,7 +4319,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -4027,19 +4328,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } if forwardInfoNode.hasAction(at: self.view.convert(location, to: forwardInfoNode.view)) { - return .action({}) + return .action(InternalBubbleTapAction.Action {}) } else { return .optionalAction(performAction) } } else if let item = self.item, let story = item.message.media.first(where: { $0 is TelegramMediaStory }) as? TelegramMediaStory { if let storyItem = item.message.associatedStories[story.storyId] { if storyItem.data.isEmpty { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.navigateToStory(item.message, story.storyId) }) } else { if let peer = item.message.peers[story.storyId.peerId] { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) }) } @@ -4051,28 +4352,45 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let convertedLocation = self.view.convert(location, to: contentNode.view) let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false) - switch tapAction { + switch tapAction.content { case .none: if let item = self.item, self.backgroundNode.frame.contains(CGPoint(x: self.frame.width - location.x, y: location.y)), let tapMessage = self.item?.controllerInteraction.tapMessage { - return .action({ + return .action(InternalBubbleTapAction.Action { tapMessage(item.message) }) } case .ignore: if let item = self.item, self.backgroundNode.frame.contains(CGPoint(x: self.frame.width - location.x, y: location.y)), let tapMessage = self.item?.controllerInteraction.tapMessage { - return .action({ + return .action(InternalBubbleTapAction.Action { tapMessage(item.message) }) } else { - return .action({ + return .action(InternalBubbleTapAction.Action { }) } - case let .url(url, concealed): - return .action({ - self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage) - }) + case let .url(url): + if case .longTap = gesture, !tapAction.hasLongTapAction, let item = self.item { + let tapMessage = item.content.firstMessage + var subFrame = self.backgroundNode.frame + if case .group = item.content { + for contentNode in self.contentNodes { + if contentNode.item?.message.stableId == tapMessage.stableId { + subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0) + break + } + } + } + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: tapMessage, selectAll: false, subFrame: subFrame, disableDefaultPressAnimation: true)) + } else { + return .action(InternalBubbleTapAction.Action({ [weak self] in + guard let self, let item = self.item else { + return + } + item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, message: item.content.firstMessage, allowInlineWebpageResolution: url.allowInlineWebpageResolution, progress: tapAction.activate?())) + }, contextMenuOnLongPress: !tapAction.hasLongTapAction)) + } case let .peerMention(peerId, _, openProfile): - return .action({ [weak self] in + return .action(InternalBubbleTapAction.Action { [weak self] in if let item = self?.item { let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).startStandalone(next: { peer in @@ -4083,17 +4401,17 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } }) case let .textMention(name): - return .action({ - self.item?.controllerInteraction.openPeerMention(name) + return .action(InternalBubbleTapAction.Action { + self.item?.controllerInteraction.openPeerMention(name, tapAction.activate?()) }) case let .botCommand(command): if let item = self.item { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.sendBotCommand(item.message.id, command) }) } case let .hashtag(peerName, hashtag): - return .action({ + return .action(InternalBubbleTapAction.Action { self.item?.controllerInteraction.openHashtag(peerName, hashtag) }) case .instantPage: @@ -4104,13 +4422,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } case .wallpaper: if let item = self.item { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.openWallpaper(item.message) }) } case .theme: if let item = self.item { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.openTheme(item.message) }) } @@ -4125,20 +4443,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let _ = item.controllerInteraction.openMessage(item.message, .default) }) } else { - return .action({ + return .action(InternalBubbleTapAction.Action { let _ = item.controllerInteraction.openMessage(item.message, .default) }) } } case let .timecode(timecode, _): if let item = self.item, let mediaMessage = mediaMessage { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.seekToTimecode(mediaMessage, timecode, forceOpen) }) } case let .bankCard(number): if let item = self.item { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.longTap(.bankCard(number), item.message) }) } @@ -4180,12 +4498,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let message = item.message if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { - return .action({}) + return .action(InternalBubbleTapAction.Action {}) + } + if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) { + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.content.firstMessage, selectAll: false, subFrame: self.backgroundNode.frame, disableDefaultPressAnimation: true)) } var tapMessage: Message? = item.content.firstMessage var selectAll = true var hasFiles = false + var disableDefaultPressAnimation = false loop: for contentNode in self.contentNodes { let convertedLocation = self.view.convert(location, to: contentNode.view) @@ -4208,27 +4530,31 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode tapMessage = contentNode.item?.message } let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false) - switch tapAction { + switch tapAction.content { case .none, .ignore: break - case let .url(url, _): - return .action({ - item.controllerInteraction.longTap(.url(url), message) - }) + case let .url(url): + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({ + item.controllerInteraction.longTap(.url(url.url), message) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case let .peerMention(peerId, mention, _): - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.longTap(.peerMention(peerId, mention), message) }) case let .textMention(name): - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.longTap(.mention(name), message) }) case let .botCommand(command): - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.longTap(.command(command), message) }) case let .hashtag(_, hashtag): - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.longTap(.hashtag(hashtag), message) }) case .instantPage: @@ -4243,12 +4569,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode break case let .timecode(timecode, text): if let mediaMessage = mediaMessage { - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.longTap(.timecode(timecode, text), mediaMessage) }) } case let .bankCard(number): - return .action({ + return .action(InternalBubbleTapAction.Action { item.controllerInteraction.longTap(.bankCard(number), message) }) case .tooltip: @@ -4273,7 +4599,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } } - return .openContextMenu(tapMessage: tapMessage, selectAll: selectAll, subFrame: subFrame) + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: tapMessage, selectAll: selectAll, subFrame: subFrame, disableDefaultPressAnimation: disableDefaultPressAnimation)) } } default: @@ -4297,7 +4623,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } @@ -4308,7 +4634,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } return nil } - + if let threadInfoNode = self.threadInfoNode, let result = threadInfoNode.hitTest(self.view.convert(point, to: threadInfoNode.view), with: event) { return result } @@ -4351,7 +4677,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return super.hitTest(point, with: event) } - override func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { for contentNode in self.contentNodes { if let result = contentNode.transitionNode(messageId: id, media: media, adjustRect: adjustRect) { if self.contentNodes.count == 1 && self.contentNodes.first is ChatMessageMediaBubbleContentNode && self.nameNode == nil && self.adminBadgeNode == nil && self.forwardInfoNode == nil && self.replyInfoNode == nil { @@ -4393,17 +4719,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - for contentNode in self.contentNodes { - let frame = contentNode.frame - if let result = contentNode.peekPreviewContent(at: point.offsetBy(dx: -frame.minX, dy: -frame.minY)) { - return result - } - } - return nil - } - - override func updateHiddenMedia() { + override public func updateHiddenMedia() { var hasHiddenMosaicStatus = false var hasHiddenBackground = false if let item = self.item { @@ -4436,7 +4752,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.backgroundWallpaperNode.isHidden = hasHiddenBackground } - override func updateAutomaticMediaDownloadSettings() { + override public func updateAutomaticMediaDownloadSettings() { if let item = self.item { for contentNode in self.contentNodes { contentNode.updateAutomaticMediaDownloadSettings(item.controllerInteraction.automaticMediaDownloadSettings) @@ -4444,7 +4760,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + override public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { for contentNode in self.contentNodes { if let playMediaWithSound = contentNode.playMediaWithSound() { return playMediaWithSound @@ -4453,7 +4769,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - override func updateSelectionState(animated: Bool) { + override public func updateSelectionState(animated: Bool) { guard let item = self.item else { return } @@ -4566,53 +4882,149 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - override func updateSearchTextHighlightState() { + override public func updateSearchTextHighlightState() { for contentNode in self.contentNodes { contentNode.updateSearchTextHighlightState(text: self.item?.controllerInteraction.searchTextHighightState?.0, messages: self.item?.controllerInteraction.searchTextHighightState?.1) } } - override func updateHighlightedState(animated: Bool) { + override public func updateHighlightedState(animated: Bool) { super.updateHighlightedState(animated: animated) guard let item = self.item, let _ = self.backgroundType else { return } - var highlighted = false + var highlightedState: HighlightedState? for contentNode in self.contentNodes { let _ = contentNode.updateHighlightedState(animated: animated) } - if let highlightedState = item.controllerInteraction.highlightedState { + if let highlightedStateValue = item.controllerInteraction.highlightedState { for (message, _) in item.content { - if highlightedState.messageStableId == message.stableId { - highlighted = true + if highlightedStateValue.messageStableId == message.stableId { + highlightedState = HighlightedState(quote: highlightedStateValue.quote) break } } } - if self.highlightedState != highlighted { - self.highlightedState = highlighted + if self.highlightedState != highlightedState { + self.highlightedState = highlightedState + + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + contentNode.updateQuoteTextHighlightState(text: nil, color: .clear, animated: true) + } + } + if let backgroundType = self.backgroundType { let graphics = PresentationResourcesChat.principalGraphics(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) - let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper - self.backgroundNode.setType(type: backgroundType, highlighted: highlighted, graphics: graphics, maskMode: self.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, backgroundNode: item.controllerInteraction.presentationContext.backgroundNode) + if self.highlightedState != nil { + let backgroundHighlightNode: ChatMessageBackground + if let current = self.backgroundHighlightNode { + backgroundHighlightNode = current + } else { + backgroundHighlightNode = ChatMessageBackground() + self.mainContextSourceNode.contentNode.insertSubnode(backgroundHighlightNode, aboveSubnode: self.backgroundNode) + self.backgroundHighlightNode = backgroundHighlightNode + + let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper + let incoming: PresentationThemeBubbleColorComponents = !hasWallpaper ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper : item.presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper + let outgoing: PresentationThemeBubbleColorComponents = !hasWallpaper ? item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper : item.presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper + + let highlightColor: UIColor + if item.message.effectivelyIncoming(item.context.account.peerId) { + if let authorNameColor = self.authorNameColor { + highlightColor = authorNameColor.withMultipliedAlpha(0.2) + } else { + highlightColor = incoming.highlightedFill + } + } else { + if let authorNameColor = self.authorNameColor { + highlightColor = authorNameColor.withMultipliedAlpha(0.2) + } else { + highlightColor = outgoing.highlightedFill + } + } + + backgroundHighlightNode.customHighlightColor = highlightColor + backgroundHighlightNode.setType(type: backgroundType, highlighted: true, graphics: graphics, maskMode: true, hasWallpaper: false, transition: .immediate, backgroundNode: nil) + + backgroundHighlightNode.frame = self.backgroundNode.frame + backgroundHighlightNode.updateLayout(size: backgroundHighlightNode.frame.size, transition: .immediate) + + if highlightedState?.quote != nil { + Queue.mainQueue().after(0.3, { [weak self] in + guard let self, let item = self.item, let backgroundHighlightNode = self.backgroundHighlightNode else { + return + } + + if let highlightedState = self.highlightedState, let quote = highlightedState.quote { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + + var quoteFrame: CGRect? + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + contentNode.updateQuoteTextHighlightState(text: quote, color: highlightColor, animated: false) + var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view) + if item.message.effectivelyIncoming(item.context.account.peerId) { + sourceFrame.origin.x += 6.0 + sourceFrame.size.width -= 6.0 + } else { + sourceFrame.size.width -= 6.0 + } + + if let localFrame = contentNode.animateQuoteTextHighlightIn(sourceFrame: sourceFrame, transition: transition) { + if self.contentNodes[0] !== contentNode && self.contentNodes[0].supernode === contentNode.supernode { + contentNode.supernode?.insertSubnode(contentNode, belowSubnode: self.contentNodes[0]) + } + + quoteFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview) + } + break + } + } + + if let quoteFrame { + self.backgroundHighlightNode = nil + + backgroundHighlightNode.updateLayout(size: quoteFrame.size, transition: transition) + transition.updateFrame(node: backgroundHighlightNode, frame: quoteFrame) + backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.05, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in + backgroundHighlightNode?.removeFromSupernode() + }) + } + } + }) + } + } + } else { + if let backgroundHighlightNode = self.backgroundHighlightNode { + self.backgroundHighlightNode = nil + if animated { + backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in + backgroundHighlightNode?.removeFromSupernode() + }) + } else { + backgroundHighlightNode.removeFromSupernode() + } + } + } } } } - @objc func shareButtonPressed() { + @objc private func shareButtonPressed() { if let item = self.item { if case .pinnedMessages = item.associatedData.subject { item.controllerInteraction.navigateToMessageStandalone(item.content.firstMessage.id) } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } @@ -4637,12 +5049,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let context = item.context let message = item.message let presentationData = context.sharedContext.currentPresentationData.with { $0 } -// let locale = presentationData.strings.baseLanguageCode -// var title = l("Messages.Translate", locale) + // let locale = presentationData.strings.baseLanguageCode + // var title = l("Messages.Translate", locale) var mode = "translate" let textToTranslate = message.textToTranslate() if message.text.contains(gTranslateSeparator) { -// title = l("Messages.UndoTranslate", locale) + // title = l("Messages.UndoTranslate", locale) mode = "undo-translate" } if mode == "undo-translate" { @@ -4670,7 +5082,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if let forwardInfo = currentMessage.forwardInfo { storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) } - + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: newMessageText, attributes: currentMessage.attributes, media: currentMessage.media)) }) }).start() @@ -4682,15 +5094,24 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } } + + @objc private func closeButtonPressed() { + if let item = self.item { + item.controllerInteraction.openNoAdsDemo() + } + } private var playedSwipeToReplyHaptic = false - @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 + var leftOffset: CGFloat = 0.0 var swipeOffset: CGFloat = 45.0 if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { offset = -24.0 + leftOffset = -10.0 } else { offset = 10.0 + leftOffset = -10.0 swipeOffset = 60.0 } @@ -4718,7 +5139,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if translation.x < 0.0 { translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) } else { - translation.x = 0.0 + if recognizer.allowBothDirections { + translation.x = -max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) + } else { + translation.x = 0.0 + } } if let item = self.item, self.swipeToReplyNode == nil { @@ -4738,8 +5163,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) if let swipeToReplyNode = self.swipeToReplyNode { - swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) - swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + if translation.x < 0.0 { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + } else { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0) + } if let (rect, containerSize) = self.absoluteRect { let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) @@ -4758,7 +5188,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.swipeToReplyFeedback = nil let translation = recognizer.translation(in: self.view) - if case .ended = recognizer.state, translation.x < -swipeOffset { + let gestureRecognized: Bool + if recognizer.allowBothDirections { + gestureRecognized = abs(translation.x) > swipeOffset + } else { + gestureRecognized = translation.x < -swipeOffset + } + if case .ended = recognizer.state, gestureRecognized { if let item = self.item { if let currentSwipeAction = currentSwipeAction { switch currentSwipeAction { @@ -4797,7 +5233,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private var absoluteRect: (CGRect, CGSize)? - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) guard !self.mainContextSourceNode.isExtractedToContextPreview else { return @@ -4848,7 +5284,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if !self.mainContextSourceNode.isExtractedToContextPreview { self.applyAbsoluteOffsetInternal(value: CGPoint(x: -value.x, y: -value.y), animationCurve: animationCurve, duration: duration) } @@ -4878,7 +5314,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { + override public func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { if self.contentContainers.count > 1 { return self.contentContainers.first(where: { $0.contentMessageStableId == stableId })?.sourceNode ?? self.mainContextSourceNode } else { @@ -4886,7 +5322,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + override public func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.mainContextSourceNode.contentNode.addSubnode(accessoryItemNode) } @@ -4896,7 +5332,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return self.mainContextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview || !self.disablesComments } - override func openMessageContextMenu() { + override public func openMessageContextMenu() { guard let item = self.item else { return } @@ -4904,7 +5340,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode item.controllerInteraction.openMessageContextMenu(item.message, true, self, subFrame, nil, nil) } - override func targetReactionView(value: MessageReaction.Reaction) -> UIView? { + override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result } @@ -4919,7 +5355,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - override func targetForStoryTransition(id: StoryId) -> UIView? { + override public func targetForStoryTransition(id: StoryId) -> UIView? { guard let item = self.item else { return nil } @@ -4940,13 +5376,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - override func unreadMessageRangeUpdated() { + override public func unreadMessageRangeUpdated() { for contentNode in self.contentNodes { contentNode.unreadMessageRangeUpdated() } } - func animateQuizInvalidOptionSelected() { + public func animateQuizInvalidOptionSelected() { if let supernode = self.supernode, let subnodes = supernode.subnodes { for i in 0 ..< subnodes.count { if subnodes[i] === self { @@ -5003,7 +5439,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.layer.add(animation, forKey: "quizInvalidRotation") } - func updatePsaTooltipMessageState(animated: Bool) { + public func updatePsaTooltipMessageState(animated: Bool) { guard let item = self.item else { return } @@ -5012,7 +5448,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - override func getStatusNode() -> ASDisplayNode? { + override public func getStatusNode() -> ASDisplayNode? { for contentNode in self.contentNodes { if let statusNode = contentNode.getStatusNode() { return statusNode @@ -5024,7 +5460,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - func hasExpandedAudioTranscription() -> Bool { + public func getQuoteRect(quote: String) -> CGRect? { + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + if let result = contentNode.getQuoteRect(quote: quote) { + return contentNode.view.convert(result, to: self.view) + } + } + } + return nil + } + + public func hasExpandedAudioTranscription() -> Bool { for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageFileBubbleContentNode { return contentNode.interactiveFileNode.hasExpandedAudioTranscription @@ -5035,7 +5482,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return false } - override func contentFrame() -> CGRect { + override public func contentFrame() -> CGRect { return self.backgroundNode.frame } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD new file mode 100644 index 00000000000..17460293d13 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageCallBubbleContentNode", + module_name = "ChatMessageCallBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift similarity index 89% rename from submodules/TelegramUI/Sources/ChatMessageCallBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 362b2bfae80..714b4e7f7f8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -6,6 +6,9 @@ import TelegramCore import Postbox import TelegramPresentationData import AppBundle +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageDateAndStatusNode private let titleFont: UIFont = Font.medium(16.0) private let labelFont: UIFont = Font.regular(13.0) @@ -16,13 +19,13 @@ private let incomingRedIcon = generateTintedImage(image: UIImage(bundleImageName private let outgoingGreenIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallOutgoingArrow"), color: UIColor(rgb: 0x36c033)) private let outgoingRedIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallOutgoingArrow"), color: UIColor(rgb: 0xff4747)) -class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { private let titleNode: TextNode private let labelNode: TextNode private let iconNode: ASImageNode private let buttonNode: HighlightableButtonNode - required init() { + required public init() { self.titleNode = TextNode() self.labelNode = TextNode() @@ -54,22 +57,22 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside) } - override func accessibilityActivate() -> Bool { + override public func accessibilityActivate() -> Bool { self.callButtonPressed() return true } - override func didLoad() { + override public func didLoad() { super.didLoad() self.view.accessibilityElementsHidden = true } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) @@ -233,19 +236,19 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - 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.2, removeOnCompletion: false) } - @objc func callButtonPressed() { + @objc private func callButtonPressed() { if let item = self.item { var isVideo = false for media in item.message.media { @@ -257,9 +260,9 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.buttonNode.frame.contains(point) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } else if self.bounds.contains(point), let item = self.item { var isVideo = false for media in item.message.media { @@ -267,9 +270,9 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { isVideo = isVideoValue } } - return .call(peerId: item.message.id.peerId, isVideo: isVideo) + return ChatMessageBubbleContentTapAction(content: .call(peerId: item.message.id.peerId, isVideo: isVideo)) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/BUILD new file mode 100644 index 00000000000..488db161a5f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageCommentFooterContentNode", + module_name = "ChatMessageCommentFooterContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/RadialStatusNode", + "//submodules/AnimatedCountLabelNode", + "//submodules/AnimatedAvatarSetNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/Sources/ChatMessageCommentFooterContentNode.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/Sources/ChatMessageCommentFooterContentNode.swift index 91b68dd87a6..926429e4a9f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode/Sources/ChatMessageCommentFooterContentNode.swift @@ -9,8 +9,10 @@ import TelegramPresentationData import RadialStatusNode import AnimatedCountLabelNode import AnimatedAvatarSetNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon -final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { private let separatorNode: ASDisplayNode private let countNode: AnimatedCountLabelNode private let alternativeCountNode: AnimatedCountLabelNode @@ -22,7 +24,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { private let unreadIconNode: ASImageNode private var statusNode: RadialStatusNode? - required init() { + required public init() { self.separatorNode = ASDisplayNode() self.separatorNode.isUserInteractionEnabled = false @@ -81,7 +83,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -96,7 +98,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { } } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeCountLayout = self.countNode.asyncLayout() let makeAlternativeCountLayout = self.alternativeCountNode.asyncLayout() @@ -390,30 +392,30 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.buttonNode.frame.contains(point) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.buttonNode.isUserInteractionEnabled && self.buttonNode.frame.contains(point) { return self.buttonNode.view } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/BUILD new file mode 100644 index 00000000000..612b19bfacc --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageContactBubbleContentNode", + module_name = "ChatMessageContactBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/PhoneNumberFormat", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift similarity index 88% rename from submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index 15274b2d444..1e3c3f46d4c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -9,13 +9,17 @@ import TelegramPresentationData import AvatarNode import AccountContext import PhoneNumberFormat +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentButtonNode private let avatarFont = avatarPlaceholderFont(size: 16.0) private let titleFont = Font.medium(14.0) private let textFont = Font.regular(14.0) -class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { private let avatarNode: AvatarNode private let dateAndStatusNode: ChatMessageDateAndStatusNode private let titleNode: TextNode @@ -26,7 +30,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { private let buttonNode: ChatMessageAttachedContentButtonNode - required init() { + required public init() { self.avatarNode = AvatarNode(font: avatarFont) self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.titleNode = TextNode() @@ -59,23 +63,23 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } } - override func accessibilityActivate() -> Bool { + override public func accessibilityActivate() -> Bool { self.buttonPressed() return true } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func didLoad() { + override public func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.contactTap(_:))) self.view.addGestureRecognizer(tapRecognizer) } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let statusLayout = self.dateAndStatusNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) @@ -93,7 +97,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } var incoming = item.message.effectivelyIncoming(item.context.account.peerId) - if case .forwardedMessages = item.associatedData.subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { incoming = false } @@ -242,30 +246,17 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { )) } - let buttonImage: UIImage - let buttonHighlightedImage: UIImage let titleColor: UIColor - let titleHighlightedColor: UIColor let avatarPlaceholderColor: UIColor if incoming { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(item.presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(item.presentationData.theme.theme)! titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor - - let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: true, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColors.fill[0] avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor } else { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(item.presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(item.presentationData.theme.theme)! titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor - - let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: false, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColors.fill[0] avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor } - let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor, false) + let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, false, true) var maxContentWidth: CGFloat = avatarSize.width + 7.0 if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { @@ -280,7 +271,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { return (contentWidth, { boundingWidth in let baseAvatarFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutConstants.text.bubbleInsets.top), size: avatarSize) - let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0) + let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, 33.0) let buttonSpacing: CGFloat = 4.0 let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) @@ -317,7 +308,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let _ = titleApply() let _ = textApply() - let _ = buttonApply() + let _ = buttonApply(animation) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 1.0), size: titleLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 20.0), size: textLayout.size) @@ -374,29 +365,29 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - 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.2, removeOnCompletion: false) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.buttonNode.frame.contains(point) { - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - @objc func contactTap(_ recognizer: UITapGestureRecognizer) { + @objc private func contactTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { let _ = item.controllerInteraction.openMessage(item.message, .default) @@ -410,14 +401,14 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionView(value: value) } return nil } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) { return result } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD new file mode 100644 index 00000000000..87db0a75553 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageDateAndStatusNode", + module_name = "ChatMessageDateAndStatusNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/LocalizedPeerData", + "//submodules/Components/ReactionButtonListComponent", + "//submodules/Components/ReactionImageComponent", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift similarity index 97% rename from submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 45855f26e9e..717ae063e91 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -29,13 +29,13 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { layer.add(basicAnimation, forKey: "clockFrameAnimation") } -enum ChatMessageDateAndStatusOutgoingType: Equatable { +public enum ChatMessageDateAndStatusOutgoingType: Equatable { case Sent(read: Bool) case Sending case Failed } -enum ChatMessageDateAndStatusType: Equatable { +public enum ChatMessageDateAndStatusType: Equatable { case BubbleIncoming case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType) case ImageIncoming @@ -166,6 +166,7 @@ private final class StatusReactionNode: ASDisplayNode { fileId: fileId, animationCache: animationCache, animationRenderer: animationRenderer, + tintColor: nil, placeholderColor: placeholderColor, animateIdle: animateIdle, reaction: value, @@ -176,27 +177,27 @@ private final class StatusReactionNode: ASDisplayNode { } } -class ChatMessageDateAndStatusNode: ASDisplayNode { - struct TrailingReactionSettings { - var displayInline: Bool - var preferAdditionalInset: Bool +public class ChatMessageDateAndStatusNode: ASDisplayNode { + public struct TrailingReactionSettings { + public var displayInline: Bool + public var preferAdditionalInset: Bool - init(displayInline: Bool, preferAdditionalInset: Bool) { + public init(displayInline: Bool, preferAdditionalInset: Bool) { self.displayInline = displayInline self.preferAdditionalInset = preferAdditionalInset } } - struct StandaloneReactionSettings { - init() { + public struct StandaloneReactionSettings { + public init() { } } - enum LayoutInput { - case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?) + public enum LayoutInput { + case trailingContent(contentWidth: CGFloat?, reactionSettings: TrailingReactionSettings?) case standalone(reactionSettings: StandaloneReactionSettings?) - var displayInlineReactions: Bool { + public var displayInlineReactions: Bool { switch self { case let .trailingContent(_, reactionSettings): if let reactionSettings = reactionSettings { @@ -214,7 +215,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - struct Arguments { + public struct Arguments { var context: AccountContext var presentationData: ChatPresentationData var edited: Bool @@ -234,7 +235,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var animationCache: AnimationCache var animationRenderer: MultiAnimationRenderer - init( + public init( context: AccountContext, presentationData: ChatPresentationData, edited: Bool, @@ -297,8 +298,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private var tapGestureRecognizer: UITapGestureRecognizer? - var openReplies: (() -> Void)? - var pressed: (() -> Void)? { + public var openReplies: (() -> Void)? + public var pressed: (() -> Void)? { didSet { if self.pressed != nil { if self.tapGestureRecognizer == nil { @@ -312,10 +313,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } } - var reactionSelected: ((MessageReaction.Reaction) -> Void)? - var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? + public var reactionSelected: ((MessageReaction.Reaction) -> Void)? + public var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? - override init() { + override public init() { self.dateNode = TextNode() self.dateNode.isUserInteractionEnabled = false self.dateNode.displaysAsynchronously = false @@ -333,7 +334,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)) { + public func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)) { let dateLayout = TextNode.asyncLayout(self.dateNode) var checkReadNode = self.checkReadNode @@ -848,14 +849,20 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if reactionButtonsSize.width.isZero { verticalReactionsInset = 0.0 - if contentWidth + layoutSize.width > arguments.constrainedSize.width { + if let contentWidth { + if contentWidth + layoutSize.width > arguments.constrainedSize.width { + resultingWidth = layoutSize.width + verticalInset = 0.0 + resultingHeight = layoutSize.height + verticalInset + } else { + resultingWidth = contentWidth + layoutSize.width + verticalInset = -layoutSize.height + resultingHeight = 0.0 + } + } else { resultingWidth = layoutSize.width verticalInset = 0.0 resultingHeight = layoutSize.height + verticalInset - } else { - resultingWidth = contentWidth + layoutSize.width - verticalInset = -layoutSize.height - resultingHeight = 0.0 } } else { var additionalVerticalInset: CGFloat = 0.0 @@ -1310,7 +1317,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)) { + public static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)) { let currentLayout = node?.asyncLayout() return { arguments in let resultNode: ChatMessageDateAndStatusNode @@ -1334,7 +1341,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - func reactionView(value: MessageReaction.Reaction) -> UIView? { + public func reactionView(value: MessageReaction.Reaction) -> UIView? { for (id, node) in self.reactionNodes { if id == value { return node.iconView @@ -1348,7 +1355,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return nil } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for (_, button) in self.reactionButtonsContainer.buttons { if button.view.frame.contains(point) { if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) { @@ -1365,6 +1372,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } -func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool { +public func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool { return forceInline } diff --git a/submodules/TelegramUI/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift similarity index 88% rename from submodules/TelegramUI/Sources/StringForMessageTimestampStatus.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index 2769155594c..b62393f58e4 100644 --- a/submodules/TelegramUI/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -7,7 +7,7 @@ import TelegramStringFormatting import LocalizedPeerData import AccountContext -enum MessageTimestampStatusFormat { +public enum MessageTimestampStatusFormat { case regular case minimal } @@ -29,7 +29,7 @@ private func dateStringForDay(strings: PresentationStrings, dateTimeFormat: Pres } } -func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, associatedData: ChatMessageItemAssociatedData) -> String { +public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, associatedData: ChatMessageItemAssociatedData) -> String { if let adAttribute = message.adAttribute { switch adAttribute.messageType { case .sponsored: @@ -87,7 +87,7 @@ func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, da } } - if let subject = associatedData.subject, case .forwardedMessages = subject { + if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { authorTitle = nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode/BUILD new file mode 100644 index 00000000000..526ff0cdbcd --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageDeliveryFailedNode", + module_name = "ChatMessageDeliveryFailedNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageDeliveryFailedNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode/Sources/ChatMessageDeliveryFailedNode.swift similarity index 81% rename from submodules/TelegramUI/Sources/ChatMessageDeliveryFailedNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode/Sources/ChatMessageDeliveryFailedNode.swift index 9a44437b6a0..caec43e2cd8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDeliveryFailedNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode/Sources/ChatMessageDeliveryFailedNode.swift @@ -4,11 +4,11 @@ import AsyncDisplayKit import Display import TelegramPresentationData -final class ChatMessageDeliveryFailedNode: ASImageNode { +public final class ChatMessageDeliveryFailedNode: ASImageNode { private let tapped: () -> Void private var theme: PresentationTheme? - init(tapped: @escaping () -> Void) { + public init(tapped: @escaping () -> Void) { self.tapped = tapped super.init() @@ -18,7 +18,7 @@ final class ChatMessageDeliveryFailedNode: ASImageNode { self.isUserInteractionEnabled = true } - override func didLoad() { + override public func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } @@ -29,7 +29,7 @@ final class ChatMessageDeliveryFailedNode: ASImageNode { } } - func updateLayout(theme: PresentationTheme) -> CGSize { + public func updateLayout(theme: PresentationTheme) -> CGSize { if self.theme !== theme { self.theme = theme self.image = PresentationResourcesChat.chatBubbleDeliveryFailedIcon(theme) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode/BUILD new file mode 100644 index 00000000000..8ac301a11ed --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageEventLogPreviousDescriptionContentNode", + module_name = "ChatMessageEventLogPreviousDescriptionContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift similarity index 71% rename from submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift index 40367667765..7f70a401efe 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -5,17 +5,20 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentNode -final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = visibility } } - required init() { + required public init() { self.contentNode = ChatMessageAttachedContentNode() super.init() @@ -23,11 +26,11 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble self.addSubnode(self.contentNode) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() return { item, layoutConstants, preparePosition, _, constrainedSize, _ in @@ -73,23 +76,23 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -97,14 +100,14 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble } }*/ } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode/BUILD new file mode 100644 index 00000000000..db002d4d41c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageEventLogPreviousLinkContentNode", + module_name = "ChatMessageEventLogPreviousLinkContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode/Sources/ChatMessageEventLogPreviousLinkContentNode.swift similarity index 71% rename from submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode/Sources/ChatMessageEventLogPreviousLinkContentNode.swift index ea50080731d..37502efe722 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode/Sources/ChatMessageEventLogPreviousLinkContentNode.swift @@ -5,17 +5,20 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentNode -final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = visibility } } - required init() { + required public init() { self.contentNode = ChatMessageAttachedContentNode() super.init() @@ -23,11 +26,11 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent self.addSubnode(self.contentNode) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() return { item, layoutConstants, preparePosition, _, constrainedSize, _ in @@ -68,23 +71,23 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -92,20 +95,17 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent } }*/ } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } return self.contentNode.transitionNode(media: media) } } - - - diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode/BUILD new file mode 100644 index 00000000000..0a07f4940ef --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageEventLogPreviousMessageContentNode", + module_name = "ChatMessageEventLogPreviousMessageContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode/Sources/ChatMessageEventLogPreviousMessageContentNode.swift similarity index 72% rename from submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode/Sources/ChatMessageEventLogPreviousMessageContentNode.swift index 4acb2a4ca15..73176c153f9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode/Sources/ChatMessageEventLogPreviousMessageContentNode.swift @@ -5,17 +5,20 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentNode -final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = visibility } } - required init() { + required public init() { self.contentNode = ChatMessageAttachedContentNode() super.init() @@ -23,11 +26,11 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont self.addSubnode(self.contentNode) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() return { item, layoutConstants, preparePosition, _, constrainedSize, _ in @@ -73,40 +76,40 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { let contentNodeFrame = self.contentNode.frame return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func updateTouchesAtPoint(_ point: CGPoint?) { + override public func updateTouchesAtPoint(_ point: CGPoint?) { let contentNodeFrame = self.contentNode.frame self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) }) } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/BUILD new file mode 100644 index 00000000000..80e45ae39ad --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageFileBubbleContentNode", + module_name = "ChatMessageFileBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift similarity index 81% rename from submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift index 39a9a6d0898..91267bd87f5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift @@ -8,11 +8,15 @@ import TelegramCore import TelegramUIPreferences import ComponentFlow import AudioTranscriptionButtonComponent +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageInteractiveFileNode -class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { - let interactiveFileNode: ChatMessageInteractiveFileNode +public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { + public let interactiveFileNode: ChatMessageInteractiveFileNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { var wasVisible = false if case .visible = oldValue { @@ -28,7 +32,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - required init() { + required public init() { self.interactiveFileNode = ChatMessageInteractiveFileNode() super.init() @@ -80,18 +84,18 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - override func accessibilityActivate() -> Bool { + override public func accessibilityActivate() -> Bool { if let item = self.item { let _ = item.controllerInteraction.openMessage(item.message, .default) } return true } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let interactiveFileLayout = self.interactiveFileNode.asyncLayout() return { item, layoutConstants, preparePosition, selection, constrainedSize, _ in @@ -103,7 +107,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } var incoming = item.message.effectivelyIncoming(item.context.account.peerId) - if case .forwardedMessages = item.associatedData.subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { incoming = false } let statusType: ChatMessageDateAndStatusType? @@ -129,6 +133,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments( context: item.context, presentationData: item.presentationData, + customTintColor: nil, message: item.message, topMessage: item.topMessage, associatedData: item.associatedData, @@ -144,6 +149,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { dateAndStatusType: statusType, displayReactions: true, messageSelection: item.message.groupingKey != nil ? selection : nil, + isAttachedContentBlock: false, layoutConstants: layoutConstants, constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height), controllerInteraction: item.controllerInteraction @@ -183,7 +189,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId { return self.interactiveFileNode.transitionNode(media: media) } else { @@ -191,48 +197,48 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { return self.interactiveFileNode.updateHiddenMedia(media) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func willUpdateIsExtractedToContextPreview(_ value: Bool) { + override public func willUpdateIsExtractedToContextPreview(_ value: Bool) { self.interactiveFileNode.willUpdateIsExtractedToContextPreview(value) } - override func updateIsExtractedToContextPreview(_ value: Bool) { + override public func updateIsExtractedToContextPreview(_ value: Bool) { self.interactiveFileNode.updateIsExtractedToContextPreview(value) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let result = self.interactiveFileNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.view), with: event) { return result } return super.hitTest(point, with: event) } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.interactiveFileNode.dateAndStatusNode.isHidden { return self.interactiveFileNode.dateAndStatusNode.reactionView(value: value) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 78cae803d23..9ff63e0b985 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -6,6 +6,7 @@ import Postbox import TelegramCore import TelegramPresentationData import LocalizedPeerData +import AccountContext public enum ChatMessageForwardInfoType: Equatable { case bubble(incoming: Bool) @@ -106,10 +107,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } } - public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { + public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) - return { presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in + return { context, presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0) let prefixFont = Font.regular(fontSize) let peerFont = Font.medium(fontSize) @@ -162,7 +163,15 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { completeSourceString = strings.Message_GenericForwardedPsa(peerString) } } else { - titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor + if incoming { + if let nameColor = peer?.nameColor { + titleColor = context.peerNameColors.get(nameColor, dark: presentationData.theme.theme.overallDarkAppearance).main + } else { + titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor + } + } else { + titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor + } if let storyData = storyData { switch storyData.storyType { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/BUILD new file mode 100644 index 00000000000..8f178acb21c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageGameBubbleContentNode", + module_name = "ChatMessageGameBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/Sources/ChatMessageGameBubbleContentNode.swift similarity index 73% rename from submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/Sources/ChatMessageGameBubbleContentNode.swift index d4c4d55f08f..1d9a30a9f1f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/Sources/ChatMessageGameBubbleContentNode.swift @@ -5,19 +5,22 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentNode -final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { private var game: TelegramMediaGame? private let contentNode: ChatMessageAttachedContentNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = visibility } } - required init() { + required public init() { self.contentNode = ChatMessageAttachedContentNode() super.init() @@ -30,18 +33,18 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - override func accessibilityActivate() -> Bool { + override public func accessibilityActivate() -> Bool { if let item = self.item { item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true, false) } return true } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() return { item, layoutConstants, preparePosition, _, constrainedSize, _ in @@ -102,23 +105,23 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -126,23 +129,23 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } }*/ } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } return self.contentNode.transitionNode(media: media) } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionView(value: value) + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + if let statusNode = self.contentNode.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD new file mode 100644 index 00000000000..ba1ade2779c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageGiftBubbleContentNode", + module_name = "ChatMessageGiftBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/LocalizedPeerData", + "//submodules/UrlEscaping", + "//submodules/TelegramStringFormatting", + "//submodules/WallpaperBackgroundNode", + "//submodules/ReactionSelectionNode", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/ShimmerEffect", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift similarity index 72% rename from submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 9eb69af77c4..a66d96e2dc3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -17,12 +17,15 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import ChatControllerInteraction import ShimmerEffect +import Markdown +import ChatMessageBubbleContentNode +import ChatMessageItemCommon 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) } -class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let labelNode: TextNode private var backgroundNode: WallpaperBubbleBackgroundNode? private var backgroundColorNode: ASDisplayNode @@ -36,6 +39,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode + private let shimmerEffectNode: ShimmerEffectForegroundNode private let buttonNode: HighlightTrackingButtonNode private let buttonStarsNode: PremiumStarsNode private let buttonTitleNode: TextNode @@ -45,7 +49,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var isPlaying: Bool = false - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = oldValue != .none let isVisible = self.visibility != .none @@ -67,7 +71,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var animationDisposable: Disposable? private var setupTimestamp: Double? - required init() { + required public init() { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false self.labelNode.displaysAsynchronously = false @@ -90,6 +94,9 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.clipsToBounds = true self.buttonNode.cornerRadius = 17.0 + + self.shimmerEffectNode = ShimmerEffectForegroundNode() + self.shimmerEffectNode.cornerRadius = 17.0 self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode.isUserInteractionEnabled = false @@ -113,6 +120,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.animationNode) self.addSubnode(self.buttonNode) + self.buttonNode.addSubnode(self.shimmerEffectNode) self.buttonNode.addSubnode(self.buttonStarsNode) self.addSubnode(self.buttonTitleNode) @@ -135,7 +143,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -148,6 +156,26 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { return } let _ = item.controllerInteraction.openMessage(item.message, .default) + self.startShimmering() + Queue.mainQueue().after(0.75) { + self.stopShimmering() + } + } + + func startShimmering() { + self.shimmerEffectNode.isHidden = false + self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let backgroundFrame = self.buttonNode.frame + self.shimmerEffectNode.frame = CGRect(origin: .zero, size: backgroundFrame.size) + self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size) + self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.2), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) + } + + func stopShimmering() { + self.shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.shimmerEffectNode.isHidden = true + }) } private func removePlaceholder(animated: Bool) { @@ -160,8 +188,8 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { }) } } - - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) @@ -173,43 +201,85 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in - let giftSize = CGSize(width: 220.0, height: 240.0) + var giftSize = CGSize(width: 220.0, height: 240.0) let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId) let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText - var duration: String = "" + var months: Int32 = 3 var animationName: String = "" + var title = item.presentationData.strings.Notification_PremiumGift_Title + var text = "" + var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View + var hasServiceMessage = true + var textSpacing: CGFloat = 0.0 for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .giftPremium(_, _, months, _, _): - duration = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string - switch months { - case 12: - animationName = "Gift12" - case 6: - animationName = "Gift6" - case 3: - animationName = "Gift3" - default: - animationName = "Gift3" + 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 + } 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 default: break } } } + switch months { + case 12: + animationName = "Gift12" + case 6: + animationName = "Gift6" + case 3: + animationName = "Gift3" + default: + animationName = "Gift3" + } + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_Title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: duration, font: Font.regular(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + giftSize.height = titleLayout.size.height + subtitleLayout.size.height + 225.0 + var labelRects = labelLayout.linesRects() if labelRects.count > 1 { let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) @@ -233,14 +303,23 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let backgroundMaskImage: (CGPoint, UIImage)? var backgroundMaskUpdated = false - if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects { - backgroundMaskImage = (currentOffset, currentImage) + if hasServiceMessage { + if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects { + backgroundMaskImage = (currentOffset, currentImage) + } else { + backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false) + backgroundMaskUpdated = true + } } else { - backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false) - backgroundMaskUpdated = true + backgroundMaskImage = nil } - let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + giftSize.height + 18.0) + var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height) + if hasServiceMessage { + backgroundSize.height += labelLayout.size.height + 18.0 + } else { + backgroundSize.height += 4.0 + } return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in @@ -253,9 +332,11 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateVisibility() + strongSelf.labelNode.isHidden = !hasServiceMessage + strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: labelLayout.size.height + 16.0), size: giftSize) + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame @@ -278,7 +359,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY - 1.0), size: subtitleLayout.size) + let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) strongSelf.subtitleNode.frame = subtitleFrame let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) @@ -347,7 +428,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if let mediaBackgroundContent = self.mediaBackgroundContent { @@ -367,19 +448,19 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } - override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } - override func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { if let backgroundNode = self.backgroundNode { backgroundNode.offsetSpring(value: value, duration: duration, damping: damping) } } - override func updateTouchesAtPoint(_ point: CGPoint?) { + override public func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [(CGRect, CGRect)]? let textNodeFrame = self.labelNode.frame @@ -431,7 +512,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.labelNode.frame if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -439,28 +520,30 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .textMention(peerName) + return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return .botCommand(botCommand) + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) } } - if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { - return .openMessage + if self.buttonNode.frame.contains(point) { + return ChatMessageBubbleContentTapAction(content: .ignore) + } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { + return ChatMessageBubbleContentTapAction(content: .openMessage) } else if self.mediaBackgroundNode.frame.contains(point) { - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } - override func unreadMessageRangeUpdated() { + override public func unreadMessageRangeUpdated() { self.updateVisibility() } @@ -497,6 +580,12 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) self.animationNode.playOnce() + + Queue.mainQueue().after(0.05) { + if let itemNode = self.itemNode, let supernode = itemNode.supernode { + supernode.addSubnode(itemNode) + } + } } if !alreadySeen && self.animationNode.isPlaying { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/BUILD new file mode 100644 index 00000000000..12c33d46025 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageGiveawayBubbleContentNode", + module_name = "ChatMessageGiveawayBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/PhoneNumberFormat", + "//submodules/TelegramStringFormatting", + "//submodules/Markdown", + "//submodules/ShimmerEffect", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift new file mode 100644 index 00000000000..e48515884e2 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -0,0 +1,831 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import AvatarNode +import AccountContext +import PhoneNumberFormat +import TelegramStringFormatting +import Markdown +import ShimmerEffect +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentButtonNode +import UndoUI + +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 { + private let dateAndStatusNode: ChatMessageDateAndStatusNode + + private let placeholderNode: StickerShimmerEffectNode + private let animationNode: AnimatedStickerNode + + private let prizeTitleNode: TextNode + private let prizeTextNode: TextNode + + private let participantsTitleNode: TextNode + private let participantsTextNode: TextNode + + private let countriesTextNode: TextNode + + private let dateTitleNode: TextNode + private let dateTextNode: TextNode + + private let badgeBackgroundNode: ASImageNode + private let badgeTextNode: TextNode + + private var giveaway: TelegramMediaGiveaway? + + private let buttonNode: ChatMessageAttachedContentButtonNode + private let channelButtons: PeerButtonsStackNode + + override public var visibility: ListViewItemNodeVisibility { + didSet { + let wasVisible = oldValue != .none + let isVisible = self.visibility != .none + + if wasVisible != isVisible { + self.visibilityStatus = isVisible + } + } + } + + private var visibilityStatus: Bool? { + didSet { + if self.visibilityStatus != oldValue { + self.updateVisibility() + } + } + } + + private var setupTimestamp: Double? + + required public init() { + self.placeholderNode = StickerShimmerEffectNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.alpha = 0.75 + + self.animationNode = DefaultAnimatedStickerNodeImpl() + + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + self.prizeTitleNode = TextNode() + self.prizeTextNode = TextNode() + + self.participantsTitleNode = TextNode() + self.participantsTextNode = TextNode() + + self.countriesTextNode = TextNode() + + self.dateTitleNode = TextNode() + self.dateTextNode = TextNode() + + self.badgeBackgroundNode = ASImageNode() + self.badgeBackgroundNode.displaysAsynchronously = false + + self.badgeTextNode = TextNode() + + self.buttonNode = ChatMessageAttachedContentButtonNode() + self.channelButtons = PeerButtonsStackNode() + + super.init() + + self.addSubnode(self.prizeTitleNode) + self.addSubnode(self.prizeTextNode) + self.addSubnode(self.participantsTitleNode) + self.addSubnode(self.participantsTextNode) + self.addSubnode(self.countriesTextNode) + self.addSubnode(self.dateTitleNode) + self.addSubnode(self.dateTextNode) + self.addSubnode(self.buttonNode) + self.addSubnode(self.channelButtons) + self.addSubnode(self.animationNode) + self.addSubnode(self.badgeBackgroundNode) + self.addSubnode(self.badgeTextNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + + self.dateAndStatusNode.reactionSelected = { [weak self] value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) + } + + self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceView, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceView, gesture, value) + } + + self.channelButtons.openPeer = { [weak self] peer in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) + } + } + + override public func accessibilityActivate() -> Bool { + self.buttonPressed() + return true + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func didLoad() { + super.didLoad() + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.bubbleTap(_:))) + self.view.addGestureRecognizer(tapRecognizer) + } + + @objc private func bubbleTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let item = self.item else { + return + } + item.controllerInteraction.displayGiveawayParticipationStatus(item.message.id) + } + + private func removePlaceholder(animated: Bool) { + self.placeholderNode.alpha = 0.0 + if !animated { + self.placeholderNode.removeFromSupernode() + } else { + self.placeholderNode.layer.animateAlpha(from: self.placeholderNode.alpha, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.placeholderNode.removeFromSupernode() + }) + } + } + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let statusLayout = self.dateAndStatusNode.asyncLayout() + let makePrizeTitleLayout = TextNode.asyncLayout(self.prizeTitleNode) + let makePrizeTextLayout = TextNode.asyncLayout(self.prizeTextNode) + + let makeParticipantsTitleLayout = TextNode.asyncLayout(self.participantsTitleNode) + let makeParticipantsTextLayout = TextNode.asyncLayout(self.participantsTextNode) + + let makeCountriesTextLayout = TextNode.asyncLayout(self.countriesTextNode) + + let makeDateTitleLayout = TextNode.asyncLayout(self.dateTitleNode) + let makeDateTextLayout = TextNode.asyncLayout(self.dateTextNode) + + let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) + + let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) + + let makeChannelsLayout = PeerButtonsStackNode.asyncLayout(self.channelButtons) + + let currentItem = self.item + + return { item, layoutConstants, _, _, constrainedSize, _ in + var giveaway: TelegramMediaGiveaway? + for media in item.message.media { + if let media = media as? TelegramMediaGiveaway { + giveaway = media; + } + } + + var themeUpdated = false + if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme { + themeUpdated = true + } + + var incoming = item.message.effectivelyIncoming(item.context.account.peerId) + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { + incoming = false + } + + 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 accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor + + var updatedBadgeImage: UIImage? + if themeUpdated { + 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: .white) + + let prizeTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor) + var prizeTextString: NSAttributedString? + if let giveaway { + prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_PrizeText( + item.presentationData.strings.Chat_Giveaway_Message_Subscriptions(giveaway.quantity), + item.presentationData.strings.Chat_Giveaway_Message_Months(giveaway.months) + ).string, 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) + } + + let participantsTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle, font: titleFont, textColor: textColor) + let participantsText: String + let countriesText: String + + if let giveaway { + if giveaway.flags.contains(.onlyNewSubscribers) { + if giveaway.channelPeerIds.count > 1 { + participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNewMany + } else { + participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNew + } + } else { + if giveaway.channelPeerIds.count > 1 { + participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsMany + } else { + participantsText = item.presentationData.strings.Chat_Giveaway_Message_Participants + } + } + if !giveaway.countries.isEmpty { + let locale = localeWithStrings(item.presentationData.strings) + let countryNames = giveaway.countries.map { id in + if let countryName = locale.localizedString(forRegionCode: id) { + return "\(flagEmoji(countryCode: id))\u{feff}\(countryName)" + } else { + return id + } + } + var countries: String = "" + if countryNames.count == 1, let country = countryNames.first { + countries = country + } else { + for i in 0 ..< countryNames.count { + countries.append(countryNames[i]) + if i == countryNames.count - 2 { + countries.append(item.presentationData.strings.Chat_Giveaway_Message_CountriesLastDelimiter) + } else if i < countryNames.count - 2 { + countries.append(item.presentationData.strings.Chat_Giveaway_Message_CountriesDelimiter) + } + } + } + countriesText = item.presentationData.strings.Chat_Giveaway_Message_CountriesFrom(countries).string + } else { + countriesText = "" + } + } else { + participantsText = "" + countriesText = "" + } + + 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 dateTextString: NSAttributedString? + if let giveaway { + dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor) + } + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + let sideInsets = layoutConstants.text.bubbleInsets.right * 2.0 + let maxTextWidth = min(200.0, max(1.0, constrainedSize.width - 7.0 - sideInsets)) + + 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 (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())) + let (participantsTextLayout, participantsTextApply) = makeParticipantsTextLayout(TextNodeLayoutArguments(attributedString: participantsTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let (countriesTextLayout, countriesTextApply) = makeCountriesTextLayout(TextNodeLayoutArguments(attributedString: countriesTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let (dateTitleLayout, dateTitleApply) = makeDateTitleLayout(TextNodeLayoutArguments(attributedString: dateTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (dateTextLayout, dateTextApply) = makeDateTextLayout(TextNodeLayoutArguments(attributedString: dateTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: item.associatedData.accountPeer, message: item.message) + if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { + dateReactionsAndPeers = ([], []) + } + for attribute in item.message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = !attribute.isHidden + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { + if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } + } + + 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 statusType: ChatMessageDateAndStatusType? + switch position { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if incoming { + statusType = .BubbleIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } + } + default: + statusType = nil + } + + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? + if let statusType = statusType { + var isReplyThread = false + if case .replyThread = item.chatLocation { + isReplyThread = true + } + + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + 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), + constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude), + availableReactions: item.associatedData.availableReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, + displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) + } + + let titleColor: UIColor + if incoming { + titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor + } else { + titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor + } + + 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" + } + + var maxContentWidth: CGFloat = 0.0 + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + maxContentWidth = max(maxContentWidth, statusSuggestedWidthAndContinue.0) + } + maxContentWidth = max(maxContentWidth, prizeTitleLayout.size.width) + maxContentWidth = max(maxContentWidth, prizeTextLayout.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] = [] + if let channelPeerIds = giveaway?.channelPeerIds { + for peerId in channelPeerIds { + if let peer = item.message.peers[peerId] { + channelPeers.append(EnginePeer(peer)) + } + } + } + let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 240.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1)) + maxContentWidth = max(maxContentWidth, channelsWidth) + maxContentWidth += 30.0 + + let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0 + + return (contentWidth, { boundingWidth in + 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 statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) + + var layoutSize = CGSize(width: contentWidth, 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) + + if countriesTextLayout.size.height > 0.0 { + layoutSize.height += countriesTextLayout.size.height + 7.0 + } + layoutSize.height += channelButtonsSize.height + + if let statusSizeAndApply = statusSizeAndApply { + layoutSize.height += statusSizeAndApply.0.height - 4.0 + } + let buttonFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutSize.height - 9.0 - buttonSize.height), size: buttonSize) + + return (layoutSize, { [weak self] animation, synchronousLoads, _ in + if let strongSelf = self { + if strongSelf.item == nil { + strongSelf.animationNode.autoplay = 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 + + strongSelf.updateVisibility() + + let _ = badgeTextApply() + let _ = prizeTitleApply() + let _ = prizeTextApply() + let _ = participantsTitleApply() + let _ = participantsTextApply() + let _ = countriesTextApply() + let _ = dateTitleApply() + let _ = dateTextApply() + let _ = channelButtonsApply() + let _ = buttonApply(animation) + + let smallSpacing: CGFloat = 2.0 + let largeSpacing: CGFloat = 14.0 + + 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) + 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) + strongSelf.badgeTextNode.frame = badgeTextFrame + strongSelf.badgeBackgroundNode.frame = badgeTextFrame.insetBy(dx: -6.0, dy: -5.0).offsetBy(dx: -1.0, dy: 0.0) + if let updatedBadgeImage { + strongSelf.badgeBackgroundNode.image = updatedBadgeImage + } + + 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 + 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 + 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 + + strongSelf.channelButtons.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - channelButtonsSize.width) / 2.0), y: originY), size: channelButtonsSize) + originY += channelButtonsSize.height + + if countriesTextLayout.size.height > 0.0 { + originY += smallSpacing * 2.0 + 3.0 + 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 + + 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 + strongSelf.dateTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTextLayout.size.width) / 2.0), y: originY), size: dateTextLayout.size) + originY += dateTextLayout.size.height + largeSpacing + + strongSelf.buttonNode.frame = buttonFrame + + if let statusSizeAndApply = statusSizeAndApply { + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: strongSelf.dateTextNode.frame.maxY + 2.0), size: statusSizeAndApply.0) + if strongSelf.dateAndStatusNode.supernode == nil { + strongSelf.addSubnode(strongSelf.dateAndStatusNode) + statusSizeAndApply.1(.None) + } else { + statusSizeAndApply.1(animation) + } + } else if strongSelf.dateAndStatusNode.supernode != nil { + strongSelf.dateAndStatusNode.removeFromSupernode() + } + + if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { + strongSelf.dateAndStatusNode.pressed = { + guard let strongSelf = self else { + return + } + item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode) + } + } else { + strongSelf.dateAndStatusNode.pressed = nil + } + + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + } + }) + }) + }) + } + } + + private func updateVisibility() { + } + + private var absoluteRect: (CGRect, CGSize)? + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteRect = (rect, containerSize) + + self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize) + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.channelButtons.frame.contains(point) { + return ChatMessageBubbleContentTapAction(content: .ignore) + } + if self.buttonNode.frame.contains(point) { + return ChatMessageBubbleContentTapAction(content: .ignore) + } + if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { + return ChatMessageBubbleContentTapAction(content: .ignore) + } + return ChatMessageBubbleContentTapAction(content: .none) + } + + @objc private func buttonPressed() { + if let item = self.item { + let _ = item.controllerInteraction.openMessage(item.message, .default) + } + } + + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + if !self.dateAndStatusNode.isHidden { + return self.dateAndStatusNode.reactionView(value: value) + } + return nil + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) { + return result + } + return super.hitTest(point, with: event) + } +} + +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) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonsStackNode)) { + let currentChannelButtons = current.buttonNodes.isEmpty ? nil : current.buttonNodes + let maybeMakeChannelButtons = current.buttonNodes.map(PeerButtonNode.asyncLayout) + + return { context, width, peers, titleColor, backgroundColor in + let targetNode = current + + var buttonNodes: [PeerButtonNode] = [] + let makeChannelButtonLayouts: [(_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonNode))] + if let currentChannelButtons { + buttonNodes = currentChannelButtons + makeChannelButtonLayouts = maybeMakeChannelButtons + } else { + for _ in peers { + buttonNodes.append(PeerButtonNode()) + } + makeChannelButtonLayouts = buttonNodes.map(PeerButtonNode.asyncLayout) + } + + var maxWidth = 0.0 + let buttonHeight: CGFloat = 24.0 + let horizontalButtonSpacing: CGFloat = 4.0 + let verticalButtonSpacing: CGFloat = 6.0 + + var sizes: [CGSize] = [] + var groups: [[Int]] = [] + var currentGroup: [Int] = [] + + var buttonContinues: [(CGFloat) -> (CGSize, () -> PeerButtonNode)] = [] + for i in 0 ..< makeChannelButtonLayouts.count { + let peer = peers[i] + let makeChannelButtonLayout = makeChannelButtonLayouts[i] + + let (buttonWidth, buttonContinue) = makeChannelButtonLayout(context, width, peer, titleColor, backgroundColor) + sizes.append(CGSize(width: buttonWidth, height: buttonHeight)) + buttonContinues.append(buttonContinue) + + var itemsWidth: CGFloat = 0.0 + for j in currentGroup { + itemsWidth += sizes[j].width + } + itemsWidth += buttonWidth + itemsWidth += CGFloat(currentGroup.count) * horizontalButtonSpacing + + if itemsWidth > width { + groups.append(currentGroup) + currentGroup = [] + } + currentGroup.append(i) + } + if !currentGroup.isEmpty { + groups.append(currentGroup) + } + + var rowWidths: [CGFloat] = [] + for group in groups { + var rowWidth: CGFloat = 0.0 + for i in group { + let buttonSize = sizes[i] + rowWidth += buttonSize.width + } + rowWidth += CGFloat(currentGroup.count) * horizontalButtonSpacing + + if rowWidth > maxWidth { + maxWidth = rowWidth + } + rowWidths.append(rowWidth) + } + + var frames: [CGRect] = [] + var originY: CGFloat = 0.0 + for i in 0 ..< groups.count { + let rowWidth = rowWidths[i] + var originX = floorToScreenPixels((maxWidth - rowWidth) / 2.0) + + for i in groups[i] { + let buttonSize = sizes[i] + frames.append(CGRect(origin: CGPoint(x: originX, y: originY), size: buttonSize)) + originX += buttonSize.width + horizontalButtonSpacing + } + originY += buttonHeight + verticalButtonSpacing + } + + return (maxWidth, { _ in + var buttonLayoutsAndApply: [(CGSize, () -> PeerButtonNode)] = [] + for buttonApply in buttonContinues { + buttonLayoutsAndApply.append(buttonApply(maxWidth)) + } + + return (CGSize(width: maxWidth, height: originY - verticalButtonSpacing), { + targetNode.buttonNodes = buttonNodes + + for i in 0 ..< buttonNodes.count { + let peer = peers[i] + let buttonNode = buttonNodes[i] + buttonNode.pressed = { [weak targetNode] in + targetNode?.openPeer(peer) + } + if buttonNode.supernode == nil { + targetNode.addSubnode(buttonNode) + } + let frame = frames[i] + buttonNode.frame = frame + } + + for (_, apply) in buttonLayoutsAndApply { + let _ = apply() + } + + return targetNode + }) + }) + } + } +} + +private final class PeerButtonNode: HighlightTrackingButtonNode { + private let backgroundNode: ASImageNode + private let textNode: TextNode + private let avatarNode: AvatarNode + + var currentBackgroundColor: UIColor? + var pressed: (() -> Void)? + + init() { + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + self.addSubnode(self.avatarNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc func buttonPressed() { + self.pressed?() + } + + static func asyncLayout(_ current: PeerButtonNode?) -> (_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonNode)) { + let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) + + return { context, width, peer, titleColor, backgroundColor in + let targetNode: PeerButtonNode + if let current = current { + targetNode = current + } else { + targetNode = PeerButtonNode() + } + + let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) + if let maybeMakeTextLayout = maybeMakeTextLayout { + makeTextLayout = maybeMakeTextLayout + } else { + makeTextLayout = TextNode.asyncLayout(targetNode.textNode) + } + + let inset: CGFloat = 1.0 + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 5.0 + + 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 (CGSize(width: refinedWidth, height: 24.0), { + let _ = textApply() + + 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 + + if targetNode.currentBackgroundColor != backgroundColor { + targetNode.currentBackgroundColor = backgroundColor + targetNode.backgroundNode.image = generateStretchableFilledCircleImage(radius: 12.0, color: backgroundColor, backgroundColor: nil) + } + + targetNode.avatarNode.setPeer( + context: context, + theme: context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: peer, + synchronousLoad: false + ) + targetNode.avatarNode.frame = CGRect(origin: CGPoint(x: inset, y: inset), size: avatarSize) + + targetNode.textNode.frame = textFrame + + return targetNode + }) + }) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/BUILD new file mode 100644 index 00000000000..ce0497ba6e4 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageInstantVideoBubbleContentNode", + module_name = "ChatMessageInstantVideoBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift similarity index 88% rename from submodules/TelegramUI/Sources/ChatMessageInstantVideoBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift index a236a18bace..17c093c9220 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift @@ -8,10 +8,29 @@ import TelegramCore import TelegramUIPreferences import ComponentFlow import AudioTranscriptionButtonComponent +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageInteractiveInstantVideoNode +import ChatMessageInteractiveFileNode -class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { - let interactiveFileNode: ChatMessageInteractiveFileNode - let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode +extension ChatMessageInteractiveInstantVideoNode.AnimateFileNodeDescription { + convenience init(_ node: ChatMessageInteractiveFileNode) { + self.init( + node: node, + textClippingNode: node.textClippingNode, + dateAndStatusNode: node.dateAndStatusNode, + fetchingTextNode: node.fetchingTextNode, + waveformView: node.waveformView, + statusNode: node.statusNode, + audioTranscriptionButton: node.audioTranscriptionButton + ) + } +} + +public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { + public let interactiveFileNode: ChatMessageInteractiveFileNode + public let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode private let maskLayer = SimpleLayer() private let maskForeground = SimpleLayer() @@ -23,7 +42,7 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed - var hasExpandedAudioTranscription: Bool { + public var hasExpandedAudioTranscription: Bool { if case .expanded = self.audioTranscriptionState { return true } else { @@ -31,7 +50,7 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { } } - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { var wasVisible = false if case .visible = oldValue { @@ -60,7 +79,7 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { return isVisible } - required init() { + required public init() { self.interactiveFileNode = ChatMessageInteractiveFileNode() self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode() @@ -145,18 +164,18 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { } } - override func accessibilityActivate() -> Bool { + override public func accessibilityActivate() -> Bool { if let item = self.item { let _ = item.controllerInteraction.openMessage(item.message, .default) } return true } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let interactiveVideoLayout = self.interactiveVideoNode.asyncLayout() let interactiveFileLayout = self.interactiveFileNode.asyncLayout() @@ -173,7 +192,7 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { } var incoming = item.message.effectivelyIncoming(item.context.account.peerId) - if case .forwardedMessages = item.associatedData.subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { incoming = false } @@ -200,6 +219,7 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments( context: item.context, presentationData: item.presentationData, + customTintColor: nil, message: item.message, topMessage: item.topMessage, associatedData: item.associatedData, @@ -215,6 +235,7 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { dateAndStatusType: statusType, displayReactions: false, messageSelection: item.message.groupingKey != nil ? selection : nil, + isAttachedContentBlock: false, layoutConstants: layoutConstants, constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height), controllerInteraction: item.controllerInteraction @@ -352,9 +373,9 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { if currentExpanded != isExpanded { if isExpanded { - strongSelf.interactiveVideoNode.animateTo(strongSelf.interactiveFileNode, animator: animation.animator) + strongSelf.interactiveVideoNode.animateTo(ChatMessageInteractiveInstantVideoNode.AnimateFileNodeDescription(strongSelf.interactiveFileNode), animator: animation.animator) } else { - strongSelf.interactiveVideoNode.animateFrom(strongSelf.interactiveFileNode, animator: animation.animator) + strongSelf.interactiveVideoNode.animateFrom(ChatMessageInteractiveInstantVideoNode.AnimateFileNodeDescription(strongSelf.interactiveFileNode), animator: animation.animator) } } } @@ -364,55 +385,55 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { return false } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.interactiveVideoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.interactiveVideoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.interactiveVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func willUpdateIsExtractedToContextPreview(_ value: Bool) { + override public func willUpdateIsExtractedToContextPreview(_ value: Bool) { self.interactiveFileNode.willUpdateIsExtractedToContextPreview(value) } - override func updateIsExtractedToContextPreview(_ value: Bool) { + override public func updateIsExtractedToContextPreview(_ value: Bool) { self.interactiveFileNode.updateIsExtractedToContextPreview(value) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if !self.interactiveFileNode.isHidden { if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } } if !self.interactiveVideoNode.isHidden { if self.interactiveVideoNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveVideoNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveVideoNode.dateAndStatusNode.view), with: nil) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } if let audioTranscriptionButton = self.interactiveVideoNode.audioTranscriptionButton, let _ = audioTranscriptionButton.hitTest(self.view.convert(point, to: audioTranscriptionButton), with: nil) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } } return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.isExpanded, let result = self.interactiveFileNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.view), with: event) { return result } @@ -422,18 +443,18 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { return super.hitTest(point, with: event) } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.interactiveVideoNode.dateAndStatusNode.isHidden { return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value) } return nil } - override func targetForStoryTransition(id: StoryId) -> UIView? { + override public func targetForStoryTransition(id: StoryId) -> UIView? { return self.interactiveVideoNode.targetForStoryTransition(id: id) } - override var disablesClipping: Bool { + override public var disablesClipping: Bool { return true } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/BUILD new file mode 100644 index 00000000000..5716eabe195 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/BUILD @@ -0,0 +1,45 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageInstantVideoItemNode", + module_name = "ChatMessageInstantVideoItemNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/ContextUI", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageShareButton", + "//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode", + "//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer", + "//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift similarity index 88% rename from submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 93b65198611..92dc14d03c5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -14,53 +14,68 @@ import ContextUI import Markdown import ChatControllerInteraction import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageBubbleContentNode +import ChatMessageReplyInfoNode +import ChatMessageInteractiveInstantVideoNode +import ChatMessageItem +import ChatMessageItemView +import ChatMessageSwipeToReplyNode +import ChatMessageSelectionNode +import ChatMessageDeliveryFailedNode +import ChatMessageShareButton +import ChatMessageActionButtonsNode +import ChatSwipeToReplyRecognizer +import ChatMessageReactionsFooterContentNode private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont -class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerDelegate { - let contextSourceNode: ContextExtractedContentContainingNode - let containerNode: ContextControllerSourceNode - let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode +public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerDelegate { + public let contextSourceNode: ContextExtractedContentContainingNode + public let containerNode: ContextControllerSourceNode + public let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode - var selectionNode: ChatMessageSelectionNode? - var deliveryFailedNode: ChatMessageDeliveryFailedNode? - var shareButtonNode: ChatMessageShareButton? + public var selectionNode: ChatMessageSelectionNode? + public var deliveryFailedNode: ChatMessageDeliveryFailedNode? + public var shareButtonNode: ChatMessageShareButton? - var swipeToReplyNode: ChatMessageSwipeToReplyNode? - var swipeToReplyFeedback: HapticFeedback? + public var swipeToReplyNode: ChatMessageSwipeToReplyNode? + public var swipeToReplyFeedback: HapticFeedback? - var appliedParams: ListViewItemLayoutParams? - var appliedItem: ChatMessageItem? - var appliedForwardInfo: (Peer?, String?)? - var appliedHasAvatar = false - var appliedCurrentlyPlaying: Bool? - var appliedAutomaticDownload = false - var avatarOffset: CGFloat? + public var appliedParams: ListViewItemLayoutParams? + public var appliedItem: ChatMessageItem? + public var appliedForwardInfo: (Peer?, String?)? + public var appliedHasAvatar = false + public var appliedCurrentlyPlaying: Bool? + public var appliedAutomaticDownload = false + public var avatarOffset: CGFloat? - var animatingHeight: Bool { + public var animatingHeight: Bool { return self.apparentHeightTransition != nil } - var viaBotNode: TextNode? - var replyInfoNode: ChatMessageReplyInfoNode? - var replyBackgroundNode: NavigationBackgroundNode? - var forwardInfoNode: ChatMessageForwardInfoNode? + public var viaBotNode: TextNode? + public var replyInfoNode: ChatMessageReplyInfoNode? + public var replyBackgroundNode: NavigationBackgroundNode? + public var forwardInfoNode: ChatMessageForwardInfoNode? - var actionButtonsNode: ChatMessageActionButtonsNode? - var reactionButtonsNode: ChatMessageReactionButtonsNode? + public var actionButtonsNode: ChatMessageActionButtonsNode? + public var reactionButtonsNode: ChatMessageReactionButtonsNode? - let messageAccessibilityArea: AccessibilityAreaNode + public let messageAccessibilityArea: AccessibilityAreaNode - var currentSwipeToReplyTranslation: CGFloat = 0.0 + public var currentSwipeToReplyTranslation: CGFloat = 0.0 - var recognizer: TapLongTapOrDoubleTapGestureRecognizer? - - var currentSwipeAction: ChatControllerInteractionSwipeAction? + public var recognizer: TapLongTapOrDoubleTapGestureRecognizer? + + private var replyRecognizer: ChatSwipeToReplyRecognizer? + public var currentSwipeAction: ChatControllerInteractionSwipeAction? - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = oldValue != .none let isVisible = self.visibility != .none @@ -74,7 +89,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD fileprivate var wasPlaying = false - required init() { + required public init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode() @@ -127,9 +142,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD switch action { case .action, .optionalAction: break - case let .openContextMenu(tapMessage, selectAll, subFrame): + case let .openContextMenu(openContextMenu): strongSelf.recognizer?.cancel() - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, strongSelf, subFrame, gesture, nil) + item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, strongSelf, openContextMenu.subFrame, gesture, nil) if (strongSelf.appliedCurrentlyPlaying ?? false) && strongSelf.interactiveVideoNode.isPlaying { strongSelf.wasPlaying = true strongSelf.interactiveVideoNode.pause() @@ -167,11 +182,11 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func didLoad() { + override public func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) @@ -220,10 +235,18 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } return false } + if let item = self.item { + let _ = item + replyRecognizer.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + self.view.disablesInteractiveTransitionGestureRecognizer = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + } + self.replyRecognizer = replyRecognizer self.view.addGestureRecognizer(replyRecognizer) + + self.view.disablesInteractiveTransitionGestureRecognizer = true } - override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { super.updateAccessibilityData(accessibilityData) self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label @@ -254,7 +277,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let layoutConstants = self.layoutConstants let makeVideoLayout = self.interactiveVideoNode.asyncLayout() @@ -363,7 +386,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject { needsShareButton = false } @@ -405,7 +428,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) var viaBotApply: (TextNodeLayout, () -> TextNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode)? var updatedReplyBackgroundNode: NavigationBackgroundNode? var replyMarkup: ReplyMarkupMessageAttribute? @@ -432,6 +455,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } var replyMessage: Message? + var replyForward: QuotedReplyMessageAttribute? + var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -474,6 +499,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let _ = attribute as? InlineBotMessageAttribute { @@ -482,13 +510,15 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - if replyMessage != nil || replyStory != nil { + if replyMessage != nil || replyForward != nil || replyStory != nil { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, strings: item.presentationData.strings, context: item.context, type: .standalone, message: replyMessage, + replyForward: replyForward, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude), @@ -537,7 +567,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } let availableWidth = max(60.0, availableContentWidth - normalDisplaySize.width + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) } if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil { @@ -617,6 +647,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD strongSelf.appliedCurrentlyPlaying = isPlaying strongSelf.appliedAutomaticDownload = automaticDownload + strongSelf.replyRecognizer?.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + strongSelf.view.disablesInteractiveTransitionGestureRecognizer = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + strongSelf.updateAccessibilityData(accessibilityData) let videoLayoutData: ChatMessageInstantVideoItemLayoutData @@ -728,13 +761,16 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(synchronousLoads) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) + replyBackgroundFrame = replyInfoFrame + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads, animation) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) @@ -743,10 +779,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD strongSelf.replyInfoNode = nil } - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.frame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)) - 4.0, y: 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame { + replyBackgroundNode.frame = replyBackgroundFrame - let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 + let cornerRadius = 4.0 replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) } @@ -894,7 +930,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { @@ -904,7 +940,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } switch action { case let .action(f): - f() + f.action() case let .optionalAction(f): f() case .openContextMenu: @@ -927,7 +963,11 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) + }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .action(InternalBubbleTapAction.Action { + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) }) } } @@ -946,7 +986,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -955,7 +995,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } if forwardInfoNode.hasAction(at: self.view.convert(location, to: forwardInfoNode.view)) { - return .action({}) + return .action(InternalBubbleTapAction.Action {}) } else { return .optionalAction(performAction) } @@ -964,7 +1004,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return nil case .longTap, .doubleTap, .secondaryTap: if let item = self.item, self.interactiveVideoNode.frame.contains(location) { - return .openContextMenu(tapMessage: item.message, selectAll: false, subFrame: self.interactiveVideoNode.frame) + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.message, selectAll: false, subFrame: self.interactiveVideoNode.frame)) } case .hold: break @@ -972,7 +1012,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return nil } - @objc func shareButtonPressed() { + @objc private func shareButtonPressed() { if let item = self.item { if case .pinnedMessages = item.associatedData.subject { item.controllerInteraction.navigateToMessageStandalone(item.content.firstMessage.id) @@ -993,7 +1033,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } @@ -1004,13 +1044,16 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } private var playedSwipeToReplyHaptic = false - @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 + var leftOffset: CGFloat = 0.0 var swipeOffset: CGFloat = 45.0 if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { offset = -24.0 + leftOffset = -10.0 } else { offset = 10.0 + leftOffset = -10.0 swipeOffset = 60.0 } @@ -1024,8 +1067,26 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } self.item?.controllerInteraction.cancelInteractiveKeyboardGestures() case .changed: + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + if offset < bandingStart { + return offset + } + let range: CGFloat = 100.0 + let coefficient: CGFloat = 0.4 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range + } + var translation = recognizer.translation(in: self.view) - translation.x = max(-80.0, min(0.0, translation.x)) + if translation.x < 0.0 { + translation.x = max(-80.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) + } else { + if recognizer.allowBothDirections { + translation.x = -max(-80.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) + } else { + translation.x = 0.0 + } + } if let item = self.item, self.swipeToReplyNode == nil { let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), backgroundNode: item.controllerInteraction.presentationContext.backgroundNode, action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) @@ -1042,7 +1103,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD if let swipeToReplyNode = self.swipeToReplyNode { swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) - swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + if translation.x < 0.0 { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + } else { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0) + } if let (rect, containerSize) = self.absoluteRect { let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) @@ -1061,7 +1128,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD self.swipeToReplyFeedback = nil let translation = recognizer.translation(in: self.view) - if case .ended = recognizer.state, translation.x < -swipeOffset { + let gestureRecognized: Bool + if recognizer.allowBothDirections { + gestureRecognized = abs(translation.x) > swipeOffset + } else { + gestureRecognized = translation.x < -swipeOffset + } + if case .ended = recognizer.state, gestureRecognized { if let item = self.item { if let currentSwipeAction = currentSwipeAction { switch currentSwipeAction { @@ -1093,7 +1166,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } @@ -1103,7 +1176,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return super.hitTest(point, with: event) } - override func updateSelectionState(animated: Bool) { + override public func updateSelectionState(animated: Bool) { guard let item = self.item else { return } @@ -1172,29 +1245,29 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - override func cancelInsertionAnimations() { + override public func cancelInsertionAnimations() { self.layer.removeAllAnimations() } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + public func animateFromLoadingPlaceholder(delay: Double, transition: ContainedViewLayoutTransition) { guard let item = self.item else { return } @@ -1204,24 +1277,24 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) } - func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) { + public func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) { snapshotView.frame = self.interactiveVideoNode.view.convert(snapshotView.frame, from: self.contextSourceNode.contentNode.view) self.interactiveVideoNode.animateFromSnapshot(snapshotView: snapshotView, transition: transition) } - override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + override public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.interactiveVideoNode.playMediaWithSound() } - override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { + override public func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { return self.contextSourceNode } - override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + override public func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } - override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { + override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { super.animateFrameTransition(progress, currentValue) guard let item = self.appliedItem, let params = self.appliedParams, progress > 0.0, let (initialHeight, targetHeight) = self.apparentHeightTransition, !targetHeight.isZero && !initialHeight.isZero else { @@ -1359,7 +1432,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - override func openMessageContextMenu() { + override public func openMessageContextMenu() { guard let item = self.item else { return } @@ -1367,7 +1440,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } private var absoluteRect: (CGRect, CGSize)? - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) var rect = rect @@ -1398,13 +1471,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let reactionButtonsNode = self.reactionButtonsNode { reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } - override func targetReactionView(value: MessageReaction.Reaction) -> UIView? { + override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result } @@ -1414,7 +1487,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return nil } - override func targetForStoryTransition(id: StoryId) -> UIView? { + override public func targetForStoryTransition(id: StoryId) -> UIView? { guard let item = self.item else { return nil } @@ -1430,7 +1503,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return nil } - override func contentFrame() -> CGRect { + override public func contentFrame() -> CGRect { return self.interactiveVideoNode.frame } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD new file mode 100644 index 00000000000..31c4d55db6d --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD @@ -0,0 +1,57 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +NGDEPS = [ + "//Nicegram/NGStrings:NGStrings", + "//Nicegram/NGTelegramIntegration:NGTelegramIntegration", + "//Nicegram/NGTranslate:NGTranslate", + "//Nicegram/NGUI:NGUI", + "@swiftpkg_nicegram_assistant_ios//:Sources_NGPremiumUI", +] + +swift_library( + name = "ChatMessageInteractiveFileNode", + module_name = "ChatMessageInteractiveFileNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = NGDEPS + [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/RadialStatusNode", + "//submodules/SemanticStatusNode", + "//submodules/FileMediaResourceStatus", + "//submodules/CheckNode", + "//submodules/MusicAlbumArtResources", + "//submodules/AudioBlob", + "//submodules/ContextUI", + "//submodules/ChatPresentationInterfaceState", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent", + "//submodules/TelegramUI/Components/AudioWaveformComponent", + "//submodules/ShimmerEffect", + "//submodules/Media/ConvertOpusToAAC", + "//submodules/Media/LocalAudioTranscription", + "//submodules/TextSelectionNode", + "//submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent", + "//submodules/UndoUI", + "//submodules/TelegramNotices", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 6d4cf88d679..daffb701a08 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -37,60 +37,44 @@ import AudioTranscriptionPendingIndicatorComponent import UndoUI import TelegramNotices import ChatControllerInteraction +import ChatMessageDateAndStatusNode +import ChatHistoryEntry +import ChatMessageItemCommon private struct FetchControls { let fetch: (Bool) -> Void let cancel: () -> Void } -enum TranscribedText: Equatable { - case success(text: String, isPending: Bool) - case error(AudioTranscriptionMessageAttribute.TranscriptionError) -} - -func transcribedText(message: Message) -> TranscribedText? { - for attribute in message.attributes { - if let attribute = attribute as? AudioTranscriptionMessageAttribute { - if !attribute.text.isEmpty { - return .success(text: attribute.text, isPending: attribute.isPending) - } else { - if attribute.isPending { - return nil - } else { - return .error(attribute.error ?? .generic) - } - } - } - } - return nil -} - -final class ChatMessageInteractiveFileNode: ASDisplayNode { - final class Arguments { - let context: AccountContext - let presentationData: ChatPresentationData - let message: Message - let topMessage: Message - let associatedData: ChatMessageItemAssociatedData - let chatLocation: ChatLocation - let attributes: ChatMessageEntryAttributes - let isPinned: Bool - let forcedIsEdited: Bool - let file: TelegramMediaFile - let automaticDownload: Bool - let incoming: Bool - let isRecentActions: Bool - let forcedResourceStatus: FileMediaResourceStatus? - let dateAndStatusType: ChatMessageDateAndStatusType? - let displayReactions: Bool - let messageSelection: Bool? - let layoutConstants: ChatMessageItemLayoutConstants - let constrainedSize: CGSize - let controllerInteraction: ChatControllerInteraction +public final class ChatMessageInteractiveFileNode: ASDisplayNode { + public final class Arguments { + public let context: AccountContext + public let presentationData: ChatPresentationData + public let customTintColor: UIColor? + public let message: Message + public let topMessage: Message + public let associatedData: ChatMessageItemAssociatedData + public let chatLocation: ChatLocation + public let attributes: ChatMessageEntryAttributes + public let isPinned: Bool + public let forcedIsEdited: Bool + public let file: TelegramMediaFile + public let automaticDownload: Bool + public let incoming: Bool + public let isRecentActions: Bool + public let forcedResourceStatus: FileMediaResourceStatus? + public let dateAndStatusType: ChatMessageDateAndStatusType? + public let displayReactions: Bool + public let messageSelection: Bool? + public let isAttachedContentBlock: Bool + public let layoutConstants: ChatMessageItemLayoutConstants + public let constrainedSize: CGSize + public let controllerInteraction: ChatControllerInteraction - init( + public init( context: AccountContext, presentationData: ChatPresentationData, + customTintColor: UIColor?, message: Message, topMessage: Message, associatedData: ChatMessageItemAssociatedData, @@ -106,12 +90,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { dateAndStatusType: ChatMessageDateAndStatusType?, displayReactions: Bool, messageSelection: Bool?, + isAttachedContentBlock: Bool, layoutConstants: ChatMessageItemLayoutConstants, constrainedSize: CGSize, controllerInteraction: ChatControllerInteraction ) { self.context = context self.presentationData = presentationData + self.customTintColor = customTintColor self.message = message self.topMessage = topMessage self.associatedData = associatedData @@ -127,6 +113,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.dateAndStatusType = dateAndStatusType self.displayReactions = displayReactions self.messageSelection = messageSelection + self.isAttachedContentBlock = isAttachedContentBlock self.layoutConstants = layoutConstants self.constrainedSize = constrainedSize self.controllerInteraction = controllerInteraction @@ -138,31 +125,25 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private let titleNode: TextNode private let descriptionNode: TextNode private let descriptionMeasuringNode: TextNode - let fetchingTextNode: ImmediateTextNode - let fetchingCompactTextNode: ImmediateTextNode + public let fetchingTextNode: ImmediateTextNode + public let fetchingCompactTextNode: ImmediateTextNode - var waveformView: ComponentHostView? + public var waveformView: ComponentHostView? - /*private let waveformNode: AudioWaveformNode - private let waveformForegroundNode: AudioWaveformNode - private var waveformShimmerNode: ShimmerEffectNode? - private var waveformMaskNode: AudioWaveformNode? - private var waveformScrubbingNode: MediaPlayerScrubbingNode?*/ - - var audioTranscriptionButton: ComponentHostView? + public var audioTranscriptionButton: ComponentHostView? private var transcriptionPendingIndicator: ComponentHostView? - let textNode: TextNode - let textClippingNode: ASDisplayNode + public let textNode: TextNode + public let textClippingNode: ASDisplayNode private var textSelectionNode: TextSelectionNode? - var updateIsTextSelectionActive: ((Bool) -> Void)? + public var updateIsTextSelectionActive: ((Bool) -> Void)? - let dateAndStatusNode: ChatMessageDateAndStatusNode + public let dateAndStatusNode: ChatMessageDateAndStatusNode private let consumableContentNode: ASImageNode private var iconNode: TransformImageNode? - let statusContainerNode: ContextExtractedContentContainingNode - var statusNode: SemanticStatusNode? + public let statusContainerNode: ContextExtractedContentContainingNode + public var statusNode: SemanticStatusNode? private var playbackAudioLevelNode: VoiceBlobNode? private var streamingStatusNode: SemanticStatusNode? private var tapRecognizer: UITapGestureRecognizer? @@ -190,7 +171,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private var inputAudioLevel: CGFloat = 0.0 private var currentAudioLevel: CGFloat = 0.0 - var visibility: Bool = false { + public var visibility: Bool = false { didSet { guard self.visibility != oldValue else { return } @@ -205,12 +186,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private var actualFetchStatus: MediaResourceStatus? private let fetchDisposable = MetaDisposable() - var toggleSelection: (Bool) -> Void = { _ in } - var activateLocalContent: () -> Void = { } - var requestUpdateLayout: (Bool) -> Void = { _ in } - var displayImportedTooltip: (ASDisplayNode) -> Void = { _ in } + public var toggleSelection: (Bool) -> Void = { _ in } + public var activateLocalContent: () -> Void = { } + public var requestUpdateLayout: (Bool) -> Void = { _ in } + public var displayImportedTooltip: (ASDisplayNode) -> Void = { _ in } - var updateTranscriptionExpanded: ((AudioTranscriptionButtonComponent.TranscriptionState) -> Void)? + public var updateTranscriptionExpanded: ((AudioTranscriptionButtonComponent.TranscriptionState) -> Void)? private var context: AccountContext? private var message: Message? @@ -221,10 +202,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private var streamingCacheStatusFrame: CGRect? private var fileIconImage: UIImage? - var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed - var forcedAudioTranscriptionText: TranscribedText? + public var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed + public var forcedAudioTranscriptionText: TranscribedText? private var transcribeDisposable: Disposable? - var hasExpandedAudioTranscription: Bool { + public var hasExpandedAudioTranscription: Bool { if case .expanded = audioTranscriptionState { return true } else { @@ -235,7 +216,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private var hapticFeedback: HapticFeedback? - override init() { + override public init() { self.titleNode = TextNode() self.titleNode.displaysAsynchronously = false self.titleNode.isUserInteractionEnabled = false @@ -298,13 +279,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.transcribeDisposable?.dispose() } - override func didLoad() { + override public func didLoad() { let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.fileTap(_:))) self.view.addGestureRecognizer(tapRecognizer) self.tapRecognizer = tapRecognizer } - @objc func cacheProgressPressed() { + @objc private func cacheProgressPressed() { guard let resourceStatus = self.resourceStatus else { return } @@ -322,7 +303,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - @objc func progressPressed() { + @objc private func progressPressed() { if let resourceStatus = self.resourceStatus { switch resourceStatus.mediaStatus { case let .fetchStatus(fetchStatus): @@ -350,7 +331,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - @objc func fileTap(_ recognizer: UITapGestureRecognizer) { + @objc private func fileTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let streamingCacheStatusFrame = self.streamingCacheStatusFrame, streamingCacheStatusFrame.contains(recognizer.location(in: self.view)) { self.cacheProgressPressed() @@ -544,7 +525,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void))) { + public func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) @@ -680,7 +661,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { audioWaveform = AudioWaveform(bitstream: waveform, bitsPerSample: 5) } } else { - candidateTitleString = NSAttributedString(string: title ?? (arguments.file.fileName ?? "Unknown Track"), font: titleFont, textColor: messageTheme.fileTitleColor) + 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 @@ -705,7 +686,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let candidateTitleString = candidateTitleString { titleString = candidateTitleString } else if !isVoice { - titleString = NSAttributedString(string: arguments.file.fileName ?? "File", font: titleFont, textColor: messageTheme.fileTitleColor) + titleString = NSAttributedString(string: arguments.file.fileName ?? "File", font: titleFont, textColor: arguments.customTintColor ?? messageTheme.fileTitleColor) } if let candidateDescriptionString = candidateDescriptionString { @@ -828,7 +809,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let controlAreaWidth: CGFloat if hasThumbnail { - let currentIconFrame = CGRect(origin: CGPoint(x: -1.0, y: -7.0), size: CGSize(width: 74.0, height: 74.0)) + let currentIconFrame: CGRect + if arguments.isAttachedContentBlock { + currentIconFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 59.0, height: 59.0)) + controlAreaWidth = 71.0 + } else { + currentIconFrame = CGRect(origin: CGPoint(x: -1.0, y: -7.0), size: CGSize(width: 74.0, height: 74.0)) + controlAreaWidth = 86.0 + } iconFrame = currentIconFrame progressFrame = CGRect( origin: CGPoint( @@ -837,13 +825,20 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { ), size: CGSize(width: progressDiameter, height: progressDiameter) ) - controlAreaWidth = 86.0 } else { - progressFrame = CGRect( - origin: CGPoint(x: 3.0, y: isVoice ? -3.0 : 0.0), - size: CGSize(width: progressDiameter, height: progressDiameter) - ) - controlAreaWidth = progressFrame.maxX + 8.0 + if arguments.isAttachedContentBlock { + progressFrame = CGRect( + origin: CGPoint(x: 3.0, y: 0.0), + size: CGSize(width: progressDiameter, height: progressDiameter) + ) + controlAreaWidth = progressFrame.maxX + 8.0 + } else { + progressFrame = CGRect( + origin: CGPoint(x: 3.0, y: isVoice ? -3.0 : 0.0), + size: CGSize(width: progressDiameter, height: progressDiameter) + ) + controlAreaWidth = progressFrame.maxX + 8.0 + } } var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? @@ -947,7 +942,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let normHeight: CGFloat if hasThumbnail { - normHeight = 64.0 + if arguments.isAttachedContentBlock { + normHeight = 58.0 + } else { + normHeight = 64.0 + } } else { normHeight = 44.0 } @@ -1490,6 +1489,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { guard let file = self.file else { return } + guard let arguments = self.arguments else { + return + } let incoming = message.effectivelyIncoming(context.account.peerId) let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing @@ -1638,7 +1640,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { backgroundNodeColor = presentationData.theme.theme.chat.message.mediaOverlayControlColors.fillColor foregroundNodeColor = .white } else { - backgroundNodeColor = messageTheme.mediaActiveControlColor + backgroundNodeColor = arguments.customTintColor ?? messageTheme.mediaActiveControlColor if incoming && messageTheme.mediaActiveControlColor.rgb != 0xffffff { foregroundNodeColor = .white } else { @@ -1702,7 +1704,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { playbackMaskLayer.path = maskPath.cgPath playbackAudioLevelNode.layer.mask = playbackMaskLayer } - self.playbackAudioLevelNode?.setColor(messageTheme.mediaActiveControlColor) + self.playbackAudioLevelNode?.setColor(arguments.customTintColor ?? messageTheme.mediaActiveControlColor) if streamingState != .none && self.streamingStatusNode == nil { let streamingStatusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor) @@ -1790,7 +1792,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize) } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode))) { + public typealias Apply = (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode + public typealias FinalizeLayout = (CGFloat) -> (CGSize, Apply) + public typealias ContinueLayout = (CGSize) -> (CGFloat, FinalizeLayout) + public typealias AsyncLayout = (Arguments) -> (CGFloat, ContinueLayout) + public static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> AsyncLayout { let currentAsyncLayout = node?.asyncLayout() return { arguments in @@ -1822,7 +1828,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func willUpdateIsExtractedToContextPreview(_ value: Bool) { + public func willUpdateIsExtractedToContextPreview(_ value: Bool) { if !value { if let textSelectionNode = self.textSelectionNode { self.textSelectionNode = nil @@ -1835,7 +1841,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func updateIsExtractedToContextPreview(_ value: Bool) { + public func updateIsExtractedToContextPreview(_ value: Bool) { if value { if self.textSelectionNode == nil, self.textClippingNode.supernode != nil, let item = self.arguments, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() { let selectionColor: UIColor @@ -1848,7 +1854,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor } - let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in self?.updateIsTextSelectionActive?(value) }, present: { [weak self] c, a in self?.arguments?.controllerInteraction.presentGlobalOverlayController(c, a) @@ -1858,8 +1864,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { guard let strongSelf = self, let item = strongSelf.arguments else { return } - item.controllerInteraction.performTextSelectionAction(true, text, action) + item.controllerInteraction.performTextSelectionAction(item.message, true, text, action) }) + textSelectionNode.enableQuote = false self.textSelectionNode = textSelectionNode self.textClippingNode.addSubnode(textSelectionNode) self.textClippingNode.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode) @@ -1879,7 +1886,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + public func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let iconNode = self.iconNode, let file = self.file, file.isEqual(to: media) { return (iconNode, iconNode.bounds, { [weak iconNode] in return (iconNode?.view.snapshotContentTree(unhide: true), nil) @@ -1889,7 +1896,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func updateHiddenMedia(_ media: [Media]?) -> Bool { + public func updateHiddenMedia(_ media: [Media]?) -> Bool { var isHidden = false if let file = self.file, let media = media { for m in media { @@ -1918,7 +1925,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.playerUpdateTimer = nil } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.dateAndStatusNode.supernode != nil { if let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) { return result @@ -1937,27 +1944,23 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return super.hitTest(point, with: event) } - func hasTapAction(at point: CGPoint) -> Bool { + public func hasTapAction(at point: CGPoint) -> Bool { if let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { return true } return false } - func animateSent() { + public func animateSent() { if let view = self.waveformView?.componentView as? AudioWaveformComponent.View { view.animateIn() } } - - func animateTo(_ node: ChatMessageInteractiveInstantVideoNode) { - - } } -final class FileMessageSelectionNode: ASDisplayNode { - enum NodeType { +public final class FileMessageSelectionNode: ASDisplayNode { + public enum NodeType { case media case file case voice diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD new file mode 100644 index 00000000000..6e073b1b223 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD @@ -0,0 +1,45 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageInteractiveInstantVideoNode", + module_name = "ChatMessageInteractiveInstantVideoNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + #"-Xfrontend", "-debug-time-function-bodies" + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/RadialStatusNode", + "//submodules/PhotoResources", + "//submodules/TelegramUniversalVideoContent", + "//submodules/FileMediaResourceStatus", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent", + "//submodules/UndoUI", + "//submodules/TelegramNotices", + "//submodules/Markdown", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", + "//submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift similarity index 89% rename from submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index ab55076e46e..d8dfd8c06a6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -20,14 +20,31 @@ import TelegramNotices import Markdown import TextFormat import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageBubbleContentNode +import ChatMessageReplyInfoNode +import InstantVideoRadialStatusNode +import ChatInstantVideoMessageDurationNode +import ChatControllerInteraction -struct ChatMessageInstantVideoItemLayoutResult { - let contentSize: CGSize - let overflowLeft: CGFloat - let overflowRight: CGFloat +public struct ChatMessageInstantVideoItemLayoutResult { + public let contentSize: CGSize + public let overflowLeft: CGFloat + public let overflowRight: CGFloat + + public init( + contentSize: CGSize, + overflowLeft: CGFloat, + overflowRight: CGFloat + ) { + self.contentSize = contentSize + self.overflowLeft = overflowLeft + self.overflowRight = overflowRight + } } -enum ChatMessageInstantVideoItemLayoutData { +public enum ChatMessageInstantVideoItemLayoutData { case unconstrained(width: CGFloat) case constrained(left: CGFloat, right: CGFloat) } @@ -37,12 +54,12 @@ private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont -enum ChatMessageInteractiveInstantVideoNodeStatusType { +public enum ChatMessageInteractiveInstantVideoNodeStatusType { case free case bubble } -class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { +public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private var hierarchyTrackingLayer: HierarchyTrackingLayer? private var trackingIsInHierarchy: Bool = false { didSet { @@ -54,7 +71,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - var canAttachContent: Bool = false { + public var canAttachContent: Bool = false { didSet { if self.canAttachContent != oldValue { Queue.mainQueue().justDispatch { @@ -68,32 +85,32 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private let secretVideoPlaceholderBackground: ASImageNode private let secretVideoPlaceholder: TransformImageNode - var audioTranscriptionButton: ComponentHostView? + public var audioTranscriptionButton: ComponentHostView? private var statusNode: RadialStatusNode? private var disappearingStatusNode: RadialStatusNode? private var playbackStatusNode: InstantVideoRadialStatusNode? - private(set) var videoFrame: CGRect? + public private(set) var videoFrame: CGRect? private var imageScale: CGFloat = 1.0 private var item: ChatMessageBubbleContentItem? private var automaticDownload: Bool? - var media: TelegramMediaFile? - var appliedForwardInfo: (Peer?, String?)? + public var media: TelegramMediaFile? + public var appliedForwardInfo: (Peer?, String?)? private let fetchDisposable = MetaDisposable() private var durationBackgroundNode: NavigationBackgroundNode? private var durationNode: ChatInstantVideoMessageDurationNode? - let dateAndStatusNode: ChatMessageDateAndStatusNode + public let dateAndStatusNode: ChatMessageDateAndStatusNode private let infoBackgroundNode: ASImageNode private let muteIconNode: ASImageNode - var viaBotNode: TextNode? - var replyInfoNode: ChatMessageReplyInfoNode? - var replyBackgroundNode: NavigationBackgroundNode? - var forwardInfoNode: ChatMessageForwardInfoNode? + public var viaBotNode: TextNode? + public var replyInfoNode: ChatMessageReplyInfoNode? + public var replyBackgroundNode: NavigationBackgroundNode? + public var forwardInfoNode: ChatMessageForwardInfoNode? private var status: FileMediaResourceStatus? private var playerStatus: MediaPlayerStatus? { @@ -115,7 +132,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - var visibility: Bool = false { + public var visibility: Bool = false { didSet { if self.visibility != oldValue { self.videoNode?.canAttachContent = self.shouldAcquireVideoContext @@ -123,15 +140,15 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - var shouldOpen: () -> Bool = { return true } + public var shouldOpen: () -> Bool = { return true } - var updateTranscriptionExpanded: ((AudioTranscriptionButtonComponent.TranscriptionState) -> Void)? - var updateTranscriptionText: ((TranscribedText?) -> Void)? + public var updateTranscriptionExpanded: ((AudioTranscriptionButtonComponent.TranscriptionState) -> Void)? + public var updateTranscriptionText: ((TranscribedText?) -> Void)? - var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed - var audioTranscriptionText: TranscribedText? + public var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed + public var audioTranscriptionText: TranscribedText? private var transcribeDisposable: Disposable? - var hasExpandedAudioTranscription: Bool { + public var hasExpandedAudioTranscription: Bool { if case .expanded = audioTranscriptionState { return true } else { @@ -142,9 +159,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private var hapticFeedback: HapticFeedback? - var requestUpdateLayout: (Bool) -> Void = { _ in } + public var requestUpdateLayout: (Bool) -> Void = { _ in } - override init() { + override public init() { self.secretVideoPlaceholderBackground = ASImageNode() self.secretVideoPlaceholderBackground.isLayerBacked = true self.secretVideoPlaceholderBackground.displaysAsynchronously = false @@ -170,7 +187,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.infoBackgroundNode.addSubnode(self.muteIconNode) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -181,7 +198,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.fetchedThumbnailDisposable.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) @@ -208,7 +225,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.layer.addSublayer(hierarchyTrackingLayer) } - func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool, _ avatarInset: CGFloat) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void) { + public func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool, _ avatarInset: CGFloat) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void) { let previousFile = self.media let currentItem = self.item @@ -231,12 +248,12 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { var updatedMuteIconImage: UIImage? var incoming = item.message.effectivelyIncoming(item.context.account.peerId) - if case .forwardedMessages = item.associatedData.subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { incoming = false } var viaBotApply: (TextNodeLayout, () -> TextNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode)? var updatedInstantVideoBackgroundImage: UIImage? let instantVideoBackgroundImage: UIImage? @@ -308,11 +325,13 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let bubbleEdgeInset: CGFloat = 4.0 let bubbleContentInsetsLeft: CGFloat = 6.0 - let availableWidth = max(60.0, width - 210.0 - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0) - let availableContentWidth = width - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0 + let availableWidth: CGFloat = max(60.0, width - 210.0 - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0) + let availableContentWidth: CGFloat = width - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0 if !ignoreHeaders { var replyMessage: Message? + var replyForward: QuotedReplyMessageAttribute? + var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? var replyStory: StoryId? for attribute in item.message.attributes { @@ -342,12 +361,15 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } } - if replyMessage != nil || replyStory != nil { + if replyMessage != nil || replyForward != nil || replyStory != nil { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyMessage?.id { } else { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( @@ -356,6 +378,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { context: item.context, type: .standalone, message: replyMessage, + replyForward: replyForward, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), @@ -407,8 +431,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { forwardAuthorSignature = forwardInfo.authorSignature } } - let availableWidth = max(60.0, availableContentWidth - 210.0 + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + let availableWidth: CGFloat = max(60.0, availableContentWidth - 210.0 + 6.0) + forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) } var notConsumed = false @@ -975,13 +999,16 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(false) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 5.0) : (width - messageInfoSize.width - bubbleEdgeInset - 9.0 + 10.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) + replyBackgroundFrame = replyInfoFrame + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, false, animation) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 5.0) : (width - messageInfoSize.width - bubbleEdgeInset - 9.0 + 10.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) animation.animator.updateFrame(layer: replyInfoNode.layer, frame: replyInfoFrame, completion: nil) messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) @@ -990,8 +1017,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { strongSelf.replyInfoNode = nil } - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - let replyBackgroundFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 4.0) : (width - messageInfoSize.width - bubbleEdgeInset)) - 4.0, y: 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame { + let replyBackgroundFrame = replyBackgroundFrame animation.animator.updateFrame(layer: replyBackgroundNode.layer, frame: replyBackgroundFrame, completion: nil) let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 @@ -1230,7 +1257,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { @@ -1264,11 +1291,14 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) return } else if let attribute = attribute as? ReplyStoryAttribute { item.controllerInteraction.navigateToStory(item.message, attribute.storyId) return + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + return } } } @@ -1285,7 +1315,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) return } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) @@ -1340,12 +1370,12 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let audioTranscriptionButton = self.audioTranscriptionButton, !audioTranscriptionButton.isHidden, audioTranscriptionButton.frame.contains(point) { return audioTranscriptionButton } if let playbackNode = self.playbackStatusNode, !self.isPlaying, !playbackNode.frame.insetBy(dx: 0.2 * playbackNode.frame.width, dy: 0.2 * playbackNode.frame.height).contains(point) { - let distanceFromCenter = point.distanceTo(playbackNode.position) + let distanceFromCenter = sqrt(pow(point.x - playbackNode.position.x, 2.0) + pow(point.y - playbackNode.position.y, 2.0)) if distanceFromCenter < 0.2 * playbackNode.frame.width { return self.view } else { @@ -1401,12 +1431,12 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - override func accessibilityActivate() -> Bool { + override public func accessibilityActivate() -> Bool { self.progressPressed() return true } - func videoContentNode(at point: CGPoint) -> ASDisplayNode? { + public func videoContentNode(at point: CGPoint) -> ASDisplayNode? { if let videoFrame = self.videoFrame { if videoFrame.contains(point) { return self.videoNode @@ -1415,7 +1445,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return nil } - static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool, _ avatarInset: CGFloat) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode) { + public static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool, _ avatarInset: CGFloat) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode) { let makeLayout = node?.asyncLayout() return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload, avatarInset in var createdNode: ChatMessageInteractiveInstantVideoNode? @@ -1438,7 +1468,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - var isPlaying: Bool { + public var isPlaying: Bool { if let status = self.status, case let .playbackStatus(playbackStatus) = status.mediaStatus, case .playing = playbackStatus { return true } else { @@ -1446,21 +1476,21 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - func seekTo(_ position: Double) { + public func seekTo(_ position: Double) { if let duration = self.playbackStatusNode?.duration { self.videoNode?.seek(position * duration) } } - func play() { + public func play() { self.videoNode?.play() } - func pause() { + public func pause() { self.videoNode?.pause() } - func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { + public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { if let item = self.item { var isUnconsumed = false for attribute in item.message.attributes { @@ -1500,7 +1530,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } private var animatedFadeIn = false - func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) { + public func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) { guard let videoFrame = self.videoFrame else { return } @@ -1609,20 +1639,40 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.updateTranscriptionExpanded?(self.audioTranscriptionState) } - func animateTo(_ node: ChatMessageInteractiveFileNode, animator: ControlledTransitionAnimator) { + public final class AnimateFileNodeDescription { + public let node: ASDisplayNode + public let textClippingNode: ASDisplayNode + public let dateAndStatusNode: ASDisplayNode + public let fetchingTextNode: ASDisplayNode + public let waveformView: UIView? + public let statusNode: ASDisplayNode? + public let audioTranscriptionButton: UIView? + + public init(node: ASDisplayNode, textClippingNode: ASDisplayNode, dateAndStatusNode: ASDisplayNode, fetchingTextNode: ASDisplayNode, waveformView: UIView?, statusNode: ASDisplayNode?, audioTranscriptionButton: UIView?) { + self.node = node + self.textClippingNode = textClippingNode + self.dateAndStatusNode = dateAndStatusNode + self.fetchingTextNode = fetchingTextNode + self.waveformView = waveformView + self.statusNode = statusNode + self.audioTranscriptionButton = audioTranscriptionButton + } + } + + public func animateTo(_ animateToFile: AnimateFileNodeDescription, animator: ControlledTransitionAnimator) { let duration: Double = 0.2 - node.alpha = 1.0 - if node.supernode == nil { - self.supernode?.insertSubnode(node, belowSubnode: self) + animateToFile.node.alpha = 1.0 + if animateToFile.node.supernode == nil { + self.supernode?.insertSubnode(animateToFile.node, belowSubnode: self) } self.alpha = 0.0 self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) - node.waveformView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) + animateToFile.waveformView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) - if let videoNode = self.videoNode, let targetNode = node.statusNode, let videoSnapshotView = videoNode.view.snapshotView(afterScreenUpdates: false) { + if let videoNode = self.videoNode, let targetNode = animateToFile.statusNode, let videoSnapshotView = videoNode.view.snapshotView(afterScreenUpdates: false) { videoSnapshotView.frame = videoNode.bounds videoNode.view.insertSubview(videoSnapshotView, at: 1) videoSnapshotView.alpha = 0.0 @@ -1648,32 +1698,33 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) } - let sourceFrame = self.view.convert(videoNode.frame, to: node.view) + let sourceFrame = self.view.convert(videoNode.frame, to: animateToFile.node.view) animator.animatePosition(layer: targetNode.layer, from: sourceFrame.center, to: targetNode.position, completion: nil) let sourceScale = (videoNode.bounds.width * self.imageScale) / targetNode.frame.width animator.animateScale(layer: targetNode.layer, from: sourceScale, to: 1.0, completion: nil) targetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) let verticalDelta = (videoNode.position.y - targetFrame.center.y) * 2.0 - animator.animatePosition(layer: node.textClippingNode.layer, from: node.textClippingNode.position.offsetBy(dx: 0.0, dy: verticalDelta), to: node.textClippingNode.position, completion: nil) - node.textClippingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + animator.animatePosition(layer: animateToFile.textClippingNode.layer, from: animateToFile.textClippingNode.position.offsetBy(dx: 0.0, dy: verticalDelta), to: animateToFile.textClippingNode.position, completion: nil) + animateToFile.textClippingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) if let maskImage = generateGradientImage(size: CGSize(width: 8.0, height: 10.0), colors: [UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 1.0], direction: .vertical) { - let textClippingFrame = node.textClippingNode.frame + let textClippingFrame = animateToFile.textClippingNode.frame let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1)) - node.textClippingNode.view.mask = maskView + animateToFile.textClippingNode.view.mask = maskView maskView.frame = CGRect(origin: CGPoint(), size: CGSize(width: textClippingFrame.width, height: maskImage.size.height)) - animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: textClippingFrame.size), completion: { [weak maskView, weak node] _ in + let nodeTextClippingNode = animateToFile.textClippingNode + animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: textClippingFrame.size), completion: { [weak maskView, weak nodeTextClippingNode] _ in maskView?.removeFromSuperview() - node?.textClippingNode.view.mask = nil + nodeTextClippingNode?.view.mask = nil }) } } - if let audioTranscriptionButton = self.audioTranscriptionButton, let targetAudioTranscriptionButton = node.audioTranscriptionButton { - let sourceFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: node.view) + if let audioTranscriptionButton = self.audioTranscriptionButton, let targetAudioTranscriptionButton = animateToFile.audioTranscriptionButton { + let sourceFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: animateToFile.node.view) animator.animatePosition(layer: targetAudioTranscriptionButton.layer, from: sourceFrame.center, to: targetAudioTranscriptionButton.center, completion: nil) targetAudioTranscriptionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) @@ -1683,28 +1734,28 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { audioTranscriptionButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) } - let sourceDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: node.view) - let targetDateFrame = node.dateAndStatusNode.view.convert(node.dateAndStatusNode.view.bounds, to: self.view) + let sourceDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: animateToFile.node.view) + let targetDateFrame = animateToFile.dateAndStatusNode.view.convert(animateToFile.dateAndStatusNode.view.bounds, to: self.view) animator.animatePosition(layer: self.dateAndStatusNode.layer, from: self.dateAndStatusNode.position, to: CGPoint(x: targetDateFrame.maxX - self.dateAndStatusNode.frame.width / 2.0 + 2.0, y: targetDateFrame.midY - 7.0), completion: nil) - animator.animatePosition(layer: node.dateAndStatusNode.layer, from: CGPoint(x: sourceDateFrame.maxX - node.dateAndStatusNode.frame.width / 2.0, y: sourceDateFrame.midY + 7.0), to: node.dateAndStatusNode.position, completion: nil) + animator.animatePosition(layer: animateToFile.dateAndStatusNode.layer, from: CGPoint(x: sourceDateFrame.maxX - animateToFile.dateAndStatusNode.frame.width / 2.0, y: sourceDateFrame.midY + 7.0), to: animateToFile.dateAndStatusNode.position, completion: nil) self.dateAndStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) - node.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05) + animateToFile.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05) if let durationNode = self.durationNode, let durationBackgroundNode = self.durationBackgroundNode { - let sourceDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: node.view) - let targetDurationFrame = node.fetchingTextNode.view.convert(node.fetchingTextNode.view.bounds, to: self.view) + let sourceDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: animateToFile.node.view) + let targetDurationFrame = animateToFile.fetchingTextNode.view.convert(animateToFile.fetchingTextNode.view.bounds, to: self.view) let delta = CGPoint(x: targetDurationFrame.center.x - durationNode.position.x, y: targetDurationFrame.center.y - durationNode.position.y) animator.animatePosition(layer: durationNode.layer, from: durationNode.position, to: targetDurationFrame.center, completion: nil) animator.animatePosition(layer: durationBackgroundNode.layer, from: durationBackgroundNode.position, to: durationBackgroundNode.position.offsetBy(dx: delta.x, dy: delta.y), completion: nil) - animator.animatePosition(layer: node.fetchingTextNode.layer, from: sourceDurationFrame.center, to: node.fetchingTextNode.position, completion: nil) + animator.animatePosition(layer: animateToFile.fetchingTextNode.layer, from: sourceDurationFrame.center, to: animateToFile.fetchingTextNode.position, completion: nil) durationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) self.durationBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) - node.fetchingTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05) + animateToFile.fetchingTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05) } if let viaBotNode = self.viaBotNode { @@ -1721,19 +1772,20 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - func animateFrom(_ node: ChatMessageInteractiveFileNode, animator: ControlledTransitionAnimator) { + public func animateFrom(_ animateFromFile: AnimateFileNodeDescription, animator: ControlledTransitionAnimator) { let duration: Double = 0.2 self.alpha = 1.0 self.isHidden = false - node.alpha = 0.0 - node.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { _ in - node.removeFromSupernode() + animateFromFile.node.alpha = 0.0 + let animateToFileNode = animateFromFile.node + animateFromFile.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak animateToFileNode] _ in + animateToFileNode?.removeFromSupernode() }) - node.waveformView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) + animateFromFile.waveformView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) - if let videoNode = self.videoNode, let sourceNode = node.statusNode { + if let videoNode = self.videoNode, let sourceNode = animateFromFile.statusNode { videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view) @@ -1751,34 +1803,35 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { playbackStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) } - let targetFrame = self.view.convert(videoNode.frame, to: node.view) + let targetFrame = self.view.convert(videoNode.frame, to: animateFromFile.node.view) animator.animatePosition(layer: sourceNode.layer, from: sourceNode.position, to: targetFrame.center, completion: nil) let targetScale = (videoNode.bounds.width * self.imageScale) / sourceNode.frame.width animator.animateScale(layer: sourceNode.layer, from: 1.0, to: targetScale, completion: nil) sourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) let verticalDelta = (videoNode.position.y - sourceFrame.center.y) * 2.0 - animator.animatePosition(layer: node.textClippingNode.layer, from: node.textClippingNode.position, to: node.textClippingNode.position.offsetBy(dx: 0.0, dy: verticalDelta), completion: nil) - node.textClippingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) + animator.animatePosition(layer: animateFromFile.textClippingNode.layer, from: animateFromFile.textClippingNode.position, to: animateFromFile.textClippingNode.position.offsetBy(dx: 0.0, dy: verticalDelta), completion: nil) + animateFromFile.textClippingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) if let maskImage = generateGradientImage(size: CGSize(width: 8.0, height: 10.0), colors: [UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 1.0], direction: .vertical) { - let textClippingFrame = node.textClippingNode.frame + let textClippingFrame = animateFromFile.textClippingNode.frame let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1)) - node.textClippingNode.view.mask = maskView + animateFromFile.textClippingNode.view.mask = maskView maskView.frame = CGRect(origin: CGPoint(), size: textClippingFrame.size) - animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: textClippingFrame.width, height: maskImage.size.height)), completion: { [weak maskView, weak node] _ in + let animateFromFileTextClippingNode = animateFromFile.textClippingNode + animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: textClippingFrame.width, height: maskImage.size.height)), completion: { [weak maskView, weak animateFromFileTextClippingNode] _ in maskView?.removeFromSuperview() - node?.textClippingNode.view.mask = nil + animateFromFileTextClippingNode?.view.mask = nil }) } } - if let audioTranscriptionButton = self.audioTranscriptionButton, let sourceAudioTranscriptionButton = node.audioTranscriptionButton { + if let audioTranscriptionButton = self.audioTranscriptionButton, let sourceAudioTranscriptionButton = animateFromFile.audioTranscriptionButton { audioTranscriptionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - let targetFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: node.view) + let targetFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: animateFromFile.node.view) animator.animatePosition(layer: sourceAudioTranscriptionButton.layer, from: sourceAudioTranscriptionButton.center, to: targetFrame.center, completion: nil) sourceAudioTranscriptionButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) @@ -1786,28 +1839,28 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { animator.animatePosition(layer: audioTranscriptionButton.layer, from: sourceFrame.center, to: audioTranscriptionButton.center, completion: nil) } - let sourceDateFrame = node.dateAndStatusNode.view.convert(node.dateAndStatusNode.view.bounds, to: self.view) - let targetDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: node.view) + let sourceDateFrame = animateFromFile.dateAndStatusNode.view.convert(animateFromFile.dateAndStatusNode.view.bounds, to: self.view) + let targetDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: animateFromFile.node.view) animator.animatePosition(layer: self.dateAndStatusNode.layer, from: CGPoint(x: sourceDateFrame.maxX - self.dateAndStatusNode.frame.width / 2.0 + 2.0, y: sourceDateFrame.midY - 7.0), to: self.dateAndStatusNode.position, completion: nil) - animator.animatePosition(layer: node.dateAndStatusNode.layer, from: node.dateAndStatusNode.position, to: CGPoint(x: targetDateFrame.maxX - node.dateAndStatusNode.frame.width / 2.0, y: targetDateFrame.midY + 7.0), completion: nil) + animator.animatePosition(layer: animateFromFile.dateAndStatusNode.layer, from: animateFromFile.dateAndStatusNode.position, to: CGPoint(x: targetDateFrame.maxX - animateFromFile.dateAndStatusNode.frame.width / 2.0, y: targetDateFrame.midY + 7.0), completion: nil) self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - node.dateAndStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) + animateFromFile.dateAndStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) if let durationNode = self.durationNode, let durationBackgroundNode = self.durationBackgroundNode { - let sourceDurationFrame = node.fetchingTextNode.view.convert(node.fetchingTextNode.view.bounds, to: self.view) - let targetDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: node.view) + let sourceDurationFrame = animateFromFile.fetchingTextNode.view.convert(animateFromFile.fetchingTextNode.view.bounds, to: self.view) + let targetDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: animateFromFile.node.view) let delta = CGPoint(x: sourceDurationFrame.center.x - durationNode.position.x, y: sourceDurationFrame.center.y - durationNode.position.y) animator.animatePosition(layer: durationNode.layer, from: sourceDurationFrame.center, to: durationNode.position, completion: nil) animator.animatePosition(layer: durationBackgroundNode.layer, from: durationBackgroundNode.position.offsetBy(dx: delta.x, dy: delta.y), to: durationBackgroundNode.position, completion: nil) - animator.animatePosition(layer: node.fetchingTextNode.layer, from: node.fetchingTextNode.position, to: targetDurationFrame.center, completion: nil) + animator.animatePosition(layer: animateFromFile.fetchingTextNode.layer, from: animateFromFile.fetchingTextNode.position, to: targetDurationFrame.center, completion: nil) durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) self.durationBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05) - node.fetchingTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) + animateFromFile.fetchingTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) } if let viaBotNode = self.viaBotNode { @@ -1826,7 +1879,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.canAttachContent = false } - func targetForStoryTransition(id: StoryId) -> UIView? { + public func targetForStoryTransition(id: StoryId) -> UIView? { guard let item = self.item else { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD new file mode 100644 index 00000000000..1e1cb0ffcd7 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD @@ -0,0 +1,46 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageInteractiveMediaNode", + module_name = "ChatMessageInteractiveMediaNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/RadialStatusNode", + "//submodules/StickerResources", + "//submodules/PhotoResources", + "//submodules/TelegramUniversalVideoContent", + "//submodules/TelegramStringFormatting", + "//submodules/GalleryUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/LocalMediaResources", + "//submodules/WallpaperResources", + "//submodules/ChatMessageInteractiveMediaBadge", + "//submodules/ContextUI", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift similarity index 97% rename from submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 4902e4bdac7..2be99311662 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -25,22 +25,26 @@ import ContextUI import InvisibleInkDustNode import ChatControllerInteraction import StoryContainerScreen +import ChatMessageDateAndStatusNode +import ChatHistoryEntry +import ChatMessageItemCommon +import WallpaperPreviewMedia private struct FetchControls { let fetch: (Bool) -> Void let cancel: () -> Void } -enum InteractiveMediaNodeSizeCalculation { +public enum InteractiveMediaNodeSizeCalculation { case constrained(CGSize) case unconstrained } -enum InteractiveMediaNodeContentMode { +public enum InteractiveMediaNodeContentMode { case aspectFit case aspectFill - var bubbleVideoDecorationContentMode: ChatBubbleVideoDecorationContentMode { + public var bubbleVideoDecorationContentMode: ChatBubbleVideoDecorationContentMode { switch self { case .aspectFit: return .aspectFit @@ -50,32 +54,52 @@ enum InteractiveMediaNodeContentMode { } } -enum InteractiveMediaNodeActivateContent { +public enum InteractiveMediaNodeActivateContent { case `default` case stream case automaticPlayback } -enum InteractiveMediaNodeAutodownloadMode { +public enum InteractiveMediaNodeAutodownloadMode { case none case prefetch case full } -enum InteractiveMediaNodePlayWithSoundMode { +public enum InteractiveMediaNodePlayWithSoundMode { case single case loop } -struct ChatMessageDateAndStatus { - var type: ChatMessageDateAndStatusType - var edited: Bool - var viewCount: Int? - var dateReactions: [MessageReaction] - var dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)] - var dateReplies: Int - var isPinned: Bool - var dateText: String +public struct ChatMessageDateAndStatus { + public var type: ChatMessageDateAndStatusType + public var edited: Bool + public var viewCount: Int? + public var dateReactions: [MessageReaction] + public var dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)] + public var dateReplies: Int + public var isPinned: Bool + public var dateText: String + + public init( + type: ChatMessageDateAndStatusType, + edited: Bool, + viewCount: Int?, + dateReactions: [MessageReaction], + dateReactionPeers: [(MessageReaction.Reaction, EnginePeer)], + dateReplies: Int, + isPinned: Bool, + dateText: String + ) { + self.type = type + self.edited = edited + self.viewCount = viewCount + self.dateReactions = dateReactions + self.dateReactionPeers = dateReactionPeers + self.dateReplies = dateReplies + self.isPinned = isPinned + self.dateText = dateText + } } public func roundedRectCgPath(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) -> CGPath { @@ -345,7 +369,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { } } -final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitionNode { +public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitionNode { private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode private var currentImageArguments: TransformImageArguments? @@ -357,11 +381,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio private var videoContent: NativeVideoContent? private var animatedStickerNode: AnimatedStickerNode? private var statusNode: RadialStatusNode? - var videoNodeDecoration: ChatBubbleVideoDecoration? - var decoration: UniversalVideoDecoration? { + public var videoNodeDecoration: ChatBubbleVideoDecoration? + public var decoration: UniversalVideoDecoration? { return self.videoNodeDecoration } - let dateAndStatusNode: ChatMessageDateAndStatusNode + public let dateAndStatusNode: ChatMessageDateAndStatusNode private var badgeNode: ChatMessageInteractiveMediaBadge? private var extendedMediaOverlayNode: ExtendedMediaOverlayNode? @@ -374,7 +398,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio private var sizeCalculation: InteractiveMediaNodeSizeCalculation? private var wideLayout: Bool? private var automaticDownload: InteractiveMediaNodeAutodownloadMode? - var automaticPlayback: Bool? + public var automaticPlayback: Bool? private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) @@ -401,8 +425,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio private var secretTimer: SwiftSignalKit.Timer? - var visibilityPromise = ValuePromise(false, ignoreRepeated: true) - var visibility: Bool = false { + public var visibilityPromise = ValuePromise(false, ignoreRepeated: true) + public var visibility: Bool = false { didSet { self.updateVisibility() } @@ -428,11 +452,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio self.visibilityPromise.set(visibility) } - var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } - var activatePinch: ((PinchSourceContainerNode) -> Void)? - var updateMessageReaction: ((Message, ChatControllerInteractionReaction) -> Void)? + public var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } + public var activatePinch: ((PinchSourceContainerNode) -> Void)? + public var updateMessageReaction: ((Message, ChatControllerInteractionReaction) -> Void)? - override init() { + override public init() { self.pinchContainerNode = PinchSourceContainerNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode() @@ -538,15 +562,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio self.secretTimer?.invalidate() } - func isAvailableForGalleryTransition() -> Bool { + public func isAvailableForGalleryTransition() -> Bool { return self.automaticPlayback ?? false } - func isAvailableForInstantPageTransition() -> Bool { + public func isAvailableForInstantPageTransition() -> Bool { return false } - override func didLoad() { + override public func didLoad() { super.didLoad() self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))) @@ -616,7 +640,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - @objc func imageTap(_ recognizer: UITapGestureRecognizer) { + @objc private func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let point = recognizer.location(in: self.imageNode.view) if let _ = self.attributes?.updatingMedia { @@ -661,7 +685,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { + public func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -2202,7 +2226,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) { + public static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in @@ -2234,11 +2258,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func setOverlayColor(_ color: UIColor?, animated: Bool) { + public func setOverlayColor(_ color: UIColor?, animated: Bool) { self.imageNode.setOverlayColor(color, animated: animated) } - func isReadyForInteractivePreview() -> Bool { + public func isReadyForInteractivePreview() -> Bool { if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { return true } else { @@ -2246,7 +2270,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func updateIsHidden(_ isHidden: Bool) { + public func updateIsHidden(_ isHidden: Bool) { if isHidden && !self.internallyVisible { self.internallyVisible = true self.updateVisibility() @@ -2280,7 +2304,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func transitionNode(adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + public func transitionNode(adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { var bounds: CGRect if let currentImageArguments = self.currentImageArguments { if adjustRect { @@ -2338,7 +2362,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio }) } - func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { + public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { var isAnimated = false if let file = self.media as? TelegramMediaFile, file.isAnimated { isAnimated = true diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/BUILD new file mode 100644 index 00000000000..78df0732838 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageInvoiceBubbleContentNode", + module_name = "ChatMessageInvoiceBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramStringFormatting", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift similarity index 74% rename from submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift index fa24d24139a..59a16518f5e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -7,22 +7,25 @@ import SwiftSignalKit import TelegramCore import TelegramUIPreferences import TelegramStringFormatting +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentNode private let titleFont: UIFont = Font.semibold(15.0) private let textFont: UIFont = Font.regular(15.0) -final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { private var invoice: TelegramMediaInvoice? private let contentNode: ChatMessageAttachedContentNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = self.visibility } } - required init() { + required public init() { self.contentNode = ChatMessageAttachedContentNode() super.init() @@ -30,11 +33,11 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.contentNode) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let contentNodeLayout = self.contentNode.asyncLayout() return { item, layoutConstants, preparePosition, _, constrainedSize, _ in @@ -98,23 +101,23 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -122,23 +125,23 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } }*/ } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } return self.contentNode.transitionNode(media: media) } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionView(value: value) + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + if let statusNode = self.contentNode.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageItem/BUILD new file mode 100644 index 00000000000..8c820694528 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageItem", + module_name = "ChatMessageItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift new file mode 100644 index 00000000000..e2d88372936 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -0,0 +1,170 @@ +import Foundation +import UIKit +import Postbox +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import ChatHistoryEntry +import ChatControllerInteraction +import TelegramPresentationData +import ChatMessageItemCommon + +public enum ChatMessageItemContent: Sequence { + case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection, attributes: ChatMessageEntryAttributes, location: MessageHistoryEntryLocation?) + case group(messages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)]) + + public func effectivelyIncoming(_ accountPeerId: PeerId, associatedData: ChatMessageItemAssociatedData? = nil) -> Bool { + if let subject = associatedData?.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { + return false + } + switch self { + case let .message(message, _, _, _, _): + return message.effectivelyIncoming(accountPeerId) + case let .group(messages): + return messages[0].0.effectivelyIncoming(accountPeerId) + } + } + + public var index: MessageIndex { + switch self { + case let .message(message, _, _, _, _): + return message.index + case let .group(messages): + return messages[0].0.index + } + } + + public var firstMessage: Message { + switch self { + case let .message(message, _, _, _, _): + return message + case let .group(messages): + return messages[0].0 + } + } + + public var firstMessageAttributes: ChatMessageEntryAttributes { + switch self { + case let .message(_, _, _, attributes, _): + return attributes + case let .group(messages): + return messages[0].3 + } + } + + public func makeIterator() -> AnyIterator<(Message, ChatMessageEntryAttributes)> { + var index = 0 + return AnyIterator { () -> (Message, ChatMessageEntryAttributes)? in + switch self { + case let .message(message, _, _, attributes, _): + if index == 0 { + index += 1 + return (message, attributes) + } else { + index += 1 + return nil + } + case let .group(messages): + if index < messages.count { + let currentIndex = index + index += 1 + return (messages[currentIndex].0, messages[currentIndex].3) + } else { + return nil + } + } + } + } +} + +public enum ChatMessageItemAdditionalContent { + case eventLogPreviousMessage(Message) + case eventLogPreviousDescription(Message) + case eventLogPreviousLink(Message) +} + +public enum ChatMessageMerge: Int32 { + case none = 0 + case fullyMerged = 1 + case semanticallyMerged = 2 + + public var merged: Bool { + if case .none = self { + return false + } else { + return true + } + } +} + +public protocol ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { + func updateSelectionState(animated: Bool) +} + +public protocol ChatMessageItem: ListViewItem { + var presentationData: ChatPresentationData { get } + var context: AccountContext { get } + var chatLocation: ChatLocation { get } + var associatedData: ChatMessageItemAssociatedData { get } + var controllerInteraction: ChatControllerInteraction { get } + var content: ChatMessageItemContent { get } + var disableDate: Bool { get } + var effectiveAuthorId: PeerId? { get } + var additionalContent: ChatMessageItemAdditionalContent? { get } + + var headers: [ListViewItemHeader] { get } + + var message: Message { get } + var read: Bool { get } + var unsent: Bool { get } + var sending: Bool { get } + var failed: Bool { get } + + func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) +} + +public func hasCommentButton(item: ChatMessageItem) -> Bool { + let firstMessage = item.content.firstMessage + + var hasDiscussion = false + if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { + hasDiscussion = true + } + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == firstMessage.id { + hasDiscussion = false + } + + if firstMessage.adAttribute != nil { + hasDiscussion = false + } + + if hasDiscussion { + var canComment = false + if case .pinnedMessages = item.associatedData.subject { + canComment = false + } else if firstMessage.id.namespace == Namespaces.Message.Local { + canComment = true + } else { + for attribute in firstMessage.attributes { + if let attribute = attribute as? ReplyThreadMessageAttribute, let commentsPeerId = attribute.commentsPeerId { + switch item.associatedData.channelDiscussionGroup { + case .unknown: + canComment = true + case let .known(groupId): + canComment = groupId == commentsPeerId + } + break + } + } + } + + if canComment { + return true + } + } else if firstMessage.id.peerId.isReplies { + return true + } + return false +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/BUILD new file mode 100644 index 00000000000..0d4b8170026 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageItemCommon", + module_name = "ChatMessageItemCommon", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/Emoji", + ], + visibility = [ + "//visibility:public", + ], +) + diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift new file mode 100644 index 00000000000..23f849fb396 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -0,0 +1,298 @@ +import Foundation +import UIKit +import Display +import Postbox +import TelegramCore +import Emoji + +public struct ChatMessageItemWidthFill { + public var compactInset: CGFloat + public var compactWidthBoundary: CGFloat + public var freeMaximumFillFactor: CGFloat + + public func widthFor(_ width: CGFloat) -> CGFloat { + if width <= self.compactWidthBoundary { + return max(1.0, width - self.compactInset) + } else { + return max(1.0, floor(width * self.freeMaximumFillFactor)) + } + } +} + +public struct ChatMessageItemBubbleLayoutConstants { + public var edgeInset: CGFloat + public var defaultSpacing: CGFloat + public var mergedSpacing: CGFloat + public var maximumWidthFill: ChatMessageItemWidthFill + public var minimumSize: CGSize + public var contentInsets: UIEdgeInsets + public var borderInset: CGFloat + public var strokeInsets: UIEdgeInsets + + public init(edgeInset: CGFloat, defaultSpacing: CGFloat, mergedSpacing: CGFloat, maximumWidthFill: ChatMessageItemWidthFill, minimumSize: CGSize, contentInsets: UIEdgeInsets, borderInset: CGFloat, strokeInsets: UIEdgeInsets) { + self.edgeInset = edgeInset + self.defaultSpacing = defaultSpacing + self.mergedSpacing = mergedSpacing + self.maximumWidthFill = maximumWidthFill + self.minimumSize = minimumSize + self.contentInsets = contentInsets + self.borderInset = borderInset + self.strokeInsets = strokeInsets + } +} + +public struct ChatMessageItemTextLayoutConstants { + public var bubbleInsets: UIEdgeInsets + + public init(bubbleInsets: UIEdgeInsets) { + self.bubbleInsets = bubbleInsets + } +} + +public struct ChatMessageItemImageLayoutConstants { + public var bubbleInsets: UIEdgeInsets + public var statusInsets: UIEdgeInsets + public var defaultCornerRadius: CGFloat + public var mergedCornerRadius: CGFloat + public var contentMergedCornerRadius: CGFloat + public var maxDimensions: CGSize + public var minDimensions: CGSize + + public init(bubbleInsets: UIEdgeInsets, statusInsets: UIEdgeInsets, defaultCornerRadius: CGFloat, mergedCornerRadius: CGFloat, contentMergedCornerRadius: CGFloat, maxDimensions: CGSize, minDimensions: CGSize) { + self.bubbleInsets = bubbleInsets + self.statusInsets = statusInsets + self.defaultCornerRadius = defaultCornerRadius + self.mergedCornerRadius = mergedCornerRadius + self.contentMergedCornerRadius = contentMergedCornerRadius + self.maxDimensions = maxDimensions + self.minDimensions = minDimensions + } +} + +public struct ChatMessageItemVideoLayoutConstants { + public var maxHorizontalHeight: CGFloat + public var maxVerticalHeight: CGFloat + + public init(maxHorizontalHeight: CGFloat, maxVerticalHeight: CGFloat) { + self.maxHorizontalHeight = maxHorizontalHeight + self.maxVerticalHeight = maxVerticalHeight + } +} + +public struct ChatMessageItemInstantVideoConstants { + public var insets: UIEdgeInsets + public var dimensions: CGSize + + public init(insets: UIEdgeInsets, dimensions: CGSize) { + self.insets = insets + self.dimensions = dimensions + } +} + +public struct ChatMessageItemFileLayoutConstants { + public var bubbleInsets: UIEdgeInsets + + public init(bubbleInsets: UIEdgeInsets) { + self.bubbleInsets = bubbleInsets + } +} + +public struct ChatMessageItemWallpaperLayoutConstants { + public var maxTextWidth: CGFloat + + public init(maxTextWidth: CGFloat) { + self.maxTextWidth = maxTextWidth + } +} + +public struct ChatMessageItemLayoutConstants { + public var avatarDiameter: CGFloat + public var timestampHeaderHeight: CGFloat + + public var bubble: ChatMessageItemBubbleLayoutConstants + public var image: ChatMessageItemImageLayoutConstants + public var video: ChatMessageItemVideoLayoutConstants + public var text: ChatMessageItemTextLayoutConstants + public var file: ChatMessageItemFileLayoutConstants + public var instantVideo: ChatMessageItemInstantVideoConstants + public var wallpapers: ChatMessageItemWallpaperLayoutConstants + + public init(avatarDiameter: CGFloat, timestampHeaderHeight: CGFloat, bubble: ChatMessageItemBubbleLayoutConstants, image: ChatMessageItemImageLayoutConstants, video: ChatMessageItemVideoLayoutConstants, text: ChatMessageItemTextLayoutConstants, file: ChatMessageItemFileLayoutConstants, instantVideo: ChatMessageItemInstantVideoConstants, wallpapers: ChatMessageItemWallpaperLayoutConstants) { + self.avatarDiameter = avatarDiameter + self.timestampHeaderHeight = timestampHeaderHeight + self.bubble = bubble + self.image = image + self.video = video + self.text = text + self.file = file + self.instantVideo = instantVideo + self.wallpapers = wallpapers + } + + public static var `default`: ChatMessageItemLayoutConstants { + return self.compact + } + + public static var compact: ChatMessageItemLayoutConstants { + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) + let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 11.0, bottom: 6.0 - UIScreenPixel, right: 11.0)) + let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 0.0, maxDimensions: CGSize(width: 300.0, height: 380.0), minDimensions: CGSize(width: 170.0, height: 74.0)) + let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) + let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) + let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) + let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) + + return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) + } + + public static var regular: ChatMessageItemLayoutConstants { + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) + let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 10.0, bottom: 6.0 - UIScreenPixel, right: 10.0)) + let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0)) + let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) + let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) + let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0)) + let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) + + return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) + } +} + +public func canViewMessageReactionList(message: Message) -> Bool { + var found = false + var canViewList = false + for attribute in message.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + canViewList = attribute.canViewList + found = true + break + } + } + + if !found { + return false + } + + if let peer = message.peers[message.id.peerId] { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + return false + } else { + return canViewList + } + } else if let _ = peer as? TelegramGroup { + return canViewList + } else if let _ = peer as? TelegramUser { + return true + } else { + return false + } + } else { + return false + } +} + +public let chatMessagePeerIdColors: [UIColor] = [ + UIColor(rgb: 0xfc5c51), + UIColor(rgb: 0xfa790f), + UIColor(rgb: 0x895dd5), + UIColor(rgb: 0x0fb297), + UIColor(rgb: 0x00c0c2), + UIColor(rgb: 0x3ca5ec), + UIColor(rgb: 0x3d72ed) +] + +public enum TranscribedText: Equatable { + case success(text: String, isPending: Bool) + case error(AudioTranscriptionMessageAttribute.TranscriptionError) +} + +public func transcribedText(message: Message) -> TranscribedText? { + for attribute in message.attributes { + if let attribute = attribute as? AudioTranscriptionMessageAttribute { + if !attribute.text.isEmpty { + return .success(text: attribute.text, isPending: attribute.isPending) + } else { + if attribute.isPending { + return nil + } else { + return .error(attribute.error ?? .generic) + } + } + } + } + return nil +} + +public func isPollEffectivelyClosed(message: Message, poll: TelegramMediaPoll) -> Bool { + if poll.isClosed { + return true + } else { + return false + } +} + +public extension ChatReplyThreadMessage { + var effectiveTopId: MessageId { + return self.channelMessageId ?? self.messageId + } +} + +public func messageIsElligibleForLargeEmoji(_ message: Message) -> Bool { + if !message.text.isEmpty && message.text.containsOnlyEmoji { + if !(message.textEntitiesAttribute?.entities.isEmpty ?? true) { + return false + } + return true + } else { + return false + } +} + +public func messageIsElligibleForLargeCustomEmoji(_ message: Message) -> Bool { + let text = message.text.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: " ", with: "") + guard !text.isEmpty && text.containsOnlyEmoji else { + return false + } + let entities = message.textEntitiesAttribute?.entities ?? [] + guard entities.count > 0 else { + return false + } + for entity in entities { + if case let .CustomEmoji(_, fileId) = entity.type { + if let _ = message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { + + } else { + return false + } + } else { + return false + } + } + return true +} + +public func canAddMessageReactions(message: Message) -> Bool { + if message.id.namespace != Namespaces.Message.Cloud { + return false + } + if let peer = message.peers[message.id.peerId] { + if let _ = peer as? TelegramSecretChat { + return false + } + } else { + return false + } + for media in message.media { + if let _ = media as? TelegramMediaAction { + return false + } else if let story = media as? TelegramMediaStory { + if story.isMention { + return false + } + } else if let _ = media as? TelegramMediaExpiredContent { + return false + } + } + return true +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD new file mode 100644 index 00000000000..e704fe56750 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD @@ -0,0 +1,42 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageItemImpl", + module_name = "ChatMessageItemImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/Emoji", + "//submodules/PersistentStringHash", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode", + "//submodules/AvatarNode", + "//submodules/TelegramUniversalVideoContent", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/GalleryUI", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/WallpaperBackgroundNode", + "//submodules/AvatarVideoNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift similarity index 82% rename from submodules/TelegramUI/Sources/ChatMessageDateHeader.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index c48afe8e2be..1f9dc254729 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -15,6 +15,7 @@ import HierarchyTrackingLayer import WallpaperBackgroundNode import ChatControllerInteraction import AvatarVideoNode +import ChatMessageItem private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -26,18 +27,18 @@ private let timezoneOffset: Int32 = { private let granularity: Int32 = 60 * 60 * 24 -final class ChatMessageDateHeader: ListViewItemHeader { +public final class ChatMessageDateHeader: ListViewItemHeader { private let timestamp: Int32 private let roundedTimestamp: Int32 private let scheduled: Bool - let id: ListViewItemNode.HeaderId - let presentationData: ChatPresentationData - let controllerInteraction: ChatControllerInteraction? - let context: AccountContext - let action: ((Int32, Bool) -> Void)? + public let id: ListViewItemNode.HeaderId + public let presentationData: ChatPresentationData + public let controllerInteraction: ChatControllerInteraction? + public let context: AccountContext + public let action: ((Int32, Bool) -> Void)? - init(timestamp: Int32, scheduled: Bool, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { + public init(timestamp: Int32, scheduled: Bool, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { self.timestamp = timestamp self.scheduled = scheduled self.presentationData = presentationData @@ -48,10 +49,10 @@ final class ChatMessageDateHeader: ListViewItemHeader { self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.roundedTimestamp)) } - let stickDirection: ListViewItemHeaderStickDirection = .bottom - let stickOverInsets: Bool = true + public let stickDirection: ListViewItemHeaderStickDirection = .bottom + public let stickOverInsets: Bool = true - let height: CGFloat = 34.0 + public let height: CGFloat = 34.0 public func combinesWith(other: ListViewItemHeader) -> Bool { if let other = other as? ChatMessageDateHeader, other.id == self.id { @@ -61,11 +62,11 @@ final class ChatMessageDateHeader: ListViewItemHeader { } } - func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { + public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { return ChatMessageDateHeaderNode(localTimestamp: self.roundedTimestamp, scheduled: self.scheduled, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction, context: self.context, action: self.action) } - func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { guard let node = node as? ChatMessageDateHeaderNode, let next = next as? ChatMessageDateHeader else { return } @@ -114,10 +115,10 @@ private func dateHeaderTimestampId(timestamp: Int32) -> Int32 { } } -final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { - let labelNode: TextNode - let backgroundNode: NavigationBackgroundNode - let stickBackgroundNode: ASImageNode +public final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { + public let labelNode: TextNode + public let backgroundNode: NavigationBackgroundNode + public let stickBackgroundNode: ASImageNode private var backgroundContent: WallpaperBubbleBackgroundNode? @@ -133,7 +134,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { private var absolutePosition: (CGRect, CGSize)? - init(localTimestamp: Int32, scheduled: Bool, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { + public init(localTimestamp: Int32, scheduled: Bool, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction?, context: AccountContext, action: ((Int32, Bool) -> Void)? = nil) { self.presentationData = presentationData self.controllerInteraction = controllerInteraction self.context = context @@ -219,13 +220,13 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.view.addGestureRecognizer(ListViewTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { + public func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { let previousPresentationData = self.presentationData self.presentationData = presentationData @@ -251,11 +252,11 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.setNeedsLayout() } - func updateBackgroundColor(color: UIColor, enableBlur: Bool) { + public func updateBackgroundColor(color: UIColor, enableBlur: Bool) { self.backgroundNode.updateColor(color: color, enableBlur: enableBlur, transition: .immediate) } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absolutePosition = (rect, containerSize) if let backgroundContent = self.backgroundContent { var backgroundFrame = backgroundContent.frame @@ -265,7 +266,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } } - override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { let chatDateSize: CGFloat = 20.0 let chatDateInset: CGFloat = 6.0 @@ -293,7 +294,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } } - override func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { if !self.stickDistanceFactor.isEqual(to: factor) { self.stickBackgroundNode.alpha = factor @@ -311,7 +312,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } } - override func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { + override public func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { self.flashingOnScrolling = isFlashingOnScrolling self.updateFlashing(animated: animated) } @@ -335,7 +336,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } @@ -348,35 +349,35 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { return nil } - override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + override public func touchesCancelled(_ touches: Set?, with event: UIEvent?) { super.touchesCancelled(touches, with: event) } - @objc func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { + @objc private func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { if case .ended = recognizer.state { self.action?(self.localTimestamp, self.stickDistanceFactor < 0.5) } } } -final class ChatMessageAvatarHeader: ListViewItemHeader { - struct Id: Hashable { - var peerId: PeerId - var timestampId: Int32 +public final class ChatMessageAvatarHeader: ListViewItemHeader { + public struct Id: Hashable { + public var peerId: PeerId + public var timestampId: Int32 } - let id: ListViewItemNode.HeaderId - let peerId: PeerId - let peer: Peer? - let messageReference: MessageReference? - let adMessageId: EngineMessage.Id? - let effectiveTimestamp: Int32 - let presentationData: ChatPresentationData - let context: AccountContext - let controllerInteraction: ChatControllerInteraction - let storyStats: PeerStoryStats? + public let id: ListViewItemNode.HeaderId + public let peerId: PeerId + public let peer: Peer? + public let messageReference: MessageReference? + public let adMessageId: EngineMessage.Id? + public let effectiveTimestamp: Int32 + public let presentationData: ChatPresentationData + public let context: AccountContext + public let controllerInteraction: ChatControllerInteraction + public let storyStats: PeerStoryStats? - 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 @@ -399,10 +400,10 @@ final class ChatMessageAvatarHeader: ListViewItemHeader { self.storyStats = storyStats } - let stickDirection: ListViewItemHeaderStickDirection = .top - let stickOverInsets: Bool = false + public let stickDirection: ListViewItemHeaderStickDirection = .top + public let stickOverInsets: Bool = false - let height: CGFloat = 38.0 + public let height: CGFloat = 38.0 public func combinesWith(other: ListViewItemHeader) -> Bool { if let other = other as? ChatMessageAvatarHeader, other.id == self.id { @@ -415,15 +416,18 @@ final class ChatMessageAvatarHeader: ListViewItemHeader { } } - func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { - return ChatMessageAvatarHeaderNode(peerId: self.peerId, peer: self.peer, messageReference: self.messageReference, adMessageId: self.adMessageId, presentationData: self.presentationData, context: self.context, controllerInteraction: self.controllerInteraction, storyStats: self.storyStats, synchronousLoad: synchronousLoad) + public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { + return ChatMessageAvatarHeaderNodeImpl(peerId: self.peerId, peer: self.peer, messageReference: self.messageReference, adMessageId: self.adMessageId, presentationData: self.presentationData, context: self.context, controllerInteraction: self.controllerInteraction, storyStats: self.storyStats, synchronousLoad: synchronousLoad) } - func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { - guard let node = node as? ChatMessageAvatarHeaderNode else { + public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + guard let node = node as? ChatMessageAvatarHeaderNodeImpl else { return } node.updatePresentationData(self.presentationData, context: self.context) + if let peer = self.peer { + node.updatePeer(peer: peer) + } node.updateStoryStats(storyStats: self.storyStats, theme: self.presentationData.theme.theme, force: false) } } @@ -432,7 +436,7 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0) private let maxVideoLoopCount = 3 -final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { +public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, ChatMessageAvatarHeaderNode { private let context: AccountContext private var presentationData: ChatPresentationData private let controllerInteraction: ChatControllerInteraction @@ -440,11 +444,11 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { private let peerId: PeerId private let messageReference: MessageReference? - private let peer: Peer? + private var peer: Peer? private let adMessageId: EngineMessage.Id? private let containerNode: ContextControllerSourceNode - let avatarNode: AvatarNode + public let avatarNode: AvatarNode private var avatarVideoNode: AvatarVideoNode? private var cachedDataDisposable = MetaDisposable() @@ -465,7 +469,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { } } - 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 @@ -512,13 +516,20 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { self.cachedDataDisposable.dispose() } - func setCustomLetters(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, letters: [String], emptyColor: UIColor) { + public func setCustomLetters(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, letters: [String], emptyColor: UIColor) { self.containerNode.isGestureEnabled = false self.avatarNode.setCustomLetters(letters, icon: !letters.isEmpty ? nil : .phone) } + + public func updatePeer(peer: Peer) { + if let previousPeer = self.peer, previousPeer.nameColor != peer.nameColor { + self.peer = peer + self.avatarNode.setPeer(context: self.context, theme: self.presentationData.theme.theme, peer: EnginePeer(peer), authorOfMessage: self.messageReference, overrideImage: nil, emptyColor: .black, synchronousLoad: false, displayDimensions: CGSize(width: 38.0, height: 38.0)) + } + } - func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { + public func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { self.containerNode.isGestureEnabled = peer.smallProfileImage != nil var overrideImage: AvatarNodeImageOverride? @@ -607,7 +618,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { } } - func updateStoryStats(storyStats: PeerStoryStats?, theme: PresentationTheme, force: Bool) { + public func updateStoryStats(storyStats: PeerStoryStats?, theme: PresentationTheme, force: Bool) { /*if storyStats != nil { var backgroundContent: WallpaperBubbleBackgroundNode? if let current = self.backgroundContent { @@ -656,42 +667,42 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { }*/ } - override func didLoad() { + override public func didLoad() { super.didLoad() self.avatarNode.view.addGestureRecognizer(ListViewTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { + public func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { if self.presentationData !== presentationData { self.presentationData = presentationData self.setNeedsLayout() } } - override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { self.containerNode.frame = CGRect(origin: CGPoint(x: leftInset + 3.0, y: 0.0), size: CGSize(width: 38.0, height: 38.0)) self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) } - override func animateRemoved(duration: Double) { + override public func animateRemoved(duration: Double) { self.alpha = 0.0 self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.avatarNode.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false) } - override func animateAdded(duration: Double) { + override public func animateAdded(duration: Double) { self.layer.animateAlpha(from: 0.0, to: self.alpha, duration: 0.2) self.avatarNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) } - override func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { } - override func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { + override public func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { } - func updateSelectionState(animated: Bool) { + public func updateSelectionState(animated: Bool) { let offset: CGFloat = self.controllerInteraction.selectionState != nil ? 42.0 : 0.0 let previousSubnodeTransform = self.subnodeTransform @@ -701,7 +712,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } @@ -709,11 +720,11 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { return result } - override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + override public func touchesCancelled(_ touches: Set?, with event: UIEvent?) { super.touchesCancelled(touches, with: event) } - @objc func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { + @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) diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift similarity index 82% rename from submodules/TelegramUI/Sources/ChatMessageItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index c682ce51f28..be55f582a56 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -11,74 +11,12 @@ import AccountContext import Emoji import PersistentStringHash import ChatControllerInteraction - -public enum ChatMessageItemContent: Sequence { - case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection, attributes: ChatMessageEntryAttributes, location: MessageHistoryEntryLocation?) - case group(messages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)]) - - func effectivelyIncoming(_ accountPeerId: PeerId, associatedData: ChatMessageItemAssociatedData? = nil) -> Bool { - if let subject = associatedData?.subject, case .forwardedMessages = subject { - return false - } - switch self { - case let .message(message, _, _, _, _): - return message.effectivelyIncoming(accountPeerId) - case let .group(messages): - return messages[0].0.effectivelyIncoming(accountPeerId) - } - } - - var index: MessageIndex { - switch self { - case let .message(message, _, _, _, _): - return message.index - case let .group(messages): - return messages[0].0.index - } - } - - var firstMessage: Message { - switch self { - case let .message(message, _, _, _, _): - return message - case let .group(messages): - return messages[0].0 - } - } - - var firstMessageAttributes: ChatMessageEntryAttributes { - switch self { - case let .message(_, _, _, attributes, _): - return attributes - case let .group(messages): - return messages[0].3 - } - } - - public func makeIterator() -> AnyIterator<(Message, ChatMessageEntryAttributes)> { - var index = 0 - return AnyIterator { () -> (Message, ChatMessageEntryAttributes)? in - switch self { - case let .message(message, _, _, attributes, _): - if index == 0 { - index += 1 - return (message, attributes) - } else { - index += 1 - return nil - } - case let .group(messages): - if index < messages.count { - let currentIndex = index - index += 1 - return (messages[currentIndex].0, messages[currentIndex].3) - } else { - return nil - } - } - } - } -} +import ChatHistoryEntry +import ChatMessageItem +import ChatMessageItemView +import ChatMessageStickerItemNode +import ChatMessageAnimatedStickerItemNode +import ChatMessageBubbleItemNode private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge { if let story = media as? TelegramMediaStory, story.isMention { @@ -199,13 +137,11 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs return .none } -func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) -> Bool{ +public func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) -> Bool{ let lhsHeader: ChatMessageDateHeader? let rhsHeader: ChatMessageDateHeader? - if let lhs = lhs as? ChatMessageItem { + if let lhs = lhs as? ChatMessageItemImpl { lhsHeader = lhs.dateHeader - } else if let _ = lhs as? ChatHoleItem { - lhsHeader = nil } else if let lhs = lhs as? ChatUnreadItem { lhsHeader = lhs.header } else if let lhs = lhs as? ChatReplyCountItem { @@ -214,11 +150,8 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - lhsHeader = nil } if let rhs = rhs { - if let rhs = rhs as? ChatMessageItem { + if let rhs = rhs as? ChatMessageItemImpl { rhsHeader = rhs.dateHeader - } else if let _ = rhs as? ChatHoleItem { - //rhsHeader = rhs.header - rhsHeader = nil } else if let rhs = rhs as? ChatUnreadItem { rhsHeader = rhs.header } else if let rhs = rhs as? ChatReplyCountItem { @@ -236,36 +169,16 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - } } -public enum ChatMessageItemAdditionalContent { - case eventLogPreviousMessage(Message) - case eventLogPreviousDescription(Message) - case eventLogPreviousLink(Message) -} - -enum ChatMessageMerge: Int32 { - case none = 0 - case fullyMerged = 1 - case semanticallyMerged = 2 - - var merged: Bool { - if case .none = self { - return false - } else { - return true - } - } -} - -public final class ChatMessageItem: ListViewItem, CustomStringConvertible { - let presentationData: ChatPresentationData - let context: AccountContext - let chatLocation: ChatLocation - let associatedData: ChatMessageItemAssociatedData - let controllerInteraction: ChatControllerInteraction - let content: ChatMessageItemContent - let disableDate: Bool - let effectiveAuthorId: PeerId? - let additionalContent: ChatMessageItemAdditionalContent? +public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible { + public let presentationData: ChatPresentationData + public let context: AccountContext + public let chatLocation: ChatLocation + public let associatedData: ChatMessageItemAssociatedData + public let controllerInteraction: ChatControllerInteraction + public let content: ChatMessageItemContent + public let disableDate: Bool + public let effectiveAuthorId: PeerId? + public let additionalContent: ChatMessageItemAdditionalContent? // MARK: Nicegram let wantTrButton: [(Bool, [String])] @@ -273,9 +186,9 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let dateHeader: ChatMessageDateHeader let avatarHeader: ChatMessageAvatarHeader? - let headers: [ListViewItemHeader] + public let headers: [ListViewItemHeader] - var message: Message { + public var message: Message { switch self.content { case let .message(message, _, _, _, _): return message @@ -284,7 +197,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - var read: Bool { + public var read: Bool { switch self.content { case let .message(_, read, _, _, _): return read @@ -293,7 +206,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - var unsent: Bool { + public var unsent: Bool { switch self.content { case let .message(message, _, _, _, _): return message.flags.contains(.Unsent) @@ -302,7 +215,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - var sending: Bool { + public var sending: Bool { switch self.content { case let .message(message, _, _, _, _): return message.flags.contains(.Sending) @@ -311,7 +224,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - var failed: Bool { + public var failed: Bool { switch self.content { case let .message(message, _, _, _, _): return message.flags.contains(.Failed) @@ -348,7 +261,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { if let forwardInfo = content.firstMessage.forwardInfo { effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), 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) } } displayAuthorInfo = incoming && effectiveAuthor != nil @@ -431,7 +344,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { self.avatarHeader = avatarHeader var headers: [ListViewItemHeader] = [self.dateHeader] - if case .forwardedMessages = associatedData.subject { + if case .messageOptions = associatedData.subject { headers = [] } if let avatarHeader = self.avatarHeader { @@ -485,7 +398,6 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { break loop case let .Video(_, _, flags, _): if flags.contains(.instantRoundVideo) { -// viewClassName = ChatMessageInstantVideoItemNode.self viewClassName = ChatMessageBubbleItemNode.self break loop } @@ -524,7 +436,18 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let nodeLayout = node.asyncLayout() let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) + + var disableDate = self.disableDate + if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { + switch info { + case .reply, .link: + disableDate = true + default: + break + } + } + + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate) node.contentSize = layout.contentSize node.insets = layout.insets @@ -548,18 +471,18 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - final func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { + public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { var mergedTop: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none var dateAtBottom = false - if let top = top as? ChatMessageItem { + if let top = top as? ChatMessageItemImpl { if top.dateHeader.id != self.dateHeader.id { mergedBottom = .none } else { mergedBottom = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, message, top.message) } } - if let bottom = bottom as? ChatMessageItem { + if let bottom = bottom as? ChatMessageItemImpl { if bottom.dateHeader.id != self.dateHeader.id { mergedTop = .none dateAtBottom = true @@ -574,8 +497,6 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { if bottom.header.id != self.dateHeader.id { dateAtBottom = true } - } else if let _ = bottom as? ChatHoleItem { - dateAtBottom = true } else { dateAtBottom = true } @@ -593,7 +514,17 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { async { let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) + var disableDate = self.disableDate + if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { + switch info { + case .reply, .link: + disableDate = true + default: + break + } + } + + let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate) Queue.mainQueue().async { completion(layout, { info in apply(animation, info, false) diff --git a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift similarity index 84% rename from submodules/TelegramUI/Sources/ChatReplyCountItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift index c8e722a9731..e16d544a70f 100644 --- a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift @@ -8,18 +8,19 @@ import TelegramPresentationData import AccountContext import WallpaperBackgroundNode import ChatControllerInteraction +import ChatMessageItemCommon private let titleFont = UIFont.systemFont(ofSize: 13.0) -class ChatReplyCountItem: ListViewItem { - let index: MessageIndex - let isComments: Bool - let count: Int - let presentationData: ChatPresentationData - let header: ChatMessageDateHeader - let controllerInteraction: ChatControllerInteraction +public class ChatReplyCountItem: ListViewItem { + public let index: MessageIndex + public let isComments: Bool + public let count: Int + public let presentationData: ChatPresentationData + public let header: ChatMessageDateHeader + public let controllerInteraction: ChatControllerInteraction - init(index: MessageIndex, isComments: Bool, count: Int, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction) { + public init(index: MessageIndex, isComments: Bool, count: Int, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction) { self.index = index self.isComments = isComments self.count = count @@ -28,7 +29,7 @@ class ChatReplyCountItem: ListViewItem { self.controllerInteraction = controllerInteraction } - 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 = ChatReplyCountItemNode() Queue.mainQueue().async { @@ -62,8 +63,8 @@ class ChatReplyCountItem: ListViewItem { } } -class ChatReplyCountItemNode: ListViewItemNode { - var item: ChatReplyCountItem? +public class ChatReplyCountItemNode: ListViewItemNode { + public var item: ChatReplyCountItem? private let labelNode: TextNode private var backgroundNode: WallpaperBubbleBackgroundNode? private let backgroundColorNode: ASDisplayNode @@ -74,7 +75,7 @@ class ChatReplyCountItemNode: ListViewItemNode { private var absoluteRect: (CGRect, CGSize)? - init() { + public init() { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false @@ -90,17 +91,17 @@ class ChatReplyCountItemNode: ListViewItemNode { self.canBeUsedAsScrollToItemAnchor = false } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatReplyCountItem { let dateAtBottom = !chatItemsHaveCommonDateHeader(item, nextItem) let (layout, apply) = self.asyncLayout()(item, params, dateAtBottom) @@ -110,7 +111,7 @@ class ChatReplyCountItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: ChatReplyCountItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ChatReplyCountItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants @@ -191,7 +192,7 @@ class ChatReplyCountItemNode: ListViewItemNode { } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { var rect = rect rect.origin.y = containerSize.height - rect.maxY + self.insets.top @@ -206,7 +207,7 @@ class ChatReplyCountItemNode: ListViewItemNode { } } - override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: CGPoint(x: value.x, y: -value.y), animationCurve: animationCurve, duration: duration) } diff --git a/submodules/TelegramUI/Sources/ChatUnreadItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift similarity index 84% rename from submodules/TelegramUI/Sources/ChatUnreadItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift index ecce438083b..95604423a56 100644 --- a/submodules/TelegramUI/Sources/ChatUnreadItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift @@ -8,23 +8,24 @@ import TelegramPresentationData import AccountContext import WallpaperBackgroundNode import ChatControllerInteraction +import ChatMessageItemCommon private let titleFont = UIFont.systemFont(ofSize: 13.0) -class ChatUnreadItem: ListViewItem { - let index: MessageIndex - let presentationData: ChatPresentationData - let controllerInteraction: ChatControllerInteraction - let header: ChatMessageDateHeader +public class ChatUnreadItem: ListViewItem { + public let index: MessageIndex + public let presentationData: ChatPresentationData + public let controllerInteraction: ChatControllerInteraction + public let header: ChatMessageDateHeader - init(index: MessageIndex, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, context: AccountContext) { + public init(index: MessageIndex, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, context: AccountContext) { self.index = index self.presentationData = presentationData self.controllerInteraction = controllerInteraction self.header = ChatMessageDateHeader(timestamp: index.timestamp, scheduled: false, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context) } - 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 = ChatUnreadItemNode() @@ -66,12 +67,12 @@ class ChatUnreadItem: ListViewItem { } } -class ChatUnreadItemNode: ListViewItemNode { - var item: ChatUnreadItem? - let backgroundNode: ASImageNode - let labelNode: TextNode +public class ChatUnreadItemNode: ListViewItemNode { + public var item: ChatUnreadItem? + public let backgroundNode: ASImageNode + public let labelNode: TextNode - let activateArea: AccessibilityAreaNode + public let activateArea: AccessibilityAreaNode private var wallpaperBackgroundNode: WallpaperBackgroundNode? private var backgroundContent: WallpaperBubbleBackgroundNode? @@ -82,7 +83,7 @@ class ChatUnreadItemNode: ListViewItemNode { private let layoutConstants = ChatMessageItemLayoutConstants.default - init() { + public init() { self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displayWithoutProcessing = true @@ -107,17 +108,17 @@ class ChatUnreadItemNode: ListViewItemNode { self.canBeUsedAsScrollToItemAnchor = false } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatUnreadItem { let dateAtBottom = !chatItemsHaveCommonDateHeader(item, nextItem) let (layout, apply) = self.asyncLayout()(item, params, dateAtBottom) @@ -127,7 +128,7 @@ class ChatUnreadItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: ChatUnreadItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ChatUnreadItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants let currentTheme = self.theme @@ -186,7 +187,7 @@ class ChatUnreadItemNode: ListViewItemNode { } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { super.updateAbsoluteRect(rect, within: containerSize) self.absolutePosition = (rect, containerSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/BUILD new file mode 100644 index 00000000000..597406f49af --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageItemView", + module_name = "ChatMessageItemView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/ContextUI", + "//submodules/ChatListUI", + "//submodules/TelegramPresentationData", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift similarity index 81% rename from submodules/TelegramUI/Sources/ChatMessageItemView.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 91fe70e1c51..1eb561e3961 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -11,106 +11,11 @@ import ChatListUI import TelegramPresentationData import SwiftSignalKit import ChatControllerInteraction +import ChatMessageItemCommon +import TextFormat +import ChatMessageItem -struct ChatMessageItemWidthFill { - var compactInset: CGFloat - var compactWidthBoundary: CGFloat - var freeMaximumFillFactor: CGFloat - - func widthFor(_ width: CGFloat) -> CGFloat { - if width <= self.compactWidthBoundary { - return max(1.0, width - self.compactInset) - } else { - return max(1.0, floor(width * self.freeMaximumFillFactor)) - } - } -} - -struct ChatMessageItemBubbleLayoutConstants { - var edgeInset: CGFloat - var defaultSpacing: CGFloat - var mergedSpacing: CGFloat - var maximumWidthFill: ChatMessageItemWidthFill - var minimumSize: CGSize - var contentInsets: UIEdgeInsets - var borderInset: CGFloat - var strokeInsets: UIEdgeInsets -} - -struct ChatMessageItemTextLayoutConstants { - var bubbleInsets: UIEdgeInsets -} - -struct ChatMessageItemImageLayoutConstants { - var bubbleInsets: UIEdgeInsets - var statusInsets: UIEdgeInsets - var defaultCornerRadius: CGFloat - var mergedCornerRadius: CGFloat - var contentMergedCornerRadius: CGFloat - var maxDimensions: CGSize - var minDimensions: CGSize -} - -struct ChatMessageItemVideoLayoutConstants { - var maxHorizontalHeight: CGFloat - var maxVerticalHeight: CGFloat -} - -struct ChatMessageItemInstantVideoConstants { - var insets: UIEdgeInsets - var dimensions: CGSize -} - -struct ChatMessageItemFileLayoutConstants { - var bubbleInsets: UIEdgeInsets -} - -struct ChatMessageItemWallpaperLayoutConstants { - var maxTextWidth: CGFloat -} - -struct ChatMessageItemLayoutConstants { - var avatarDiameter: CGFloat - var timestampHeaderHeight: CGFloat - - var bubble: ChatMessageItemBubbleLayoutConstants - var image: ChatMessageItemImageLayoutConstants - var video: ChatMessageItemVideoLayoutConstants - var text: ChatMessageItemTextLayoutConstants - var file: ChatMessageItemFileLayoutConstants - var instantVideo: ChatMessageItemInstantVideoConstants - var wallpapers: ChatMessageItemWallpaperLayoutConstants - - static var `default`: ChatMessageItemLayoutConstants { - return self.compact - } - - fileprivate static var compact: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) - let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) - let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 0.0, maxDimensions: CGSize(width: 300.0, height: 380.0), minDimensions: CGSize(width: 170.0, height: 74.0)) - let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) - let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) - let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) - let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) - - return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) - } - - fileprivate static var regular: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) - let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) - let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0)) - let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) - let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) - let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0)) - let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) - - return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) - } -} - -func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants, ChatMessageItemLayoutConstants), params: ListViewItemLayoutParams, presentationData: ChatPresentationData) -> ChatMessageItemLayoutConstants { +public func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants, ChatMessageItemLayoutConstants), params: ListViewItemLayoutParams, presentationData: ChatPresentationData) -> ChatMessageItemLayoutConstants { var result: ChatMessageItemLayoutConstants if params.width > 680.0 { result = constants.1 @@ -122,8 +27,8 @@ func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants let minRadius: CGFloat = 4.0 let maxRadius: CGFloat = 16.0 let radiusTransition = (presentationData.chatBubbleCorners.mainRadius - minRadius) / (maxRadius - minRadius) - let minInset: CGFloat = 9.0 - let maxInset: CGFloat = 12.0 + let minInset: CGFloat = result.text.bubbleInsets.left + let maxInset: CGFloat = 11.0 let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition))) result.text.bubbleInsets.left = textInset result.text.bubbleInsets.right = textInset @@ -131,16 +36,11 @@ func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants return result } -enum ChatMessageItemBottomNeighbor { +public enum ChatMessageItemBottomNeighbor { case none case merged(semi: Bool) } -enum ChatMessagePeekPreviewContent { - case media(Media) - case url(ASDisplayNode, CGRect, String, Bool) -} - private let voiceMessageDurationFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.unitsStyle = .spellOut @@ -161,30 +61,30 @@ private let fileSizeFormatter: ByteCountFormatter = { return formatter }() -enum ChatMessageAccessibilityCustomActionType { +public enum ChatMessageAccessibilityCustomActionType { case reply case options } -final class ChatMessageAccessibilityCustomAction: UIAccessibilityCustomAction { - let action: ChatMessageAccessibilityCustomActionType +public final class ChatMessageAccessibilityCustomAction: UIAccessibilityCustomAction { + public let action: ChatMessageAccessibilityCustomActionType - init(name: String, target: Any?, selector: Selector, action: ChatMessageAccessibilityCustomActionType) { + public init(name: String, target: Any?, selector: Selector, action: ChatMessageAccessibilityCustomActionType) { self.action = action super.init(name: name, target: target, selector: selector) } } -final class ChatMessageAccessibilityData { - let label: String? - let value: String? - let hint: String? - let traits: UIAccessibilityTraits - let customActions: [ChatMessageAccessibilityCustomAction]? - let singleUrl: String? - - init(item: ChatMessageItem, isSelected: Bool?) { +public final class ChatMessageAccessibilityData { + public let label: String? + public let value: String? + public let hint: String? + public let traits: UIAccessibilityTraits + public let customActions: [ChatMessageAccessibilityCustomAction]? + public let singleUrl: String? + + public init(item: ChatMessageItem, isSelected: Bool?) { var hint: String? var traits: UIAccessibilityTraits = [] var singleUrl: String? @@ -714,14 +614,44 @@ final class ChatMessageAccessibilityData { } } -public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { - let layoutConstants = (ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular) +public enum InternalBubbleTapAction { + public struct Action { + public var action: () -> Void + public var contextMenuOnLongPress: Bool + + public init(_ action: @escaping () -> Void, contextMenuOnLongPress: Bool = false) { + self.action = action + self.contextMenuOnLongPress = contextMenuOnLongPress + } + } + + public struct OpenContextMenu { + public var tapMessage: Message + public var selectAll: Bool + public var subFrame: CGRect + public var disableDefaultPressAnimation: Bool + + public init(tapMessage: Message, selectAll: Bool, subFrame: CGRect, disableDefaultPressAnimation: Bool = false) { + self.tapMessage = tapMessage + self.selectAll = selectAll + self.subFrame = subFrame + self.disableDefaultPressAnimation = disableDefaultPressAnimation + } + } + + case action(Action) + case optionalAction(() -> Void) + case openContextMenu(OpenContextMenu) +} + +open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { + public let layoutConstants = (ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular) - var item: ChatMessageItem? - var accessibilityData: ChatMessageAccessibilityData? - var safeInsets = UIEdgeInsets() + open var item: ChatMessageItem? + open var accessibilityData: ChatMessageAccessibilityData? + open var safeInsets = UIEdgeInsets() - var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)? + open var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)? // MARK: Nicegram public var wantTrButton: [(Bool, [String])] = [(false, [])] @@ -740,22 +670,22 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol fatalError("init(coder:) has not been implemented") } - override public func reuse() { + override open func reuse() { super.reuse() self.item = nil self.frame = CGRect() } - func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { + open func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { self.item = item } - func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + open func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { self.accessibilityData = accessibilityData } - override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatMessageItem { let doLayout = self.asyncLayout() let merged = item.mergedWithItems(top: previousItem, bottom: nextItem) @@ -766,10 +696,10 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func cancelInsertionAnimations() { + open func cancelInsertionAnimations() { } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override open func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { if short { //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } else { @@ -778,7 +708,7 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + open func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { return { _, _, _, _, _ in return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _, _, _ in @@ -786,28 +716,24 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { - return nil - } - - func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { + open func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } - func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { + open func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { return nil } - func updateHiddenMedia() { + open func updateHiddenMedia() { } - func updateSelectionState(animated: Bool) { + open func updateSelectionState(animated: Bool) { } - func updateSearchTextHighlightState() { + open func updateSearchTextHighlightState() { } - func updateHighlightedState(animated: Bool) { + open func updateHighlightedState(animated: Bool) { var isHighlightedInOverlay = false if let item = self.item, let contextHighlightedState = item.controllerInteraction.contextHighlightedState { switch item.content { @@ -827,17 +753,17 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol self.isHighlightedInOverlay = isHighlightedInOverlay } - func updateAutomaticMediaDownloadSettings() { + open func updateAutomaticMediaDownloadSettings() { } - func updateStickerSettings(forceStopAnimations: Bool) { + open func updateStickerSettings(forceStopAnimations: Bool) { } - func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + open func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return nil } - override public func headers() -> [ListViewItemHeader]? { + override open func headers() -> [ListViewItemHeader]? { if let item = self.item { return item.headers } else { @@ -845,7 +771,7 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func performMessageButtonAction(button: ReplyMarkupButton) { + open func performMessageButtonAction(button: ReplyMarkupButton) { if let item = self.item { switch button.action { case .text: @@ -855,7 +781,7 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol if url.hasPrefix("tg://") { concealed = false } - item.controllerInteraction.openUrl(url, concealed, nil, nil) + item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed)) case .requestMap: item.controllerInteraction.shareCurrentLocation() case .requestPhone: @@ -908,7 +834,7 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func presentMessageButtonContextMenu(button: ReplyMarkupButton) { + open func presentMessageButtonContextMenu(button: ReplyMarkupButton) { if let item = self.item { switch button.action { case let .url(url): @@ -919,24 +845,24 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func openMessageContextMenu() { + open func openMessageContextMenu() { } - public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { + open func targetReactionView(value: MessageReaction.Reaction) -> UIView? { return nil } - public func targetForStoryTransition(id: StoryId) -> UIView? { + open func targetForStoryTransition(id: StoryId) -> UIView? { return nil } - func getStatusNode() -> ASDisplayNode? { + open func getStatusNode() -> ASDisplayNode? { return nil } private var attachedAvatarNodeOffset: CGFloat = 0.0 - override public func attachedHeaderNodesUpdated() { + override open func attachedHeaderNodesUpdated() { if !self.attachedAvatarNodeOffset.isZero { self.updateAttachedAvatarNodeOffset(offset: self.attachedAvatarNodeOffset, transition: .immediate) } else { @@ -948,7 +874,7 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func updateAttachedAvatarNodeOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + open func updateAttachedAvatarNodeOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { self.attachedAvatarNodeOffset = offset for headerNode in self.attachedHeaderNodes { if let headerNode = headerNode as? ChatMessageAvatarHeaderNode { @@ -957,10 +883,10 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol } } - func unreadMessageRangeUpdated() { + open func unreadMessageRangeUpdated() { } - public func contentFrame() -> CGRect { + open func contentFrame() -> CGRect { return self.bounds } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/BUILD new file mode 100644 index 00000000000..39944cc9aaa --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageMapBubbleContentNode", + module_name = "ChatMessageMapBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/LiveLocationTimerNode", + "//submodules/PhotoResources", + "//submodules/MediaResources", + "//submodules/LocationResources", + "//submodules/LiveLocationPositionNode", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramStringFormatting", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageLiveLocationTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageLiveLocationTextNode.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatMessageLiveLocationTextNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageLiveLocationTextNode.swift diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 167cbb4a05e..7a945e542f9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -10,12 +10,15 @@ import PhotoResources import MediaResources import LocationResources import LiveLocationPositionNode +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private let titleFont = Font.medium(14.0) private let liveTitleFont = Font.medium(16.0) private let textFont = Font.regular(14.0) -class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { private let imageNode: TransformImageNode private let pinNode: ChatMessageLiveLocationPositionNode private let dateAndStatusNode: ChatMessageDateAndStatusNode @@ -28,7 +31,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { private var timeoutTimer: (SwiftSignalKit.Timer, Int32)? - required init() { + required public init() { self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.pinNode = ChatMessageLiveLocationPositionNode() @@ -42,14 +45,14 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.pinNode) } - override func accessibilityActivate() -> Bool { + override public func accessibilityActivate() -> Bool { if let item = self.item { let _ = item.controllerInteraction.openMessage(item.message, .default) } return true } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -57,14 +60,14 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { self.timeoutTimer?.0.invalidate() } - override func didLoad() { + override public func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:))) self.view.addGestureRecognizer(tapRecognizer) } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeImageLayout = self.imageNode.asyncLayout() let makePinLayout = self.pinNode.asyncLayout() let statusLayout = self.dateAndStatusNode.asyncLayout() @@ -89,7 +92,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } var incoming = item.message.effectivelyIncoming(item.context.account.peerId) - if case .forwardedMessages = item.associatedData.subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { incoming = false } @@ -168,7 +171,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil { var relativePosition = position if case let .linear(top, _) = position { - relativePosition = .linear(top: top, bottom: .Neighbour(false, .freeform, .default)) + relativePosition = .linear(top: top, bottom: .Neighbour(false, .text, .default)) } imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: relativePosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData) @@ -464,19 +467,19 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - 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.2, removeOnCompletion: false) } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(to: media) { let imageNode = self.imageNode return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in @@ -486,7 +489,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { return nil } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false if let currentMedia = self.media, let media = media { for item in media { @@ -501,11 +504,11 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { return mediaHidden } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - return .none + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + return ChatMessageBubbleContentTapAction(content: .none) } - @objc func imageTap(_ recognizer: UITapGestureRecognizer) { + @objc private func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { let _ = item.controllerInteraction.openMessage(item.message, .default) @@ -513,7 +516,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionView(value: value) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD new file mode 100644 index 00000000000..5de32fcb35a --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageMediaBubbleContentNode", + module_name = "ChatMessageMediaBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/GridMessageSelectionNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift similarity index 93% rename from submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 39931215e84..f6fd96bae06 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -10,9 +10,13 @@ import TelegramPresentationData import AccountContext import GridMessageSelectionNode import ChatControllerInteraction +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageInteractiveMediaNode -class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { - override var supportsMosaic: Bool { +public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { + override public var supportsMosaic: Bool { return true } @@ -23,13 +27,13 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { private var media: Media? private var automaticPlayback: Bool? - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { self.interactiveImageNode.visibility = self.visibility != .none } } - required init() { + required public init() { self.interactiveImageNode = ChatMessageInteractiveMediaNode() super.init() @@ -68,11 +72,11 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let interactiveImageLayout = self.interactiveImageNode.asyncLayout() return { item, layoutConstants, preparePosition, selection, constrainedSize, _ in @@ -398,7 +402,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId, var currentMedia = self.media { if let invoice = currentMedia as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { currentMedia = fullMedia @@ -410,16 +414,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return nil } - override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - if let message = self.item?.message, let currentMedia = self.media, !message.containsSecretMedia { - if self.interactiveImageNode.frame.contains(point), self.interactiveImageNode.isReadyForInteractivePreview() { - return (message, .media(currentMedia)) - } - } - return nil - } - - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false var currentMedia = self.media @@ -455,31 +450,31 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return mediaHidden } - override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + override public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.interactiveImageNode.playMediaWithSound() } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - return .none + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + return ChatMessageBubbleContentTapAction(content: .none) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func updateHighlightedState(animated: Bool) -> Bool { + override public func updateHighlightedState(animated: Bool) -> Bool { guard let item = self.item else { return false } @@ -498,7 +493,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return false } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.interactiveImageNode.dateAndStatusNode.isHidden { return self.interactiveImageNode.dateAndStatusNode.reactionView(value: value) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD new file mode 100644 index 00000000000..175efd024c0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessagePollBubbleContentNode", + module_name = "ChatMessagePollBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TextFormat", + "//submodules/UrlEscaping", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/AvatarNode", + "//submodules/TelegramPresentationData", + "//submodules/ChatMessageBackground", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift similarity index 95% rename from submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 1ebd825c24c..da7d147bd51 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -11,103 +11,10 @@ import AccountContext import AvatarNode import TelegramPresentationData import ChatMessageBackground - -func isPollEffectivelyClosed(message: Message, poll: TelegramMediaPoll) -> Bool { - if poll.isClosed { - return true - }/* else if let deadlineTimeout = poll.deadlineTimeout, message.id.namespace == Namespaces.Message.Cloud { - let startDate: Int32 - if let forwardInfo = message.forwardInfo { - startDate = forwardInfo.date - } else { - startDate = message.timestamp - } - - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - if timestamp >= startDate + deadlineTimeout { - return true - } else { - return false - } - }*/ else { - return false - } -} - -private struct PercentCounterItem: Comparable { - var index: Int = 0 - var percent: Int = 0 - var remainder: Int = 0 - - static func <(lhs: PercentCounterItem, rhs: PercentCounterItem) -> Bool { - if lhs.remainder > rhs.remainder { - return true - } else if lhs.remainder < rhs.remainder { - return false - } - return lhs.percent < rhs.percent - } - -} - -private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] { - var left = left - var items = items.sorted(by: <) - var i:Int = 0 - while i != items.count { - let item = items[i] - var j = i + 1 - loop: while j != items.count { - if items[j].percent != item.percent || items[j].remainder != item.remainder { - break loop - } - j += 1 - } - if items[i].remainder == 0 { - break - } - let equal = j - i - if equal <= left { - left -= equal - while i != j { - items[i].percent += 1 - i += 1 - } - } else { - i = j - } - } - return items -} - -func countNicePercent(votes: [Int], total: Int) -> [Int] { - var result:[Int] = [] - var items:[PercentCounterItem] = [] - for _ in votes { - result.append(0) - items.append(PercentCounterItem()) - } - - let count = votes.count - - var left:Int = 100 - for i in 0 ..< votes.count { - let votes = votes[i] - items[i].index = i - items[i].percent = Int((Float(votes) * 100) / Float(total)) - items[i].remainder = (votes * 100) - (items[i].percent * total) - left -= items[i].percent - } - - if left > 0 && left <= count { - items = adjustPercentCount(items, left: left) - } - for item in items { - result[item.index] = item.percent - } - - return result -} +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import PollBubbleTimerNode private final class ChatMessagePollOptionRadioNodeParameters: NSObject { let timestamp: Double @@ -864,7 +771,7 @@ private final class SolutionButtonNode: HighlightableButtonNode { } } -class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private let typeNode: TextNode private var timerNode: PollBubbleTimerNode? @@ -880,11 +787,11 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { private var poll: TelegramMediaPoll? - var solutionTipSourceNode: ASDisplayNode { + public var solutionTipSourceNode: ASDisplayNode { return self.solutionButtonNode } - required init() { + required public init() { self.textNode = TextNode() self.textNode.isUserInteractionEnabled = false self.textNode.contentMode = .topLeft @@ -975,7 +882,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -1005,7 +912,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeTypeLayout = TextNode.asyncLayout(self.typeNode) let makeVotersLayout = TextNode.asyncLayout(self.votersNode) @@ -1678,22 +1585,22 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.avatarsNode.isUserInteractionEnabled = !self.buttonViewResultsTextNode.isHidden } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -1701,17 +1608,17 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .textMention(peerName) + return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return .botCommand(botCommand) + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } else { var isBotChat: Bool = false @@ -1722,7 +1629,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { for optionNode in self.optionNodes { if optionNode.frame.contains(point), case .tap = gesture { if optionNode.isUserInteractionEnabled { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allScheduled.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat { switch poll.publicity { case .anonymous: @@ -1741,7 +1648,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { string = item.presentationData.strings.MessagePoll_QuizCount(result.count) } } - return .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0)) + return ChatMessageBubbleContentTapAction(content: .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0))) case .public: var hasNonZeroVoters = false if let voters = poll.results.voters { @@ -1754,28 +1661,28 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } if hasNonZeroVoters { if !isEstimating { - return .openPollResults(option.opaqueIdentifier) + return ChatMessageBubbleContentTapAction(content: .openPollResults(option.opaqueIdentifier)) } - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } } } } } if self.buttonNode.isUserInteractionEnabled, !self.buttonNode.isHidden, self.buttonNode.frame.contains(point) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } if self.avatarsNode.isUserInteractionEnabled, !self.avatarsNode.isHidden, self.avatarsNode.frame.contains(point) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } if self.solutionButtonNode.isUserInteractionEnabled, !self.solutionButtonNode.isHidden, !self.solutionButtonNode.alpha.isZero, self.solutionButtonNode.frame.contains(point) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } - func updatePollTooltipMessageState(animated: Bool) { + public func updatePollTooltipMessageState(animated: Bool) { guard let item = self.item else { return } @@ -1792,7 +1699,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.statusNode.isHidden { return self.statusNode.reactionView(value: value) } @@ -1801,12 +1708,12 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } private enum PeerAvatarReference: Equatable { - case letters(PeerId, [String]) + case letters(PeerId, PeerNameColor?, [String]) case image(PeerReference, TelegramMediaImageRepresentation) var peerId: PeerId { switch self { - case let .letters(value, _): + case let .letters(value, _, _): return value case let .image(value, _): return value.id @@ -1819,7 +1726,7 @@ private extension PeerAvatarReference { if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) { self = .image(peerReference, photo) } else { - self = .letters(peer.id, peer.displayLetters) + self = .letters(peer.id, peer.nameColor, peer.displayLetters) } } } @@ -1846,7 +1753,7 @@ private let defaultBorderWidth: CGFloat = 1.0 private let avatarFont = avatarPlaceholderFont(size: 8.0) -final class MergedAvatarsNode: ASDisplayNode { +public final class MergedAvatarsNode: ASDisplayNode { private var peers: [PeerAvatarReference] = [] private var images: [PeerId: UIImage] = [:] private var disposables: [PeerId: Disposable] = [:] @@ -1855,9 +1762,9 @@ final class MergedAvatarsNode: ASDisplayNode { private var imageSpacing: CGFloat = defaultMergedImageSpacing private var borderWidthValue: CGFloat = defaultBorderWidth - var pressed: (() -> Void)? + public var pressed: (() -> Void)? - override init() { + override public init() { self.buttonNode = HighlightTrackingButtonNode() super.init() @@ -1878,11 +1785,11 @@ final class MergedAvatarsNode: ASDisplayNode { self.pressed?() } - func updateLayout(size: CGSize) { + public func updateLayout(size: CGSize) { self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) } - func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) { + public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) { self.imageSize = imageSize self.imageSpacing = imageSpacing self.borderWidthValue = borderWidth @@ -1946,11 +1853,11 @@ final class MergedAvatarsNode: ASDisplayNode { } } - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol { + 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 class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { assertNotOnMainThread() let context = UIGraphicsGetCurrentContext()! @@ -1978,9 +1885,9 @@ final class MergedAvatarsNode: ASDisplayNode { context.saveGState() switch parameters.peers[i] { - case let .letters(peerId, letters): + 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) + 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] { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/BUILD new file mode 100644 index 00000000000..cfe550c3ae3 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageProfilePhotoSuggestionContentNode", + module_name = "ChatMessageProfilePhotoSuggestionContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/LocalizedPeerData", + "//submodules/TelegramStringFormatting", + "//submodules/WallpaperBackgroundNode", + "//submodules/ReactionSelectionNode", + "//submodules/PhotoResources", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramUniversalVideoContent", + "//submodules/GalleryUI", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift similarity index 93% rename from submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index 63db8c989dd..d46289cd4f4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -18,8 +18,10 @@ import UniversalMediaPlayer import TelegramUniversalVideoContent import GalleryUI import Markdown +import ChatMessageBubbleContentNode +import ChatMessageItemCommon -class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode { +public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let mediaBackgroundNode: NavigationBackgroundNode private let subtitleNode: TextNode @@ -36,7 +38,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode private let fetchDisposable = MetaDisposable() - required init() { + required public init() { self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) self.mediaBackgroundNode.clipsToBounds = true self.mediaBackgroundNode.cornerRadius = 24.0 @@ -84,7 +86,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -92,7 +94,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode self.fetchDisposable.dispose() } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId { return (self.imageNode, self.imageNode.bounds, { [weak self] in guard let strongSelf = self else { @@ -107,7 +109,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode } } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false var currentMedia: Media? if let item = item { @@ -143,7 +145,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode let _ = item.controllerInteraction.openMessage(item.message, .default) } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeImageLayout = self.imageNode.asyncLayout() let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) @@ -299,7 +301,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if let mediaBackgroundContent = self.mediaBackgroundContent { @@ -310,11 +312,11 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.mediaBackgroundNode.frame.contains(point) { - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/BUILD new file mode 100644 index 00000000000..5b4f65efebe --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageReactionsFooterContentNode", + module_name = "ChatMessageReactionsFooterContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/RadialStatusNode", + "//submodules/AnimatedCountLabelNode", + "//submodules/AnimatedAvatarSetNode", + "//submodules/Components/ReactionButtonListComponent", + "//submodules/AccountContext", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift similarity index 85% rename from submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index d6b74442ad7..86b938b6b2c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -13,49 +13,17 @@ import ReactionButtonListComponent import AccountContext import WallpaperBackgroundNode import ChatControllerInteraction +import ChatMessageBubbleContentNode +import ChatMessageItemCommon -func canViewMessageReactionList(message: Message) -> Bool { - var found = false - var canViewList = false - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - canViewList = attribute.canViewList - found = true - break - } - } - - if !found { - return false - } - - if let peer = message.peers[message.id.peerId] { - if let channel = peer as? TelegramChannel { - if case .broadcast = channel.info { - return false - } else { - return canViewList - } - } else if let _ = peer as? TelegramGroup { - return canViewList - } else if let _ = peer as? TelegramUser { - return true - } else { - return false - } - } else { - return false - } -} - -final class MessageReactionButtonsNode: ASDisplayNode { - enum DisplayType { +public final class MessageReactionButtonsNode: ASDisplayNode { + public enum DisplayType { case incoming case outgoing case freeform } - enum DisplayAlignment { + public enum DisplayAlignment { case left case right } @@ -65,10 +33,10 @@ final class MessageReactionButtonsNode: ASDisplayNode { private var backgroundMaskView: UIView? private var backgroundMaskButtons: [MessageReaction.Reaction: UIView] = [:] - var reactionSelected: ((MessageReaction.Reaction) -> Void)? - var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? + public var reactionSelected: ((MessageReaction.Reaction) -> Void)? + public var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? - override init() { + override public init() { self.container = ReactionButtonsAsyncLayoutContainer() super.init() @@ -78,10 +46,10 @@ final class MessageReactionButtonsNode: ASDisplayNode { } - func update() { + public func update() { } - func prepareUpdate( + public func prepareUpdate( context: AccountContext, presentationData: ChatPresentationData, presentationContext: ChatPresentationContext, @@ -419,7 +387,7 @@ final class MessageReactionButtonsNode: ASDisplayNode { private var absoluteRect: (CGRect, CGSize)? - func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { self.absoluteRect = (rect, containerSize) if let bubbleBackgroundNode = self.bubbleBackgroundNode { @@ -427,7 +395,7 @@ final class MessageReactionButtonsNode: ASDisplayNode { } } - func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { + public func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { self.absoluteRect = (rect, containerSize) if let bubbleBackgroundNode = self.bubbleBackgroundNode { @@ -435,19 +403,19 @@ final class MessageReactionButtonsNode: ASDisplayNode { } } - func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + public func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let bubbleBackgroundNode = self.bubbleBackgroundNode { bubbleBackgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } - func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + public func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { if let bubbleBackgroundNode = self.bubbleBackgroundNode { bubbleBackgroundNode.offsetSpring(value: value, duration: duration, damping: damping) } } - func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { for (key, button) in self.container.buttons { if key == value { return button.view.iconView @@ -456,19 +424,19 @@ final class MessageReactionButtonsNode: ASDisplayNode { return nil } - func animateIn(animation: ListViewItemUpdateAnimation) { + public func animateIn(animation: ListViewItemUpdateAnimation) { for (_, button) in self.container.buttons { animation.animator.animateScale(layer: button.view.layer, from: 0.01, to: 1.0, completion: nil) } } - func animateOut(animation: ListViewItemUpdateAnimation) { + public func animateOut(animation: ListViewItemUpdateAnimation) { for (_, button) in self.container.buttons { animation.animator.updateScale(layer: button.view.layer, scale: 0.01, completion: nil) } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for (_, button) in self.container.buttons { if button.view.frame.contains(point) { if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) { @@ -481,10 +449,10 @@ final class MessageReactionButtonsNode: ASDisplayNode { } } -final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode { private let buttonsNode: MessageReactionButtonsNode - required init() { + required public init() { self.buttonsNode = MessageReactionButtonsNode() super.init() @@ -508,11 +476,11 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let buttonsNode = self.buttonsNode return { item, layoutConstants, preparePosition, _, constrainedSize, _ in @@ -561,25 +529,25 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false))) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), to: CGPoint(), duration: duration, removeOnCompletion: true, additive: true) } - override func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) { + override public func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) { self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), duration: duration, removeOnCompletion: false, additive: true) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in completion() @@ -587,38 +555,38 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false))) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil), result !== self.buttonsNode.view { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view { return result } return nil } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { return self.buttonsNode.reactionTargetView(value: value) } } -final class ChatMessageReactionButtonsNode: ASDisplayNode { - final class Arguments { - let context: AccountContext - let presentationData: ChatPresentationData - let presentationContext: ChatPresentationContext - let availableReactions: AvailableReactions? - let reactions: ReactionsMessageAttribute - let message: Message - let accountPeer: EnginePeer? - let isIncoming: Bool - let constrainedWidth: CGFloat +public final class ChatMessageReactionButtonsNode: ASDisplayNode { + public final class Arguments { + public let context: AccountContext + public let presentationData: ChatPresentationData + public let presentationContext: ChatPresentationContext + public let availableReactions: AvailableReactions? + public let reactions: ReactionsMessageAttribute + public let message: Message + public let accountPeer: EnginePeer? + public let isIncoming: Bool + public let constrainedWidth: CGFloat - init( + public init( context: AccountContext, presentationData: ChatPresentationData, presentationContext: ChatPresentationContext, @@ -643,10 +611,10 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { private let buttonsNode: MessageReactionButtonsNode - var reactionSelected: ((MessageReaction.Reaction) -> Void)? - var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? + public var reactionSelected: ((MessageReaction.Reaction) -> Void)? + public var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? - override init() { + override public init() { self.buttonsNode = MessageReactionButtonsNode() super.init() @@ -662,7 +630,7 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) { + public class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) { return { arguments in let node = maybeNode ?? ChatMessageReactionButtonsNode() @@ -692,12 +660,12 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { } } - func animateIn(animation: ListViewItemUpdateAnimation) { + public func animateIn(animation: ListViewItemUpdateAnimation) { self.buttonsNode.animateIn(animation: animation) self.buttonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - func animateOut(animation: ListViewItemUpdateAnimation, completion: @escaping () -> Void) { + public func animateOut(animation: ListViewItemUpdateAnimation, completion: @escaping () -> Void) { self.buttonsNode.animateOut(animation: animation) animation.animator.updateAlpha(layer: self.buttonsNode.layer, alpha: 0.0, completion: { _ in completion() @@ -705,30 +673,30 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { animation.animator.updateFrame(layer: self.buttonsNode.layer, frame: self.buttonsNode.layer.frame.offsetBy(dx: 0.0, dy: -self.buttonsNode.layer.bounds.height / 2.0), completion: nil) } - func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { return self.buttonsNode.reactionTargetView(value: value) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view { return result } return nil } - func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { self.buttonsNode.update(rect: rect, within: containerSize, transition: transition) } - func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { + public func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { self.buttonsNode.update(rect: rect, within: containerSize, transition: transition) } - func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + public func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { self.buttonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) } - func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + public func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { self.buttonsNode.offsetSpring(value: value, duration: duration, damping: damping) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD new file mode 100644 index 00000000000..2d5e53a9b21 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD @@ -0,0 +1,34 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageReplyInfoNode", + module_name = "ChatMessageReplyInfoNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift similarity index 55% rename from submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 3f1699fb42b..64d8b54e47b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -15,19 +15,72 @@ import InvisibleInkDustNode import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer +import ChatMessageItemCommon +import MessageInlineBlockBackgroundView public enum ChatMessageReplyInfoType { case bubble(incoming: Bool) case standalone } +private let quoteIcon: UIImage = { + return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed().withRenderingMode(.alwaysTemplate) +}() + +private let channelIcon: UIImage = { + let sourceImage = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")! + return generateImage(CGSize(width: sourceImage.size.width + 4.0, height: sourceImage.size.height + 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + sourceImage.draw(at: CGPoint(x: 2.0, y: 1.0 + UIScreenPixel)) + UIGraphicsPopContext() + })!.precomposed().withRenderingMode(.alwaysTemplate) +}() + +private func generateGroupIcon() -> UIImage { + let sourceImage = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")! + return generateImage(CGSize(width: sourceImage.size.width, height: sourceImage.size.height + 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + sourceImage.draw(at: CGPoint(x: 0.0, y: 1.0 - UIScreenPixel)) + UIGraphicsPopContext() + + //context.setFillColor(UIColor.white.cgColor) + //context.fill(CGRect(origin: CGPoint(), size: size)) + })!.precomposed().withRenderingMode(.alwaysTemplate) +} + +private let groupIcon: UIImage = { + return generateGroupIcon() +}() + public class ChatMessageReplyInfoNode: ASDisplayNode { + public final class TransitionReplyPanel { + public let titleNode: ASDisplayNode + public let textNode: ASDisplayNode + public let lineNode: ASDisplayNode + public let imageNode: ASDisplayNode + public let relativeSourceRect: CGRect + public let relativeTargetRect: CGRect + + public init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) { + self.titleNode = titleNode + self.textNode = textNode + self.lineNode = lineNode + self.imageNode = imageNode + self.relativeSourceRect = relativeSourceRect + self.relativeTargetRect = relativeTargetRect + } + } + public class Arguments { public let presentationData: ChatPresentationData public let strings: PresentationStrings public let context: AccountContext public let type: ChatMessageReplyInfoType public let message: Message? + public let replyForward: QuotedReplyMessageAttribute? + public let quote: (quote: EngineMessageReplyQuote, isQuote: Bool)? public let story: StoryId? public let parentMessage: Message public let constrainedSize: CGSize @@ -41,6 +94,8 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { context: AccountContext, type: ChatMessageReplyInfoType, message: Message?, + replyForward: QuotedReplyMessageAttribute?, + quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?, story: StoryId?, parentMessage: Message, constrainedSize: CGSize, @@ -53,6 +108,8 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { self.context = context self.type = type self.message = message + self.replyForward = replyForward + self.quote = quote self.story = story self.parentMessage = parentMessage self.constrainedSize = constrainedSize @@ -70,8 +127,9 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } + private let backgroundView: MessageInlineBlockBackgroundView + private var quoteIconView: UIImageView? private let contentNode: ASDisplayNode - private let lineNode: ASImageNode private var titleNode: TextNode? private var textNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? @@ -79,25 +137,41 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { private var previousMediaReference: AnyMediaReference? private var expiredStoryIconView: UIImageView? + private var currentProgressDisposable: Disposable? + override public init() { + self.backgroundView = MessageInlineBlockBackgroundView(frame: CGRect()) + self.contentNode = ASDisplayNode() self.contentNode.isUserInteractionEnabled = false self.contentNode.displaysAsynchronously = false self.contentNode.contentMode = .left self.contentNode.contentsScale = UIScreenScale - self.lineNode = ASImageNode() - self.lineNode.displaysAsynchronously = false - self.lineNode.displayWithoutProcessing = true - self.lineNode.isLayerBacked = true - super.init() self.addSubnode(self.contentNode) - self.contentNode.addSubnode(self.lineNode) } - public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode) { + deinit { + self.currentProgressDisposable?.dispose() + } + + public func makeProgress() -> Promise { + let progress = Promise() + self.currentProgressDisposable?.dispose() + self.currentProgressDisposable = (progress.get() + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] hasProgress in + guard let self else { + return + } + self.backgroundView.displayProgress = hasProgress + }) + return progress + } + + public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode) @@ -105,25 +179,140 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { return { arguments in let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) - let titleFont = Font.medium(fontSize) + let titleFont = Font.semibold(fontSize) let textFont = Font.regular(fontSize) - var titleString: String - let textString: NSAttributedString + var titleString: NSAttributedString + var textString: NSAttributedString let isMedia: Bool let isText: Bool var isExpiredStory: Bool = false var isStory: Bool = false + let titleColor: UIColor + let mainColor: UIColor + let dustColor: UIColor + var secondaryColor: UIColor? + var tertiaryColor: UIColor? + + var authorNameColor: UIColor? + var dashSecondaryColor: UIColor? + var dashTertiaryColor: UIColor? + + let author = arguments.message?.effectiveAuthor + + let colors = author?.nameColor.flatMap { arguments.context.peerNameColors.get($0, dark: arguments.presentationData.theme.theme.overallDarkAppearance) } + authorNameColor = colors?.main + dashSecondaryColor = colors?.secondary + dashTertiaryColor = colors?.tertiary + + switch arguments.type { + case let .bubble(incoming): + titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor + if incoming { + if let authorNameColor { + mainColor = authorNameColor + secondaryColor = dashSecondaryColor + tertiaryColor = dashTertiaryColor + } else { + mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor + } + } else { + mainColor = arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor + if dashSecondaryColor != nil { + secondaryColor = .clear + } + if dashTertiaryColor != nil { + tertiaryColor = .clear + } + } + dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor + case .standalone: + let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) + titleColor = serviceColor.primaryText + if dashSecondaryColor != nil { + secondaryColor = .clear + } + if dashTertiaryColor != nil { + tertiaryColor = .clear + } + + mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText + dustColor = titleColor + } + if let message = arguments.message { let author = message.effectiveAuthor - titleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount + let rawTitleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor) if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) || arguments.parentMessage.forwardInfo != nil { if let author = forwardInfo.author { - titleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) + let rawTitleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor) } else if let authorSignature = forwardInfo.authorSignature { - titleString = authorSignature + let rawTitleString = authorSignature + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor) + } + } + + if message.id.peerId != arguments.parentMessage.id.peerId, let peer = message.peers[message.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) { + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + let font = titleFont + let runDelegateData = RunDelegateData( + ascent: font.ascender, + descent: font.descender, + width: channelIcon.size.width + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + let rawTitleString = NSMutableAttributedString(attributedString: titleString) + rawTitleString.insert(NSAttributedString(string: ">", attributes: [ + .attachment: channelIcon, + .foregroundColor: titleColor, + NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String): runDelegate + ]), at: 0) + titleString = rawTitleString + } else { + let rawTitleString = NSMutableAttributedString(attributedString: titleString) + rawTitleString.append(NSAttributedString(string: "\u{200B}", font: titleFont, textColor: titleColor)) + rawTitleString.append(NSAttributedString(string: ">", attributes: [ + .attachment: groupIcon, + .foregroundColor: titleColor, + NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String): runDelegate + ])) + rawTitleString.append(NSAttributedString(string: peer.debugDisplayTitle, font: titleFont, textColor: titleColor)) + titleString = rawTitleString + } } } @@ -131,11 +320,39 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { textString = textStringValue isMedia = isMediaValue isText = isTextValue + } else if let replyForward = arguments.replyForward { + if let replyAuthorId = replyForward.peerId, let replyAuthor = arguments.parentMessage.peers[replyAuthorId] { + let rawTitleString = EnginePeer(replyAuthor).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor) + } else { + let rawTitleString = replyForward.authorName ?? " " + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor) + } + + textString = NSAttributedString(string: replyForward.quote?.text ?? arguments.presentationData.strings.VoiceOver_ChatList_Message) + if let media = replyForward.quote?.media { + if let text = replyForward.quote?.text, !text.isEmpty { + isMedia = false + } else { + if let contentKind = mediaContentKind(EngineMedia(media), message: nil, strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) { + let (string, _) = stringForMediaKind(contentKind, strings: arguments.strings) + textString = string + } else { + textString = NSAttributedString(string: arguments.presentationData.strings.VoiceOver_ChatList_Message) + } + isMedia = true + } + } else { + isMedia = false + } + isText = replyForward.quote?.text != nil && replyForward.quote?.text != "" } else if let story = arguments.story { if let authorPeer = arguments.parentMessage.peers[story.peerId] { - titleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) + let rawTitleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor) } else { - titleString = arguments.strings.User_DeletedAccount + let rawTitleString = arguments.strings.User_DeletedAccount + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor) } isText = false @@ -163,47 +380,20 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { isMedia = true } } else { - titleString = " " + titleString = NSAttributedString(string: " ", font: titleFont, textColor: titleColor) textString = NSAttributedString(string: " ") isMedia = true isText = false } - let placeholderColor: UIColor = arguments.parentMessage.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor - - let titleColor: UIColor - let lineImage: UIImage? - let textColor: UIColor - let dustColor: UIColor - - var authorNameColor: UIColor? + let isIncoming = arguments.parentMessage.effectivelyIncoming(arguments.context.account.peerId) - let author = arguments.message?.effectiveAuthor + let placeholderColor: UIColor = isIncoming ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor - if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser { - authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } - if let rawAuthorNameColor = authorNameColor { - var dimColors = false - switch arguments.presentationData.theme.theme.name { - case .builtin(.nightAccent), .builtin(.night): - dimColors = true - default: - break - } - if dimColors { - var hue: CGFloat = 0.0 - var saturation: CGFloat = 0.0 - var brightness: CGFloat = 0.0 - rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) - authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) - } - } - } + let textColor: UIColor switch arguments.type { case let .bubble(incoming): - titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor - lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(arguments.presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(arguments.presentationData.theme.theme) if isExpiredStory || isStory { textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor } else if isMedia { @@ -211,22 +401,22 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } else { textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor } - dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor case .standalone: - let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) - titleColor = serviceColor.primaryText - - let graphics = PresentationResourcesChat.additionalGraphics(arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) - lineImage = graphics.chatServiceVerticalLineImage textColor = titleColor - dustColor = titleColor } - let messageText: NSAttributedString if isText, let message = arguments.message { - var text = foldLineBreaks(message.text) - var messageEntities = message.textEntitiesAttribute?.entities ?? [] + var text: String + var messageEntities: [MessageTextEntity] + + if let quote = arguments.quote?.quote, !quote.text.isEmpty { + text = quote.text + messageEntities = quote.entities + } else { + text = foldLineBreaks(message.text) + messageEntities = message.textEntitiesAttribute?.entities ?? [] + } if let translateToLanguage = arguments.associatedData.translateToLanguage, !text.isEmpty { for attribute in message.attributes { @@ -250,10 +440,27 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } if entities.count > 0 { - messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + messageText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) } else { messageText = NSAttributedString(string: text, font: textFont, textColor: textColor) } + } else if isText, let replyForward = arguments.replyForward, let quote = replyForward.quote { + let entities = quote.entities.filter { entity in + if case .Strikethrough = entity.type { + return true + } else if case .Spoiler = entity.type { + return true + } else if case .CustomEmoji = entity.type { + return true + } else { + return false + } + } + if entities.count > 0 { + messageText = stringWithAppliedEntities(quote.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil) + } else { + messageText = NSAttributedString(string: quote.text, font: textFont, textColor: textColor) + } } else { messageText = NSAttributedString(string: textString.string, font: textFont, textColor: textColor) } @@ -272,7 +479,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { imageDimensions = representation.dimensions.cgSize } break - } else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker { + } else if let file = media as? TelegramMediaFile, !file.isVideoSticker { updatedMediaReference = .message(message: MessageReference(message), media: file) if let dimensions = file.dimensions { @@ -303,6 +510,24 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } } + } else if let replyForward = arguments.replyForward, let media = replyForward.quote?.media { + if let image = media as? TelegramMediaImage { + updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: image) + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + } else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker { + updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: file) + + if let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + imageDimensions = representation.dimensions.cgSize + } + if file.isInstantVideo { + hasRoundImage = true + } + } } var imageTextInset: CGFloat = 0.0 @@ -310,26 +535,57 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { imageTextInset += floor(arguments.presentationData.fontSize.baseDisplaySize * 32.0 / 17.0) } - let maximumTextWidth = max(0.0, arguments.constrainedSize.width - imageTextInset) + let maximumTextWidth = max(0.0, arguments.constrainedSize.width - 8.0 - imageTextInset) var contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height) let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) - let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + var additionalTitleWidth: CGFloat = 0.0 + var maxTitleNumberOfLines = 1 + var maxTextNumberOfLines = 1 + var adjustedConstrainedTextSize = contrainedTextSize + var textCutout: TextNodeCutout? + var textCutoutWidth: CGFloat = 0.0 + + var isQuote = false + if let quote = arguments.quote, quote.isQuote { + isQuote = true + } else if let replyForward = arguments.replyForward, replyForward.quote != nil, replyForward.isQuote { + isQuote = true + } + + if isQuote { + additionalTitleWidth += 10.0 + maxTitleNumberOfLines = 2 + maxTextNumberOfLines = 5 + if imageTextInset != 0.0 { + adjustedConstrainedTextSize.width += imageTextInset + textCutout = TextNodeCutout(topLeft: CGSize(width: imageTextInset + 6.0, height: 10.0)) + textCutoutWidth = imageTextInset + 6.0 + } + } + + let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets)) if isExpiredStory || isStory { contrainedTextSize.width -= 26.0 } - let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + + if titleLayout.numberOfLines > 1 { + textCutout = nil + } + + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: maxTextNumberOfLines, truncationType: .end, constrainedSize: adjustedConstrainedTextSize, alignment: .natural, lineSpacing: 0.07, cutout: textCutout, insets: textInsets)) let imageSide: CGFloat - imageSide = titleLayout.size.height + textLayout.size.height - 12.0 + let titleLineHeight: CGFloat = titleLayout.linesRects().first?.height ?? 12.0 + imageSide = titleLineHeight * 2.0 var applyImage: (() -> TransformImageNode)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: imageSide, height: imageSide) leftInset += imageSide + 6.0 - var radius: CGFloat = 6.0 + var radius: CGFloat = 4.0 var imageSize = imageDimensions.aspectFilled(boundingSize) if hasRoundImage { radius = boundingSize.width / 2.0 @@ -374,12 +630,15 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } - var size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing) + var size = CGSize() + size.width = max(titleLayout.size.width + additionalTitleWidth - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right - textCutoutWidth) + leftInset + 6.0 + size.height = titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing + size.height += 2.0 if isExpiredStory || isStory { size.width += 16.0 } - return (size, { attemptSynchronous in + return (size, { realSize, attemptSynchronous, animation in let node: ChatMessageReplyInfoNode if let maybeNode = maybeNode { node = maybeNode @@ -419,7 +678,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { node.addSubnode(imageNode) node.imageNode = imageNode } - imageNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 3.0), size: CGSize(width: imageSide, height: imageSide)) + imageNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 4.0), size: CGSize(width: imageSide, height: imageSide)) if let updateImageSignal = updateImageSignal { imageNode.setSignal(updateImageSignal) @@ -434,7 +693,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: spacing - textInsets.top + 1.0), size: titleLayout.size) - let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size) + let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0 - textCutoutWidth, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size) textNode.textNode.frame = textFrame.offsetBy(dx: (isExpiredStory || isStory) ? 18.0 : 0.0, dy: 0.0) if isExpiredStory || isStory { @@ -490,9 +749,57 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { dustNode.removeFromSupernode() node.dustNode = nil } - - node.lineNode.image = lineImage - node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 4.0))) + + if node.backgroundView.superview == nil { + node.contentNode.view.insertSubview(node.backgroundView, at: 0) + } + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: realSize.width, height: realSize.height)) + + node.backgroundView.frame = backgroundFrame + + var pattern: MessageInlineBlockBackgroundView.Pattern? + if let backgroundEmojiId = author?.backgroundEmojiId { + pattern = MessageInlineBlockBackgroundView.Pattern( + context: arguments.context, + fileId: backgroundEmojiId, + file: arguments.parentMessage.associatedMedia[MediaId( + namespace: Namespaces.Media.CloudFile, + id: backgroundEmojiId + )] as? TelegramMediaFile + ) + } + var isTransparent: Bool = false + if case .standalone = arguments.type { + isTransparent = true + } + node.backgroundView.update( + size: backgroundFrame.size, + isTransparent: isTransparent, + primaryColor: mainColor, + secondaryColor: secondaryColor, + thirdColor: tertiaryColor, + pattern: pattern, + animation: animation + ) + + if isQuote { + let quoteIconView: UIImageView + if let current = node.quoteIconView { + quoteIconView = current + } else { + quoteIconView = UIImageView(image: quoteIcon) + node.quoteIconView = quoteIconView + node.contentNode.view.addSubview(quoteIconView) + } + quoteIconView.tintColor = mainColor + quoteIconView.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 4.0 - quoteIcon.size.width, y: backgroundFrame.minY + 4.0), size: quoteIcon.size) + } else { + if let quoteIconView = node.quoteIconView { + node.quoteIconView = nil + quoteIconView.removeFromSuperview() + } + } node.contentNode.frame = CGRect(origin: CGPoint(), size: size) @@ -500,8 +807,19 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { }) } } + + public func updateTouchesAtPoint(_ point: CGPoint?) { + var isHighlighted = false + if point != nil { + isHighlighted = true + } + + let transition: ContainedViewLayoutTransition = .animated(duration: isHighlighted ? 0.3 : 0.2, curve: .easeInOut) + let scale: CGFloat = isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0 + transition.updateSublayerTransformScale(node: self, scale: scale, beginWithCurrentState: true) + } - func animateFromInputPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint { + public func animateFromInputPanel(sourceReplyPanel: TransitionReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint { let sourceParentNode = ASDisplayNode() let sourceParentOffset: CGPoint @@ -588,19 +906,19 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } do { - let lineNode = self.lineNode + let backgroundView = self.backgroundView let offset = CGPoint( - x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - lineNode.frame.minX, - y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - lineNode.frame.minY + x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - backgroundView.frame.minX, + y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - backgroundView.frame.minY ) - transition.horizontal.animatePositionAdditive(node: lineNode, offset: CGPoint(x: offset.x, y: 0.0)) - transition.vertical.animatePositionAdditive(node: lineNode, offset: CGPoint(x: 0.0, y: offset.y)) + transition.horizontal.animatePositionAdditive(layer: backgroundView.layer, offset: CGPoint(x: offset.x, y: 0.0)) + transition.vertical.animatePositionAdditive(layer: backgroundView.layer, offset: CGPoint(x: 0.0, y: offset.y)) sourceParentNode.addSubnode(sourceReplyPanel.lineNode) - lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) sourceReplyPanel.lineNode.frame = sourceReplyPanel.lineNode.frame .offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/BUILD new file mode 100644 index 00000000000..a8bc3b4d00b --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageRestrictedBubbleContentNode", + module_name = "ChatMessageRestrictedBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) + diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift similarity index 92% rename from submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 437d1b8a93b..c11a20eba41 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -7,12 +7,15 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import TextFormat +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon -class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private let statusNode: ChatMessageDateAndStatusNode - required init() { + required public init() { self.textNode = TextNode() self.statusNode = ChatMessageDateAndStatusNode() @@ -25,11 +28,11 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.textNode) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let textLayout = TextNode.asyncLayout(self.textNode) let statusLayout = self.statusNode.asyncLayout() @@ -204,17 +207,17 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode/BUILD new file mode 100644 index 00000000000..3e8e689e0d5 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageSelectionNode", + module_name = "ChatMessageSelectionNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/TelegramPresentationData", + "//submodules/CheckNode", + "//submodules/TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageSelectionNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode/Sources/ChatMessageSelectionNode.swift similarity index 74% rename from submodules/TelegramUI/Sources/ChatMessageSelectionNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode/Sources/ChatMessageSelectionNode.swift index 3921dad579c..2f9d7a0b10f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSelectionNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode/Sources/ChatMessageSelectionNode.swift @@ -5,13 +5,13 @@ import TelegramPresentationData import CheckNode import TelegramCore -final class ChatMessageSelectionNode: ASDisplayNode { +public final class ChatMessageSelectionNode: ASDisplayNode { private let toggle: (Bool) -> Void - private(set) var selected = false + public private(set) var selected = false private let checkNode: CheckNode - init(wallpaper: TelegramWallpaper, theme: PresentationTheme, toggle: @escaping (Bool) -> Void) { + public init(wallpaper: TelegramWallpaper, theme: PresentationTheme, toggle: @escaping (Bool) -> Void) { self.toggle = toggle let style: CheckNodeTheme.Style @@ -29,26 +29,26 @@ final class ChatMessageSelectionNode: ASDisplayNode { self.addSubnode(self.checkNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func updateSelected(_ selected: Bool, animated: Bool) { + public func updateSelected(_ selected: Bool, animated: Bool) { if self.selected != selected { self.selected = selected self.checkNode.setSelected(selected, animated: animated) } } - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.toggle(!self.selected) } } - func updateLayout(size: CGSize, leftInset: CGFloat) { + public func updateLayout(size: CGSize, leftInset: CGFloat) { let checkSize = CGSize(width: 28.0, height: 28.0) self.checkNode.frame = CGRect(origin: CGPoint(x: 6.0 + leftInset, y: floor((size.height - checkSize.height) / 2.0)), size: checkSize) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/BUILD new file mode 100644 index 00000000000..cc323f2e045 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageShareButton", + module_name = "ChatMessageShareButton", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/AccountContext", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift new file mode 100644 index 00000000000..b7f8301eb64 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift @@ -0,0 +1,180 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ChatControllerInteraction +import AccountContext +import TelegramCore +import Postbox +import WallpaperBackgroundNode +import ChatMessageItemCommon + +public class ChatMessageShareButton: HighlightableButtonNode { + private var backgroundContent: WallpaperBubbleBackgroundNode? + private var backgroundBlurView: PortalView? + + private let iconNode: ASImageNode + private var iconOffset = CGPoint() + + private var theme: PresentationTheme? + private var isReplies: Bool = false + + private var textNode: ImmediateTextNode? + + private var absolutePosition: (CGRect, CGSize)? + + public init() { + self.iconNode = ASImageNode() + + super.init(pointerStyle: nil) + + self.allowsGroupOpacity = true + + self.addSubnode(self.iconNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Nicegram (translateButton) + public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false, translateButton: Bool = false) -> CGSize { + var isReplies = false + var replyCount = 0 + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { + for attribute in message.attributes { + if let attribute = attribute as? ReplyThreadMessageAttribute { + replyCount = Int(attribute.count) + isReplies = true + break + } + } + } + if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.effectiveTopId == message.id { + replyCount = 0 + isReplies = false + } + if disableComments { + replyCount = 0 + isReplies = false + } + + if self.theme !== presentationData.theme.theme || self.isReplies != isReplies { + self.theme = presentationData.theme.theme + self.isReplies = isReplies + + var updatedIconImage: UIImage? + var updatedIconOffset = CGPoint() + // MARK: Nicegram (if translateButton) + if translateButton { + updatedIconImage = PresentationResourcesChat.chatTranslateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + } else if case .pinnedMessages = subject { + updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + updatedIconOffset = CGPoint(x: UIScreenPixel, y: 1.0) + } else if isReplies { + updatedIconImage = PresentationResourcesChat.chatFreeCommentButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + } else if message.id.peerId.isRepliesOrSavedMessages(accountPeerId: account.peerId) { + updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + updatedIconOffset = CGPoint(x: UIScreenPixel, y: 1.0) + } else { + updatedIconImage = PresentationResourcesChat.chatFreeShareButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + } + //self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) + self.iconNode.image = updatedIconImage + self.iconOffset = updatedIconOffset + } + var size = CGSize(width: 30.0, height: 30.0) + var offsetIcon = false + if isReplies, replyCount > 0 { + offsetIcon = true + + let textNode: ImmediateTextNode + if let current = self.textNode { + textNode = current + } else { + textNode = ImmediateTextNode() + self.textNode = textNode + self.addSubnode(textNode) + } + + let textColor = bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: presentationData.theme.wallpaper) + + let countString: String + if replyCount >= 1000 * 1000 { + countString = "\(replyCount / 1000_000)M" + } else if replyCount >= 1000 { + countString = "\(replyCount / 1000)K" + } else { + countString = "\(replyCount)" + } + + textNode.attributedText = NSAttributedString(string: countString, font: Font.regular(11.0), textColor: textColor) + let textSize = textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + size.height += textSize.height - 1.0 + textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: size.height - textSize.height - 4.0), size: textSize) + } else if let textNode = self.textNode { + self.textNode = nil + textNode.removeFromSupernode() + } + + if self.backgroundBlurView == nil { + if let backgroundBlurView = controllerInteraction.presentationContext.backgroundNode?.makeFreeBackground() { + self.backgroundBlurView = backgroundBlurView + self.view.insertSubview(backgroundBlurView.view, at: 0) + + backgroundBlurView.view.clipsToBounds = true + } + } + if let backgroundBlurView = self.backgroundBlurView { + backgroundBlurView.view.frame = CGRect(origin: CGPoint(), size: size) + backgroundBlurView.view.layer.cornerRadius = min(size.width, size.height) / 2.0 + } + + //self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + //self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(self.backgroundNode.bounds.width, self.backgroundNode.bounds.height) / 2.0, transition: .immediate) + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0) + self.iconOffset.x, y: floor((size.width - image.size.width) / 2.0) - (offsetIcon ? 1.0 : 0.0) + self.iconOffset.y), size: image.size) + } + + + if controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { + if self.backgroundContent == nil, let backgroundContent = controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + self.backgroundContent = backgroundContent + self.insertSubnode(backgroundContent, at: 0) + } + } else { + self.backgroundContent?.removeFromSupernode() + self.backgroundContent = nil + } + + if let backgroundContent = self.backgroundContent { + //self.backgroundNode.isHidden = true + self.backgroundBlurView?.view.isHidden = true + backgroundContent.cornerRadius = min(size.width, size.height) / 2.0 + backgroundContent.frame = CGRect(origin: CGPoint(), size: size) + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } else { + //self.backgroundNode.isHidden = false + self.backgroundBlurView?.view.isHidden = false + } + + return size + } + + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absolutePosition = (rect, containerSize) + if let backgroundContent = self.backgroundContent { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/BUILD new file mode 100644 index 00000000000..54233cea218 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/BUILD @@ -0,0 +1,45 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageStickerItemNode", + module_name = "ChatMessageStickerItemNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/StickerResources", + "//submodules/ContextUI", + "//submodules/Markdown", + "//submodules/ShimmerEffect", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageShareButton", + "//submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode", + "//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift similarity index 84% rename from submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index c8d04374bf6..9e077fedced 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -15,18 +15,31 @@ import ShimmerEffect import WallpaperBackgroundNode import ChatControllerInteraction import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageReplyInfoNode +import ChatMessageItem +import ChatMessageItemView +import ChatMessageSwipeToReplyNode +import ChatMessageSelectionNode +import ChatMessageDeliveryFailedNode +import ChatMessageShareButton +import ChatMessageThreadInfoNode +import ChatMessageActionButtonsNode +import ChatMessageReactionsFooterContentNode +import ChatSwipeToReplyRecognizer private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont -class ChatMessageStickerItemNode: ChatMessageItemView { - let contextSourceNode: ContextExtractedContentContainingNode +public class ChatMessageStickerItemNode: ChatMessageItemView { + public let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode - let imageNode: TransformImageNode + public let imageNode: TransformImageNode private var backgroundNode: WallpaperBubbleBackgroundNode? private var placeholderNode: StickerShimmerEffectNode - var textNode: TextNode? + public var textNode: TextNode? private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? @@ -35,7 +48,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var deliveryFailedNode: ChatMessageDeliveryFailedNode? private var shareButtonNode: ChatMessageShareButton? - var telegramFile: TelegramMediaFile? + public var telegramFile: TelegramMediaFile? private let fetchDisposable = MetaDisposable() private var viaBotNode: TextNode? @@ -43,8 +56,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var threadInfoNode: ChatMessageThreadInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundContent: WallpaperBubbleBackgroundNode? - private var replyBackgroundNode: NavigationBackgroundNode? private var forwardInfoNode: ChatMessageForwardInfoNode? + private var forwardBackgroundContent: WallpaperBubbleBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? private var reactionButtonsNode: ChatMessageReactionButtonsNode? @@ -55,13 +68,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var currentSwipeToReplyTranslation: CGFloat = 0.0 + private var replyRecognizer: ChatSwipeToReplyRecognizer? private var currentSwipeAction: ChatControllerInteractionSwipeAction? private var appliedForwardInfo: (Peer?, String?)? private var enableSynchronousImageApply: Bool = false - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = oldValue != .none let isVisible = self.visibility != .none @@ -81,7 +95,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - required init() { + required public init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() @@ -145,8 +159,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { switch action { case .action, .optionalAction: break - case let .openContextMenu(tapMessage, selectAll, subFrame): - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, strongSelf, subFrame, gesture, nil) + case let .openContextMenu(openContextMenu): + item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, strongSelf, openContextMenu.subFrame, gesture, nil) } } } @@ -165,7 +179,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -173,7 +187,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.fetchDisposable.dispose() } - private func removePlaceholder(animated: Bool) { if !animated { self.placeholderNode.removeFromSupernode() @@ -185,7 +198,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func didLoad() { + override public func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) @@ -221,7 +234,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let action = strongSelf.gestureRecognized(gesture: .longTap, location: point, recognizer: recognizer) { switch action { case let .action(f): - f() + f.action() recognizer.cancel() case let .optionalAction(f): f() @@ -254,12 +267,23 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } return false } + if let item = self.item { + let _ = item + replyRecognizer.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + self.view.disablesInteractiveTransitionGestureRecognizer = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + } + self.replyRecognizer = replyRecognizer self.view.addGestureRecognizer(replyRecognizer) } - override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { + override public func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { super.setupItem(item, synchronousLoad: synchronousLoad) + self.replyRecognizer?.allowBothDirections = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + if self.isNodeLoaded { + self.view.disablesInteractiveTransitionGestureRecognizer = false//!item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + } + for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { if self.telegramFile != telegramFile { @@ -275,7 +299,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } private var absoluteRect: (CGRect, CGSize)? - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if !self.contextSourceNode.isExtractedToContextPreview { var rect = rect @@ -326,10 +350,18 @@ class ChatMessageStickerItemNode: ChatMessageItemView { replyBackgroundContent.update(rect: rect, within: containerSize, transition: .immediate) } + + if let forwardBackgroundContent = self.forwardBackgroundContent { + var forwardBackgroundContentFrame = forwardBackgroundContent.frame + forwardBackgroundContentFrame.origin.x += rect.minX + forwardBackgroundContentFrame.origin.y += rect.minY + + forwardBackgroundContent.update(rect: rect, within: containerSize, transition: .immediate) + } } } - override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } @@ -339,7 +371,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { super.updateAccessibilityData(accessibilityData) self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label @@ -370,7 +402,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let displaySize = CGSize(width: 184.0, height: 184.0) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants @@ -514,7 +546,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject { needsShareButton = false } @@ -602,10 +634,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var viaBotApply: (TextNodeLayout, () -> TextNode)? var threadInfoApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode)? var replyMarkup: ReplyMarkupMessageAttribute? - var availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + var availableWidth = min(200.0, max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)) + availableWidth -= 20.0 if isEmoji { availableWidth -= 24.0 } @@ -625,6 +658,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } var replyMessage: Message? + var replyForward: QuotedReplyMessageAttribute? + var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -652,6 +687,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { @@ -659,7 +697,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - var hasReply = replyMessage != nil || replyStory != nil + var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil if case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId { hasReply = false @@ -679,13 +717,15 @@ class ChatMessageStickerItemNode: ChatMessageItemView { )) } - if hasReply, (replyMessage != nil || replyStory != nil) { + if hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, strings: item.presentationData.strings, context: item.context, type: .standalone, message: replyMessage, + replyForward: replyForward, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), @@ -748,14 +788,19 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } let availableForwardWidth = max(60.0, availableWidth + 6.0) - forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableForwardWidth, height: CGFloat.greatestFiniteMagnitude)) + forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableForwardWidth, height: CGFloat.greatestFiniteMagnitude)) } var needsReplyBackground = false - if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil { + if replyInfoApply != nil { needsReplyBackground = true } + var needsForwardBackground = false + if viaBotApply != nil || forwardInfoSizeApply != nil { + needsForwardBackground = true + } + var maxContentWidth = imageSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { @@ -939,34 +984,31 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if needsReplyBackground { - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) - } else { - let replyBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)) - strongSelf.replyBackgroundNode = replyBackgroundNode - strongSelf.contextSourceNode.contentNode.addSubnode(replyBackgroundNode) - } - - if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { - if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { - backgroundContent.clipsToBounds = true - strongSelf.replyBackgroundContent = backgroundContent - strongSelf.insertSubnode(backgroundContent, at: 0) - } - } else { - strongSelf.replyBackgroundContent?.removeFromSupernode() - strongSelf.replyBackgroundContent = nil + if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.replyBackgroundContent = backgroundContent + strongSelf.contextSourceNode.contentNode.insertSubnode(backgroundContent, at: 0) } - } else if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.removeFromSupernode() - strongSelf.replyBackgroundNode = nil - + } else { if let replyBackgroundContent = strongSelf.replyBackgroundContent { replyBackgroundContent.removeFromSupernode() strongSelf.replyBackgroundContent = nil } } + if needsForwardBackground { + if strongSelf.forwardBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.forwardBackgroundContent = backgroundContent + strongSelf.contextSourceNode.contentNode.insertSubnode(backgroundContent, at: 0) + } + } else { + if let forwardBackgroundContent = strongSelf.forwardBackgroundContent { + forwardBackgroundContent.removeFromSupernode() + strongSelf.forwardBackgroundContent = nil + } + } + var headersOffset: CGFloat = 0.0 if let (threadInfoSize, threadInfoApply) = threadInfoApply { let threadInfoNode = threadInfoApply(synchronousLoads) @@ -994,16 +1036,24 @@ class ChatMessageStickerItemNode: ChatMessageItemView { messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: 0.0) } + var forwardAreaFrame: CGRect? if let (viaBotLayout, viaBotApply) = viaBotApply, forwardInfoSizeApply == nil { let viaBotNode = viaBotApply() if strongSelf.viaBotNode == nil { strongSelf.viaBotNode = viaBotNode strongSelf.contextSourceNode.contentNode.addSubnode(viaBotNode) } - let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0), size: viaBotLayout.size) + let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0 + 5.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0 - 5.0)), y: headersOffset + 8.0), size: viaBotLayout.size) + viaBotNode.frame = viaBotFrame messageInfoSize = CGSize(width: messageInfoSize.width, height: viaBotLayout.size.height) + + if let forwardAreaFrameValue = forwardAreaFrame { + forwardAreaFrame = forwardAreaFrameValue.union(viaBotFrame) + } else { + forwardAreaFrame = viaBotFrame + } } else if let viaBotNode = strongSelf.viaBotNode { viaBotNode.removeFromSupernode() strongSelf.viaBotNode = nil @@ -1019,10 +1069,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { forwardInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize) + let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0 + 5.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0 - 5.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize) forwardInfoNode.frame = forwardInfoFrame - messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height - 1.0) + messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height + 8.0) + + if let forwardAreaFrameValue = forwardAreaFrame { + forwardAreaFrame = forwardAreaFrameValue.union(forwardInfoFrame) + } else { + forwardAreaFrame = forwardInfoFrame + } } else if let forwardInfoNode = strongSelf.forwardInfoNode { if animation.isAnimated { if let forwardInfoNode = strongSelf.forwardInfoNode { @@ -1037,13 +1093,25 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + var forwardBackgroundFrame: CGRect? + if let forwardAreaFrame { + forwardBackgroundFrame = forwardAreaFrame.insetBy(dx: -6.0, dy: -3.0) + } + + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(synchronousLoads) + if headersOffset != 0.0 { + headersOffset += 6.0 + } + + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - replyInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) + replyBackgroundFrame = replyInfoFrame + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads, animation) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) @@ -1052,26 +1120,25 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode = nil } - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.frame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)) - 4.0, y: headersOffset + 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) - - let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 - replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) - - if let backgroundContent = strongSelf.replyBackgroundContent { - let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 - - replyBackgroundNode.isHidden = true - backgroundContent.cornerRadius = cornerRadius - backgroundContent.frame = replyBackgroundNode.frame - if let (rect, containerSize) = strongSelf.absoluteRect { - var backgroundFrame = backgroundContent.frame - backgroundFrame.origin.x += rect.minX - backgroundFrame.origin.y += rect.minY - backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) - } - } else { - replyBackgroundNode.isHidden = false + if let backgroundContent = strongSelf.replyBackgroundContent, let replyBackgroundFrame { + backgroundContent.cornerRadius = 4.0 + backgroundContent.frame = replyBackgroundFrame + if let (rect, containerSize) = strongSelf.absoluteRect { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + if let backgroundContent = strongSelf.forwardBackgroundContent, let forwardBackgroundFrame { + backgroundContent.cornerRadius = 4.0 + backgroundContent.frame = forwardBackgroundFrame + if let (rect, containerSize) = strongSelf.absoluteRect { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } } @@ -1080,7 +1147,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode?.alpha = panelsAlpha strongSelf.viaBotNode?.alpha = panelsAlpha strongSelf.forwardInfoNode?.alpha = panelsAlpha - strongSelf.replyBackgroundNode?.alpha = panelsAlpha + strongSelf.replyBackgroundContent?.alpha = panelsAlpha + strongSelf.forwardBackgroundContent?.alpha = panelsAlpha if isFailed { let deliveryFailedNode: ChatMessageDeliveryFailedNode @@ -1237,7 +1305,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { @@ -1250,14 +1318,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } switch action { case let .action(f): - f() + f.action() case let .optionalAction(f): f() - case let .openContextMenu(tapMessage, selectAll, subFrame): + case let .openContextMenu(openContextMenu): if canAddMessageReactions(message: item.message) { - item.controllerInteraction.updateMessageReaction(tapMessage, .default) + item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default) } else { - item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil, nil) + item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil) } } } else if case .tap = gesture { @@ -1303,12 +1371,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) }) } else if let attribute = attribute as? ReplyStoryAttribute { return .optionalAction({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .optionalAction({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }) } } } @@ -1326,7 +1398,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return } } - item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) + item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil)) } else if let peer = forwardInfo.source ?? forwardInfo.author { item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { @@ -1335,7 +1407,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if forwardInfoNode.hasAction(at: self.view.convert(location, to: forwardInfoNode.view)) { - return .action({}) + return .action(InternalBubbleTapAction.Action {}) } else { return .optionalAction(performAction) } @@ -1351,7 +1423,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return nil case .longTap, .doubleTap, .secondaryTap: if let item = self.item, self.imageNode.frame.contains(location) { - return .openContextMenu(tapMessage: item.message, selectAll: false, subFrame: self.imageNode.frame) + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.message, selectAll: false, subFrame: self.imageNode.frame)) } case .hold: break @@ -1359,7 +1431,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return nil } - @objc func shareButtonPressed() { + @objc private func shareButtonPressed() { if let item = self.item { if case .pinnedMessages = item.associatedData.subject { item.controllerInteraction.navigateToMessageStandalone(item.content.firstMessage.id) @@ -1380,7 +1452,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { - item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) break } } @@ -1391,13 +1463,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } private var playedSwipeToReplyHaptic = false - @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { + @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 + var leftOffset: CGFloat = 0.0 var swipeOffset: CGFloat = 45.0 if let item = self.item, item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) { offset = -24.0 + leftOffset = -10.0 } else { offset = 10.0 + leftOffset = -10.0 swipeOffset = 60.0 } @@ -1425,7 +1500,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if translation.x < 0.0 { translation.x = max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) } else { - translation.x = 0.0 + if recognizer.allowBothDirections { + translation.x = -max(-180.0, min(0.0, -rubberBandingOffset(offset: abs(translation.x), bandingStart: swipeOffset))) + } else { + translation.x = 0.0 + } } if let item = self.item, self.swipeToReplyNode == nil { @@ -1443,7 +1522,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let swipeToReplyNode = self.swipeToReplyNode { swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) - swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + if translation.x < 0.0 { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: bounds.size.width + offset + 33.0 * 0.5, y: self.contentSize.height / 2.0) + } else { + swipeToReplyNode.bounds = CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.position = CGPoint(x: leftOffset - 33.0 * 0.5, y: self.contentSize.height / 2.0) + } if let (rect, containerSize) = self.absoluteRect { let mappedRect = CGRect(origin: CGPoint(x: rect.minX + swipeToReplyNode.frame.minX, y: rect.minY + swipeToReplyNode.frame.minY), size: swipeToReplyNode.frame.size) @@ -1462,7 +1547,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.swipeToReplyFeedback = nil let translation = recognizer.translation(in: self.view) - if case .ended = recognizer.state, translation.x < -swipeOffset { + let gestureRecognized: Bool + if recognizer.allowBothDirections { + gestureRecognized = abs(translation.x) > swipeOffset + } else { + gestureRecognized = translation.x < -swipeOffset + } + if case .ended = recognizer.state, gestureRecognized { if let item = self.item { if let currentSwipeAction = currentSwipeAction { switch currentSwipeAction { @@ -1494,7 +1585,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } @@ -1509,7 +1600,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return super.hitTest(point, with: event) } - override func updateSelectionState(animated: Bool) { + override public func updateSelectionState(animated: Bool) { guard let item = self.item else { return } @@ -1535,8 +1626,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let forwardInfoNode = self.forwardInfoNode { transition.updateAlpha(node: forwardInfoNode, alpha: panelsAlpha) } - if let replyBackgroundNode = self.replyBackgroundNode { - transition.updateAlpha(node: replyBackgroundNode, alpha: panelsAlpha) + if let replyBackgroundContent = self.replyBackgroundContent { + transition.updateAlpha(node: replyBackgroundContent, alpha: panelsAlpha) + } + if let forwardBackgroundContent = self.forwardBackgroundContent { + transition.updateAlpha(node: forwardBackgroundContent, alpha: panelsAlpha) } if let selectionState = item.controllerInteraction.selectionState { @@ -1579,10 +1673,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let alpha: CGFloat = 0.0 let previousAlpha = replyInfoNode.alpha replyInfoNode.alpha = alpha - self.replyBackgroundNode?.alpha = alpha + self.replyBackgroundContent?.alpha = alpha if animated { replyInfoNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) - self.replyBackgroundNode?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) + self.replyBackgroundContent?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) } } } else { @@ -1608,16 +1702,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let alpha: CGFloat = 1.0 let previousAlpha = replyInfoNode.alpha replyInfoNode.alpha = alpha - self.replyBackgroundNode?.alpha = alpha + self.replyBackgroundContent?.alpha = alpha if animated { replyInfoNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) - self.replyBackgroundNode?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) + self.replyBackgroundContent?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) } } } } - override func updateHighlightedState(animated: Bool) { + override public func updateHighlightedState(animated: Bool) { super.updateHighlightedState(animated: animated) if let item = self.item { @@ -1640,37 +1734,51 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func cancelInsertionAnimations() { + override public func cancelInsertionAnimations() { self.layer.removeAllAnimations() } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { + override public func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { return self.contextSourceNode } - override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + override public func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } + + public final class AnimationTransitionTextInput { + public let backgroundView: UIView + public let contentView: UIView + public let sourceRect: CGRect + public let scrollOffset: CGFloat + + public init(backgroundView: UIView, contentView: UIView, sourceRect: CGRect, scrollOffset: CGFloat) { + self.backgroundView = backgroundView + self.contentView = contentView + self.sourceRect = sourceRect + self.scrollOffset = scrollOffset + } + } - func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { + public func animateContentFromTextInputField(textInput: AnimationTransitionTextInput, transition: CombinedTransition) { guard let _ = self.item else { return } @@ -1722,8 +1830,56 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) } + + public final class AnimationTransitionSticker { + public let imageNode: TransformImageNode? + public let animationNode: ASDisplayNode? + public let placeholderNode: ASDisplayNode? + public let imageLayer: CALayer? + public let relativeSourceRect: CGRect + + var sourceFrame: CGRect { + if let imageNode = self.imageNode { + return imageNode.frame + } else if let imageLayer = self.imageLayer { + return imageLayer.bounds + } else { + return CGRect(origin: CGPoint(), size: relativeSourceRect.size) + } + } + + var sourceLayer: CALayer? { + if let imageNode = self.imageNode { + return imageNode.layer + } else if let imageLayer = self.imageLayer { + return imageLayer + } else { + return nil + } + } + + func snapshotContentTree() -> UIView? { + if let animationNode = self.animationNode { + return animationNode.view.snapshotContentTree() + } else if let imageNode = self.imageNode { + return imageNode.view.snapshotContentTree() + } else if let sourceLayer = self.imageLayer { + return sourceLayer.snapshotContentTreeAsView() + } else { + return nil + } + } + + public init(imageNode: TransformImageNode?, animationNode: ASDisplayNode?, placeholderNode: ASDisplayNode?, imageLayer: CALayer?, relativeSourceRect: CGRect) { + self.imageNode = imageNode + self.animationNode = animationNode + self.placeholderNode = placeholderNode + self.imageLayer = imageLayer + self.relativeSourceRect = relativeSourceRect + } + } - func animateContentFromStickerGridItem(stickerSource: ChatMessageTransitionNode.Sticker, transition: CombinedTransition) { + public func animateContentFromStickerGridItem(stickerSource: AnimationTransitionSticker, transition: CombinedTransition) { guard let _ = self.item else { return } @@ -1807,20 +1963,50 @@ class ChatMessageStickerItemNode: ChatMessageItemView { placeholderNode.layer.animateAlpha(from: 0.0, to: placeholderNode.alpha, duration: 0.4) } } + + public final class AnimationTransitionReplyPanel { + public let titleNode: ASDisplayNode + public let textNode: ASDisplayNode + public let lineNode: ASDisplayNode + public let imageNode: ASDisplayNode + public let relativeSourceRect: CGRect + public let relativeTargetRect: CGRect + + public init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) { + self.titleNode = titleNode + self.textNode = textNode + self.lineNode = lineNode + self.imageNode = imageNode + self.relativeSourceRect = relativeSourceRect + self.relativeTargetRect = relativeTargetRect + } + } - func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { + public func animateReplyPanel(sourceReplyPanel: AnimationTransitionReplyPanel, transition: CombinedTransition) { if let replyInfoNode = self.replyInfoNode { let localRect = self.contextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) + let mappedPanel = ChatMessageReplyInfoNode.TransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ) - let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, localRect: localRect, transition: transition) - if let replyBackgroundNode = self.replyBackgroundNode { - transition.animatePositionAdditive(layer: replyBackgroundNode.layer, offset: offset) - replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: mappedPanel, localRect: localRect, transition: transition) + if let replyBackgroundContent = self.replyBackgroundContent { + transition.animatePositionAdditive(layer: replyBackgroundContent.layer, offset: offset) + replyBackgroundContent.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + if let forwardBackgroundContent = self.forwardBackgroundContent { + transition.animatePositionAdditive(layer: forwardBackgroundContent.layer, offset: offset) + forwardBackgroundContent.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } } } - func animateFromLoadingPlaceholder(messageContainer: ChatLoadingPlaceholderMessageContainer, delay: Double, transition: ContainedViewLayoutTransition) { + public func animateFromLoadingPlaceholder(delay: Double, transition: ContainedViewLayoutTransition) { guard let item = self.item else { return } @@ -1830,14 +2016,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { transition.animateTransformScale(node: self, from: CGPoint(x: 0.85, y: 0.85), delay: delay) } - override func openMessageContextMenu() { + override public func openMessageContextMenu() { guard let item = self.item else { return } item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil, nil) } - override func targetForStoryTransition(id: StoryId) -> UIView? { + override public func targetForStoryTransition(id: StoryId) -> UIView? { guard let item = self.item else { return nil } @@ -1853,7 +2039,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return nil } - override func targetReactionView(value: MessageReaction.Reaction) -> UIView? { + override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result } @@ -1863,7 +2049,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return nil } - override func contentFrame() -> CGRect { + override public func contentFrame() -> CGRect { return self.imageNode.frame } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode/BUILD new file mode 100644 index 00000000000..a9aa212221f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageStoryMentionContentNode", + module_name = "ChatMessageStoryMentionContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/LocalizedPeerData", + "//submodules/TelegramStringFormatting", + "//submodules/WallpaperBackgroundNode", + "//submodules/ReactionSelectionNode", + "//submodules/PhotoResources", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramUniversalVideoContent", + "//submodules/GalleryUI", + "//submodules/Markdown", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", + "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode/Sources/ChatMessageStoryMentionContentNode.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode/Sources/ChatMessageStoryMentionContentNode.swift index b8fcbd37d07..da5c821dddb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode/Sources/ChatMessageStoryMentionContentNode.swift @@ -21,8 +21,10 @@ import Markdown import ComponentFlow import AvatarStoryIndicatorComponent import AvatarNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon -class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { +public class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let mediaBackgroundNode: NavigationBackgroundNode private let subtitleNode: TextNode @@ -36,7 +38,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { private let fetchDisposable = MetaDisposable() - required init() { + required public init() { self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) self.mediaBackgroundNode.clipsToBounds = true self.mediaBackgroundNode.cornerRadius = 24.0 @@ -84,7 +86,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -92,7 +94,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { self.fetchDisposable.dispose() } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId { return (self.imageNode, self.imageNode.bounds, { [weak self] in guard let strongSelf = self else { @@ -107,7 +109,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { } } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false var currentMedia: Media? if let item = item { @@ -137,7 +139,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { let _ = item.controllerInteraction.openMessage(item.message, .default) } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeImageLayout = self.imageNode.asyncLayout() let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) @@ -346,7 +348,7 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if let mediaBackgroundContent = self.mediaBackgroundContent { @@ -357,11 +359,11 @@ class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.mediaBackgroundNode.frame.contains(point) { - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode/BUILD new file mode 100644 index 00000000000..150eeba1ad0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageSwipeToReplyNode", + module_name = "ChatMessageSwipeToReplyNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/AppBundle", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode/Sources/ChatMessageSwipeToReplyNode.swift similarity index 95% rename from submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode/Sources/ChatMessageSwipeToReplyNode.swift index 97aac1c46e7..8a26af4a4a3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode/Sources/ChatMessageSwipeToReplyNode.swift @@ -8,8 +8,8 @@ import ChatControllerInteraction private let size = CGSize(width: 33.0, height: 33.0) -final class ChatMessageSwipeToReplyNode: ASDisplayNode { - enum Action { +public final class ChatMessageSwipeToReplyNode: ASDisplayNode { + public enum Action { case reply case like case unlike @@ -27,7 +27,7 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { private var absolutePosition: (CGRect, CGSize)? - init(fillColor: UIColor, enableBlur: Bool, foregroundColor: UIColor, backgroundNode: WallpaperBackgroundNode?, action: ChatMessageSwipeToReplyNode.Action) { + public init(fillColor: UIColor, enableBlur: Bool, foregroundColor: UIColor, backgroundNode: WallpaperBackgroundNode?, action: ChatMessageSwipeToReplyNode.Action) { self.backgroundNode = NavigationBackgroundNode(color: fillColor, enableBlur: enableBlur) self.backgroundNode.isUserInteractionEnabled = false @@ -138,7 +138,7 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { } } - override func didLoad() { + override public func didLoad() { super.didLoad() if let backgroundContent = self.backgroundContent { @@ -149,7 +149,7 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { } private var animatedWave = false - func updateProgress(_ progress: CGFloat) { + public func updateProgress(_ progress: CGFloat) { let progress = max(0.0, min(1.0, progress)) var foregroundProgress = min(1.0, progress * 1.2) var scaleProgress = 0.65 + foregroundProgress * 0.35 @@ -175,7 +175,7 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { } } - func playSuccessAnimation() { + public func playSuccessAnimation() { guard !self.animatedWave else { return } @@ -214,7 +214,7 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { self.fillLayer.animate(from: path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) } - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absolutePosition = (rect, containerSize) if let backgroundContent = self.backgroundContent { var backgroundFrame = backgroundContent.frame @@ -225,7 +225,7 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { } } -extension ChatMessageSwipeToReplyNode.Action { +public extension ChatMessageSwipeToReplyNode.Action { init(_ action: ChatControllerInteractionSwipeAction?) { if let action = action { switch action { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD new file mode 100644 index 00000000000..748b191a917 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD @@ -0,0 +1,44 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageTextBubbleContentNode", + module_name = "ChatMessageTextBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TextFormat", + "//submodules/UrlEscaping", + "//submodules/TelegramUniversalVideoContent", + "//submodules/TextSelectionNode", + "//submodules/InvisibleInkDustNode", + "//submodules/Emoji", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/YuvConversion", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/LottieAnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/MessageQuoteComponent", + "//submodules/TelegramUI/Components/TextLoadingEffect", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift similarity index 56% rename from submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f567ed93fe5..05d675a0aee 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -20,6 +20,12 @@ import LottieAnimationCache import MultiAnimationRenderer import EmojiTextAttachmentView import TextNodeWithEntities +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ShimmeringLinkNode +import ChatMessageItemCommon +import TextLoadingEffect +import ChatControllerInteraction private final class CachedChatMessageText { let text: String @@ -47,13 +53,14 @@ private final class CachedChatMessageText { } } -class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { + private let containerNode: ASDisplayNode private let textNode: TextNodeWithEntities private var spoilerTextNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode - private let statusNode: ChatMessageDateAndStatusNode + public var statusNode: ChatMessageDateAndStatusNode? private var linkHighlightingNode: LinkHighlightingNode? private var shimmeringNode: ShimmeringLinkNode? private var textSelectionNode: TextSelectionNode? @@ -62,7 +69,19 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var cachedChatMessageText: CachedChatMessageText? - override var visibility: ListViewItemNodeVisibility { + private var textSelectionState: Promise? + + private var linkPreviewHighlightText: String? + private var linkPreviewOptionsDisposable: Disposable? + private var linkPreviewHighlightingNodes: [LinkHighlightingNode] = [] + + private var quoteHighlightingNode: LinkHighlightingNode? + + private var linkProgressRange: NSRange? + private var linkProgressView: TextLoadingEffectView? + private var linkProgressDisposable: Disposable? + + override public var visibility: ListViewItemNodeVisibility { didSet { if oldValue != self.visibility { switch self.visibility { @@ -80,51 +99,42 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - required init() { - self.textNode = TextNodeWithEntities() + required public init() { + self.containerNode = ASDisplayNode() - self.statusNode = ChatMessageDateAndStatusNode() + self.textNode = TextNodeWithEntities() self.textAccessibilityOverlayNode = TextAccessibilityOverlayNode() super.init() + self.addSubnode(self.containerNode) + self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.contentMode = .topLeft self.textNode.textNode.contentsScale = UIScreenScale self.textNode.textNode.displaysAsynchronously = true - self.addSubnode(self.textNode.textNode) - self.addSubnode(self.textAccessibilityOverlayNode) + self.containerNode.addSubnode(self.textNode.textNode) + self.containerNode.addSubnode(self.textAccessibilityOverlayNode) self.textAccessibilityOverlayNode.openUrl = { [weak self] url in - self?.item?.controllerInteraction.openUrl(url, false, false, nil) - } - - self.statusNode.reactionSelected = { [weak self] value in - guard let strongSelf = self, let item = strongSelf.item else { - return - } - item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) - } - - self.statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in - guard let strongSelf = self, let item = strongSelf.item else { - gesture?.cancel() - return - } - - item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) + self?.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false)) } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + deinit { + self.linkPreviewOptionsDisposable?.dispose() + self.linkProgressDisposable?.dispose() + } + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode) - let statusLayout = self.statusNode.asyncLayout() + let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) let currentCachedChatMessageText = self.cachedChatMessageText @@ -132,10 +142,35 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + var topInset: CGFloat = 0.0 + var bottomInset: CGFloat = 0.0 + if case let .linear(top, bottom) = position { + switch top { + case .None: + topInset = layoutConstants.text.bubbleInsets.top + case let .Neighbour(_, topType, _): + switch topType { + case .text: + topInset = layoutConstants.text.bubbleInsets.top - 2.0 + case .header, .footer, .media, .reactions: + topInset = layoutConstants.text.bubbleInsets.top + } + default: + topInset = layoutConstants.text.bubbleInsets.top + } + + switch bottom { + case .None: + bottomInset = layoutConstants.text.bubbleInsets.bottom + default: + bottomInset = layoutConstants.text.bubbleInsets.bottom - 3.0 + } + } + let message = item.message let incoming: Bool - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { incoming = false } else { incoming = item.message.effectivelyIncoming(item.context.account.peerId) @@ -176,7 +211,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let dateFormat: MessageTimestampStatusFormat - if let subject = item.associatedData.subject, case .forwardedMessages = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject { dateFormat = .minimal } else { dateFormat = .regular @@ -271,7 +306,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { messageEntities = updatingMedia.entities?.entities ?? [] } - if let translateToLanguage = item.associatedData.translateToLanguage, !item.message.text.isEmpty && incoming { + if let subject = item.associatedData.subject, case .messageOptions = subject { + } else if let translateToLanguage = item.associatedData.translateToLanguage, !item.message.text.isEmpty && incoming { isTranslating = true for attribute in item.message.attributes { if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { @@ -339,7 +375,38 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let textFont = item.presentationData.messageFont if let entities = entities { - attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message) + var underlineLinks = true + if !messageTheme.primaryTextColor.isEqual(messageTheme.linkTextColor) { + underlineLinks = false + } + + let author = item.message.author + let mainColor: UIColor + var secondaryColor: UIColor? = nil + var tertiaryColor: UIColor? = nil + + let nameColors = author?.nameColor.flatMap { item.context.peerNameColors.get($0, dark: item.presentationData.theme.theme.overallDarkAppearance) } + if !incoming { + mainColor = messageTheme.accentTextColor + if let _ = nameColors?.secondary { + secondaryColor = .clear + } + if let _ = nameColors?.tertiary { + tertiaryColor = .clear + } + } else { + let authorNameColor = nameColors?.main + secondaryColor = nameColors?.secondary + tertiaryColor = nameColors?.tertiary + + if let authorNameColor { + mainColor = authorNameColor + } else { + mainColor = messageTheme.accentTextColor + } + } + + attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: mainColor, baseQuoteSecondaryTintColor: secondaryColor, baseQuoteTertiaryTintColor: tertiaryColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, underlineLinks: underlineLinks, message: item.message, adjustQuoteFontSize: true) } else if !rawText.isEmpty { attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor) } else { @@ -380,7 +447,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { spoilerTextLayoutAndApply = nil } - var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? if let statusType = statusType { var isReplyThread = false if case .replyThread = item.chatLocation { @@ -422,8 +489,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) - textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) - textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) + 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 { @@ -443,7 +510,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right - boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom + + boundingSize.height += topInset + bottomInset return (boundingSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { @@ -452,6 +520,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.cachedChatMessageText = updatedCachedChatMessageText } + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize) + let cachedLayout = strongSelf.textNode.textNode.cachedLayout if case .System = animation { @@ -463,7 +533,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { fadeNode.contents = textContents fadeNode.frame = strongSelf.textNode.textNode.frame fadeNode.isLayerBacked = true - strongSelf.addSubnode(fadeNode) + strongSelf.containerNode.addSubnode(fadeNode) fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in fadeNode?.removeFromSupernode() }) @@ -484,7 +554,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { spoilerTextNode.textNode.contentMode = .topLeft spoilerTextNode.textNode.contentsScale = UIScreenScale spoilerTextNode.textNode.displaysAsynchronously = false - strongSelf.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode) + strongSelf.containerNode.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode) strongSelf.spoilerTextNode = spoilerTextNode } @@ -497,7 +567,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) strongSelf.dustNode = dustNode - strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) + strongSelf.containerNode.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, textColor: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) @@ -536,27 +606,94 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateIsTranslating(isTranslating) - if let statusSizeAndApply = statusSizeAndApply { - animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil) - if strongSelf.statusNode.supernode == nil { - strongSelf.addSubnode(strongSelf.statusNode) - statusSizeAndApply.1(.None) + if let statusSizeAndApply { + let statusNode = statusSizeAndApply.1(strongSelf.statusNode == nil ? .None : animation) + let statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) + + if strongSelf.statusNode !== statusNode { + strongSelf.statusNode?.removeFromSupernode() + strongSelf.statusNode = statusNode + + strongSelf.addSubnode(statusNode) + + statusNode.reactionSelected = { [weak strongSelf] value in + guard let strongSelf, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) + } + statusNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in + guard let strongSelf, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) + } + statusNode.frame = statusFrame } else { - statusSizeAndApply.1(animation) + animation.animator.updateFrame(layer: statusNode.layer, frame: statusFrame, completion: nil) } - } else if strongSelf.statusNode.supernode != nil { - strongSelf.statusNode.removeFromSupernode() + } else if let statusNode = strongSelf.statusNode { + strongSelf.statusNode = nil + statusNode.removeFromSupernode() } - if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { - strongSelf.statusNode.pressed = { - guard let strongSelf = self else { + if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported), let statusNode = strongSelf.statusNode { + statusNode.pressed = { + guard let strongSelf = self, let statusNode = strongSelf.statusNode else { return } - item.controllerInteraction.displayImportedMessageTooltip(strongSelf.statusNode) + item.controllerInteraction.displayImportedMessageTooltip(statusNode) } } else { - strongSelf.statusNode.pressed = nil + strongSelf.statusNode?.pressed = nil + } + + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject { + if case let .reply(info) = info { + if strongSelf.textSelectionNode == nil { + strongSelf.updateIsExtractedToContextPreview(true) + if let initialQuote = info.quote, item.message.id == initialQuote.messageId { + let nsString = item.message.text as NSString + let subRange = nsString.range(of: initialQuote.text) + if subRange.location != NSNotFound { + strongSelf.beginTextSelection(range: subRange, displayMenu: true) + } + } + + if strongSelf.textSelectionState == nil { + if let textSelectionNode = strongSelf.textSelectionNode { + let range = textSelectionNode.getSelection() + strongSelf.textSelectionState = Promise(strongSelf.getSelectionState(range: range)) + } else { + strongSelf.textSelectionState = Promise(strongSelf.getSelectionState(range: nil)) + } + } + if let textSelectionState = strongSelf.textSelectionState { + info.selectionState.set(textSelectionState.get()) + } + } + } else if case let .link(link) = info { + if strongSelf.linkPreviewOptionsDisposable == nil { + strongSelf.linkPreviewOptionsDisposable = (link.options + |> deliverOnMainQueue).startStrict(next: { [weak strongSelf] options in + guard let strongSelf else { + return + } + + if options.hasAlternativeLinks { + strongSelf.linkPreviewHighlightText = options.url + strongSelf.updateLinkPreviewTextHighlightState(text: strongSelf.linkPreviewHighlightText) + } + }) + } + } + } + + strongSelf.updateLinkProgressState() + if let linkPreviewHighlightText = strongSelf.linkPreviewHighlightText { + strongSelf.updateLinkPreviewTextHighlightState(text: linkPreviewHighlightText) } } }) @@ -565,48 +702,116 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.statusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if case .tap = gesture { + } else { + if let item = self.item, let subject = item.associatedData.subject, case .messageOptions = subject { + return ChatMessageBubbleContentTapAction(content: .none) + } + } + let textNodeFrame = self.textNode.textNode.frame if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true - if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + var urlRange: NSRange? + if let (attributeText, fullText, urlRangeValue) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.URL, index: index) { + urlRange = urlRangeValue concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } - return .url(url: url, concealed: concealed) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)), activate: { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + + self.linkProgressDisposable?.dispose() + + if self.linkProgressRange != nil { + self.linkProgressRange = nil + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + let updatedRange: NSRange? = value ? urlRange : nil + if self.linkProgressRange != updatedRange { + self.linkProgressRange = updatedRange + self.updateLinkProgressState() + } + }) + + return promise + }) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .textMention(peerName) + var urlRange: NSRange? + if let (_, _, urlRangeValue) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.PeerTextMention, index: index) { + urlRange = urlRangeValue + } + + return ChatMessageBubbleContentTapAction(content: .textMention(peerName), activate: { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + + self.linkProgressDisposable?.dispose() + + if self.linkProgressRange != nil { + self.linkProgressRange = nil + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + let updatedRange: NSRange? = value ? urlRange : nil + if self.linkProgressRange != updatedRange { + self.linkProgressRange = updatedRange + self.updateLinkProgressState() + } + }) + + return promise + }) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return .botCommand(botCommand) + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) } else if let timecode = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode { - return .timecode(timecode.time, timecode.text) + return ChatMessageBubbleContentTapAction(content: .timecode(timecode.time, timecode.text)) } else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String { - return .bankCard(bankCard) + return ChatMessageBubbleContentTapAction(content: .bankCard(bankCard)) } else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String { - return .copy(pre) + return ChatMessageBubbleContentTapAction(content: .copy(pre)) + } else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String { + return ChatMessageBubbleContentTapAction(content: .copy(code)) } else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file { - return .customEmoji(file) + return ChatMessageBubbleContentTapAction(content: .customEmoji(file)) } else { if let item = self.item, item.message.text.count == 1, !item.presentationData.largeEmoji { let (emoji, fitz) = item.message.text.basicEmoji @@ -618,24 +823,24 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } if let emojiFile = emojiFile { - return .largeEmoji(emoji, fitz, emojiFile) + return ChatMessageBubbleContentTapAction(content: .largeEmoji(emoji, fitz, emojiFile)) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } } else { - if let _ = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: nil) { - return .ignore + if let statusNode = self.statusNode, let _ = statusNode.hitTest(self.view.convert(point, to: statusNode.view), with: nil) { + return ChatMessageBubbleContentTapAction(content: .ignore) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.statusNode.supernode != nil, let result = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: event) { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let statusNode = self.statusNode, statusNode.supernode != nil, let result = statusNode.hitTest(self.view.convert(point, to: statusNode.view), with: event) { return result } return super.hitTest(point, with: event) @@ -657,7 +862,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { shimmeringNode.updateLayout(self.textNode.textNode.frame.size) shimmeringNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.shimmeringNode = shimmeringNode - self.insertSubnode(shimmeringNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(shimmeringNode, belowSubnode: self.textNode.textNode) } } else if let shimmeringNode = self.shimmeringNode { self.shimmeringNode = nil @@ -667,7 +872,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { }) } } - override func updateTouchesAtPoint(_ point: CGPoint?) { + override public func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [CGRect]? var spoilerRects: [CGRect]? @@ -704,7 +909,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) @@ -717,29 +922,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - if let item = self.item { - let textNodeFrame = self.textNode.textNode.frame - if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let value = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - if let rects = self.textNode.textNode.attributeRects(name: TelegramTextAttributes.URL, at: index), !rects.isEmpty { - var rect = rects[0] - for i in 1 ..< rects.count { - rect = rect.union(rects[i]) - } - var concealed = true - if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: value, text: attributeText, fullText: fullText) - } - return (item.message, .url(self, rect, value, concealed)) - } - } - } - } - return nil - } - - override func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { + override public func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { guard let item = self.item else { return } @@ -752,12 +935,12 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { for i in 0 ..< rectsSet.count { let rects = rectsSet[i] let textHighlightNode: LinkHighlightingNode - if self.textHighlightingNodes.count < i { + if i < self.textHighlightingNodes.count { textHighlightNode = self.textHighlightingNodes[i] } else { textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.textHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.textHighlightColor) self.textHighlightingNodes.append(textHighlightNode) - self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) } textHighlightNode.frame = self.textNode.textNode.frame textHighlightNode.updateRects(rects) @@ -768,7 +951,171 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func willUpdateIsExtractedToContextPreview(_ value: Bool) { + private func updateLinkPreviewTextHighlightState(text: String?) { + guard let item = self.item else { + return + } + + var rectsSet: [[CGRect]] = [] + if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { + let nsString = string as NSString + let range = nsString.range(of: text) + if range.location != NSNotFound { + if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { + rectsSet = [rects] + } + } + } + for i in 0 ..< rectsSet.count { + let rects = rectsSet[i] + let textHighlightNode: LinkHighlightingNode + if i < self.linkPreviewHighlightingNodes.count { + textHighlightNode = self.linkPreviewHighlightingNodes[i] + } else { + textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor.withMultipliedAlpha(0.5) : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor.withMultipliedAlpha(0.5)) + self.linkPreviewHighlightingNodes.append(textHighlightNode) + self.containerNode.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) + } + textHighlightNode.frame = self.textNode.textNode.frame + textHighlightNode.updateRects(rects) + } + for i in (rectsSet.count ..< self.linkPreviewHighlightingNodes.count).reversed() { + self.linkPreviewHighlightingNodes[i].removeFromSupernode() + self.linkPreviewHighlightingNodes.remove(at: i) + } + } + + private func updateLinkProgressState() { + guard let item = self.item else { + return + } + + let range: NSRange = self.linkProgressRange ?? NSRange(location: NSNotFound, length: 0) + if range.location != NSNotFound { + let linkProgressView: TextLoadingEffectView + if let current = self.linkProgressView { + linkProgressView = current + } else { + linkProgressView = TextLoadingEffectView(frame: CGRect()) + self.linkProgressView = linkProgressView + self.containerNode.view.addSubview(linkProgressView) + } + linkProgressView.frame = self.textNode.textNode.frame + + let progressColor: UIColor = item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor + + linkProgressView.update(color: progressColor, textNode: self.textNode.textNode, range: range) + } else { + if let linkProgressView = self.linkProgressView { + self.linkProgressView = nil + linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in + linkProgressView?.removeFromSuperview() + }) + } + } + } + + public func animateQuoteTextHighlightIn(sourceFrame: CGRect, transition: ContainedViewLayoutTransition) -> CGRect? { + if let quoteHighlightingNode = self.quoteHighlightingNode { + var currentRect = CGRect() + for rect in quoteHighlightingNode.rects { + if currentRect.isEmpty { + currentRect = rect + } else { + currentRect = currentRect.union(rect) + } + } + if !currentRect.isEmpty { + currentRect = currentRect.insetBy(dx: -quoteHighlightingNode.inset, dy: -quoteHighlightingNode.inset) + let innerRect = currentRect.offsetBy(dx: quoteHighlightingNode.frame.minX, dy: quoteHighlightingNode.frame.minY) + + quoteHighlightingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.04) + + let fromScale = CGPoint(x: sourceFrame.width / innerRect.width, y: sourceFrame.height / innerRect.height) + + var fromTransform = CATransform3DIdentity + let fromOffset = CGPoint(x: sourceFrame.midX - innerRect.midX, y: sourceFrame.midY - innerRect.midY) + + fromTransform = CATransform3DTranslate(fromTransform, fromOffset.x, fromOffset.y, 0.0) + + fromTransform = CATransform3DTranslate(fromTransform, -quoteHighlightingNode.bounds.width * 0.5 + currentRect.midX, -quoteHighlightingNode.bounds.height * 0.5 + currentRect.midY, 0.0) + fromTransform = CATransform3DScale(fromTransform, fromScale.x, fromScale.y, 1.0) + fromTransform = CATransform3DTranslate(fromTransform, quoteHighlightingNode.bounds.width * 0.5 - currentRect.midX, quoteHighlightingNode.bounds.height * 0.5 - currentRect.midY, 0.0) + + quoteHighlightingNode.transform = fromTransform + transition.updateTransform(node: quoteHighlightingNode, transform: CGAffineTransformIdentity) + + return currentRect.offsetBy(dx: quoteHighlightingNode.frame.minX, dy: quoteHighlightingNode.frame.minY) + } + } + return nil + } + + public func getQuoteRect(quote: String) -> CGRect? { + var rectsSet: [CGRect] = [] + if !quote.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { + let nsString = string as NSString + let range = nsString.range(of: quote) + if range.location != NSNotFound { + if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { + rectsSet = rects + } + } + } + if !rectsSet.isEmpty { + var currentRect = CGRect() + for rect in rectsSet { + if currentRect.isEmpty { + currentRect = rect + } else { + currentRect = currentRect.union(rect) + } + } + + return currentRect.offsetBy(dx: self.textNode.textNode.frame.minX, dy: self.textNode.textNode.frame.minY) + } + + return nil + } + + public func updateQuoteTextHighlightState(text: String?, color: UIColor, animated: Bool) { + var rectsSet: [CGRect] = [] + if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { + let nsString = string as NSString + let range = nsString.range(of: text) + if range.location != NSNotFound { + if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { + rectsSet = rects + } + } + } + if !rectsSet.isEmpty { + let rects = rectsSet + let textHighlightNode: LinkHighlightingNode + if let current = self.quoteHighlightingNode { + textHighlightNode = current + } else { + textHighlightNode = LinkHighlightingNode(color: color) + self.quoteHighlightingNode = textHighlightNode + self.containerNode.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) + } + textHighlightNode.frame = self.textNode.textNode.frame + textHighlightNode.updateRects(rects) + } else { + if let quoteHighlightingNode = self.quoteHighlightingNode { + self.quoteHighlightingNode = nil + if animated { + quoteHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak quoteHighlightingNode] _ in + quoteHighlightingNode?.removeFromSupernode() + }) + } else { + quoteHighlightingNode.removeFromSupernode() + } + } + } + } + + override public func willUpdateIsExtractedToContextPreview(_ value: Bool) { if !value { if let textSelectionNode = self.textSelectionNode { self.textSelectionNode = nil @@ -781,9 +1128,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateIsExtractedToContextPreview(_ value: Bool) { + override public func updateIsExtractedToContextPreview(_ value: Bool) { if value { - if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() { + if self.textSelectionNode == nil, let item = self.item, let rootNode = item.controllerInteraction.chatControllerNode() { let selectionColor: UIColor let knobColor: UIColor if item.message.effectivelyIncoming(item.context.account.peerId) { @@ -794,20 +1141,31 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor } - let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor), strings: item.presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { [weak self] value in + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { [weak self] value in self?.updateIsTextSelectionActive?(value) }, present: { [weak self] c, a in - self?.item?.controllerInteraction.presentGlobalOverlayController(c, a) + guard let self, let item = self.item else { + return + } + + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .reply = info { + item.controllerInteraction.presentControllerInCurrent(c, a) + } else { + item.controllerInteraction.presentGlobalOverlayController(c, a) + } }, rootNode: { [weak rootNode] in return rootNode }, performAction: { [weak self] text, action in guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.performTextSelectionAction(true, text, action) + item.controllerInteraction.performTextSelectionAction(item.message, true, text, action) }) textSelectionNode.updateRange = { [weak self] selectionRange in - if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { + guard let strongSelf = self else { + return + } + if let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { for (spoilerRange, _) in textLayout.spoilers { if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { dustNode.update(revealed: true) @@ -815,10 +1173,39 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } } + if let textSelectionState = strongSelf.textSelectionState { + textSelectionState.set(.single(strongSelf.getSelectionState(range: selectionRange))) + } + } + + let enableCopy = !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected() + textSelectionNode.enableCopy = enableCopy + + var enableQuote = !item.message.text.isEmpty + var enableOtherActions = true + if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .reply = info { + enableOtherActions = false + } else if item.controllerInteraction.canSetupReply(item.message) == .reply { + //enableOtherActions = false + } + + if !item.controllerInteraction.canSendMessages() && !enableCopy { + enableQuote = false } + if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat { + enableQuote = false + } + if item.message.containsSecretMedia { + enableQuote = false + } + + textSelectionNode.enableQuote = enableQuote + textSelectionNode.enableTranslate = enableOtherActions + textSelectionNode.enableShare = enableOtherActions + textSelectionNode.menuSkipCoordnateConversion = !enableOtherActions self.textSelectionNode = textSelectionNode - self.addSubnode(textSelectionNode) - self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode) + self.containerNode.addSubnode(textSelectionNode) + self.containerNode.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode) textSelectionNode.frame = self.textNode.textNode.frame textSelectionNode.highlightAreaNode.frame = self.textNode.textNode.frame } @@ -839,19 +1226,19 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.statusNode.isHidden { - return self.statusNode.reactionView(value: value) + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + if let statusNode = self.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) } return nil } - override func getStatusNode() -> ASDisplayNode? { + override public func getStatusNode() -> ASDisplayNode? { return self.statusNode } - func animateFrom(sourceView: UIView, scrollOffset: CGFloat, widthDifference: CGFloat, transition: CombinedTransition) { - self.view.addSubview(sourceView) + public func animateFrom(sourceView: UIView, scrollOffset: CGFloat, widthDifference: CGFloat, transition: CombinedTransition) { + self.containerNode.view.addSubview(sourceView) sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak sourceView] _ in sourceView?.removeFromSuperview() @@ -866,7 +1253,73 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { transition.vertical.animatePositionAdditive(node: self.textNode.textNode, offset: offset) transition.updatePosition(layer: sourceView.layer, position: CGPoint(x: sourceView.layer.position.x - offset.x, y: sourceView.layer.position.y - offset.y)) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - transition.horizontal.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: -widthDifference, y: 0.0)) + if let statusNode = self.statusNode { + statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + transition.horizontal.animatePositionAdditive(node: statusNode, offset: CGPoint(x: -widthDifference, y: 0.0)) + } + } + + public func beginTextSelection(range: NSRange?, displayMenu: Bool = true) { + guard let textSelectionNode = self.textSelectionNode else { + return + } + guard let string = self.textNode.textNode.cachedLayout?.attributedString else { + return + } + let nsString = string.string as NSString + let range = range ?? NSRange(location: 0, length: nsString.length) + textSelectionNode.setSelection(range: range, displayMenu: displayMenu) + } + + public func cancelTextSelection() { + guard let textSelectionNode = self.textSelectionNode else { + return + } + textSelectionNode.cancelSelection() + } + + private func getSelectionState(range: NSRange?) -> ChatControllerSubject.MessageOptionsInfo.SelectionState { + var quote: ChatControllerSubject.MessageOptionsInfo.Quote? + if let item = self.item, let range, let selection = self.getCurrentTextSelection(customRange: range) { + quote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: item.message.id, text: selection.text) + } + return ChatControllerSubject.MessageOptionsInfo.SelectionState(canQuote: true, quote: quote) + } + + public func getCurrentTextSelection(customRange: NSRange? = nil) -> (text: String, entities: [MessageTextEntity])? { + guard let textSelectionNode = self.textSelectionNode else { + return nil + } + guard let range = customRange ?? textSelectionNode.getSelection() else { + return nil + } + guard let item = self.item else { + return nil + } + guard let string = self.textNode.textNode.cachedLayout?.attributedString else { + return nil + } + let nsString = string.string as NSString + let substring = nsString.substring(with: range) + + var entities: [MessageTextEntity] = [] + if let textEntitiesAttribute = item.message.textEntitiesAttribute { + entities = messageTextEntitiesInRange(entities: textEntitiesAttribute.entities, range: range, onlyQuoteable: true) + } + + return (substring, entities) + } + + public func animateClippingTransition(offset: CGFloat, animation: ListViewItemUpdateAnimation) { + self.containerNode.clipsToBounds = true + self.containerNode.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: self.containerNode.bounds.size) + self.containerNode.alpha = 0.0 + animation.animator.updateAlpha(layer: self.containerNode.layer, alpha: 1.0, completion: nil) + animation.animator.updateBounds(layer: self.containerNode.layer, bounds: CGRect(origin: CGPoint(), size: self.containerNode.bounds.size), completion: { [weak self] completed in + guard let self, completed else { + return + } + self.containerNode.clipsToBounds = false + }) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD new file mode 100644 index 00000000000..a870c334131 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageThreadInfoNode", + module_name = "ChatMessageThreadInfoNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift similarity index 96% rename from submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift index 4f610e79a92..1fab74cb24c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift @@ -172,25 +172,25 @@ private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, })) } -enum ChatMessageThreadInfoType { +public enum ChatMessageThreadInfoType { case bubble(incoming: Bool) case standalone } -class ChatMessageThreadInfoNode: ASDisplayNode { - class Arguments { - let presentationData: ChatPresentationData - let strings: PresentationStrings - let context: AccountContext - let controllerInteraction: ChatControllerInteraction - let type: ChatMessageThreadInfoType - let threadId: Int64 - let parentMessage: Message - let constrainedSize: CGSize - let animationCache: AnimationCache? - let animationRenderer: MultiAnimationRenderer? +public class ChatMessageThreadInfoNode: ASDisplayNode { + public class Arguments { + public let presentationData: ChatPresentationData + public let strings: PresentationStrings + public let context: AccountContext + public let controllerInteraction: ChatControllerInteraction + public let type: ChatMessageThreadInfoType + public let threadId: Int64 + public let parentMessage: Message + public let constrainedSize: CGSize + public let animationCache: AnimationCache? + public let animationRenderer: MultiAnimationRenderer? - init( + public init( presentationData: ChatPresentationData, strings: PresentationStrings, context: AccountContext, @@ -215,7 +215,7 @@ class ChatMessageThreadInfoNode: ASDisplayNode { } } - var visibility: Bool = false { + public var visibility: Bool = false { didSet { if self.visibility != oldValue { self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil @@ -249,7 +249,7 @@ class ChatMessageThreadInfoNode: ASDisplayNode { private var absolutePosition: (CGRect, CGSize)? - override init() { + override public init() { self.contentNode = HighlightTrackingButtonNode() self.contentBackgroundNode = ASImageNode() @@ -298,7 +298,7 @@ class ChatMessageThreadInfoNode: ASDisplayNode { self.pressed() } - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absolutePosition = (rect, containerSize) if let backgroundContent = self.backgroundContent { var backgroundFrame = backgroundContent.frame @@ -308,7 +308,7 @@ class ChatMessageThreadInfoNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageThreadInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode) { + public class func asyncLayout(_ maybeNode: ChatMessageThreadInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode) { let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) return { arguments in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/BUILD new file mode 100644 index 00000000000..bbe8d60e5f4 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageTransitionNode", + module_name = "ChatMessageTransitionNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift new file mode 100644 index 00000000000..eef65155476 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode/Sources/ChatMessageTransitionNode.swift @@ -0,0 +1,15 @@ +import Foundation +import UIKit +import ChatMessageItemView +import AsyncDisplayKit + +public protocol ChatMessageTransitionNodeDecorationItemNode: ASDisplayNode { + var contentView: UIView { get } +} + +public protocol ChatMessageTransitionNode: AnyObject { + typealias DecorationItemNode = ChatMessageTransitionNodeDecorationItemNode + + func add(decorationView: UIView, itemNode: ChatMessageItemView) -> DecorationItemNode + func remove(decorationNode: DecorationItemNode) +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD new file mode 100644 index 00000000000..87d31d072a9 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageUnsupportedBubbleContentNode", + module_name = "ChatMessageUnsupportedBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift similarity index 56% rename from submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift index 1a3d3650608..b154236175d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift @@ -6,11 +6,14 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageAttachedContentButtonNode -final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNode { private var buttonNode: ChatMessageAttachedContentButtonNode - required init() { + required public init() { self.buttonNode = ChatMessageAttachedContentButtonNode() super.init() @@ -23,11 +26,11 @@ final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNod } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) return { item, layoutConstants, _, _, constrainedSize, _ in @@ -39,32 +42,21 @@ final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNod let presentationData = item.presentationData let insets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 9.0, right: 12.0) - let buttonImage: UIImage - let buttonHighlightedImage: UIImage let titleColor: UIColor - let titleHighlightedColor: UIColor if incoming { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme.theme)! titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] } else { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme.theme)! titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] } - let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, presentationData.strings.Conversation_UpdateTelegram, titleColor, titleHighlightedColor, false) + let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, nil, false, presentationData.strings.Conversation_UpdateTelegram, titleColor, false, true) let initialWidth = buttonWidth + insets.left + insets.right return (initialWidth, { boundingWidth in - var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))? + var actionButtonSizeAndApply: ((CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))? let refinedButtonWidth = max(boundingWidth - insets.left - insets.right, buttonWidth) - let (size, apply) = continueActionButtonLayout(refinedButtonWidth) + let (size, apply) = continueActionButtonLayout(refinedButtonWidth, 33.0) actionButtonSizeAndApply = (size, apply) let adjustedBoundingSize = CGSize(width: refinedButtonWidth + insets.left + insets.right, height: insets.bottom + size.height) @@ -73,7 +65,7 @@ final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNod strongSelf.item = item if let (size, apply) = actionButtonSizeAndApply { - _ = apply() + _ = apply(animation) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: size) } } @@ -83,30 +75,30 @@ final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNod } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { if self.buttonNode.frame.contains(point) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/BUILD new file mode 100644 index 00000000000..b702b224245 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageWallpaperBubbleContentNode", + module_name = "ChatMessageWallpaperBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/LocalizedPeerData", + "//submodules/TelegramStringFormatting", + "//submodules/WallpaperBackgroundNode", + "//submodules/PhotoResources", + "//submodules/WallpaperResources", + "//submodules/Markdown", + "//submodules/RadialStatusNode", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageWallpaperBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatMessageWallpaperBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift index 28bf5655236..58ce6de289c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWallpaperBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift @@ -18,8 +18,11 @@ import Markdown import RadialStatusNode import ComponentFlow import AudioTranscriptionPendingIndicatorComponent +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import WallpaperPreviewMedia -class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let mediaBackgroundNode: NavigationBackgroundNode private let subtitleNode: TextNode @@ -38,7 +41,7 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { private let fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() - required init() { + required public init() { self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) self.mediaBackgroundNode.clipsToBounds = true self.mediaBackgroundNode.cornerRadius = 24.0 @@ -103,7 +106,7 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -112,7 +115,7 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { self.statusDisposable.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() if #available(iOS 13.0, *) { @@ -130,7 +133,7 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { item.context.account.pendingPeerMediaUploadManager.cancel(peerId: item.message.id.peerId) } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId { return (self.imageNode, self.imageNode.bounds, { [weak self] in guard let strongSelf = self else { @@ -145,7 +148,7 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false var currentMedia: Media? if let item = item { @@ -204,7 +207,7 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { } } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeImageLayout = self.imageNode.asyncLayout() let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) @@ -445,7 +448,7 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if let mediaBackgroundContent = self.mediaBackgroundContent { @@ -456,13 +459,13 @@ class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.statusOverlayNode.alpha > 0.0 { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } else if self.mediaBackgroundNode.frame.contains(point) { - return .openMessage + return ChatMessageBubbleContentTapAction(content: .openMessage) } else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD new file mode 100644 index 00000000000..61ab438674f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageWebpageBubbleContentNode", + module_name = "ChatMessageWebpageBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/WebsiteType", + "//submodules/InstantPageUI", + "//submodules/UrlHandling", + "//submodules/GalleryData", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + "//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift similarity index 70% rename from submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 7ec0339055b..34341548266 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -13,21 +13,27 @@ import InstantPageUI import UrlHandling import GalleryData import TelegramPresentationData +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import WallpaperPreviewMedia +import ChatMessageInteractiveMediaNode +import ChatMessageAttachedContentNode +import ChatControllerInteraction private let titleFont: UIFont = Font.semibold(15.0) -final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { +public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { private var webPage: TelegramMediaWebpage? - private let contentNode: ChatMessageAttachedContentNode + public private(set) var contentNode: ChatMessageAttachedContentNode - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { self.contentNode.visibility = self.visibility } } - required init() { + required public init() { self.contentNode = ChatMessageAttachedContentNode() super.init() @@ -36,7 +42,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { self.contentNode.openMedia = { [weak self] mode in if let strongSelf = self, let item = strongSelf.item { if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { - if let _ = content.image, let _ = content.instantPage { + if let _ = content.instantPage { if instantPageType(of: content) != .album { item.controllerInteraction.openInstantPage(item.message, item.associatedData) return @@ -47,6 +53,31 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } else if content.type == "telegram_theme" { item.controllerInteraction.openTheme(item.message) return + } else { + if content.embedUrl == nil && (content.title != nil || content.text != nil) && content.story == nil { + var shouldOpenUrl = true + if let file = content.file { + if !file.isVideo, !file.isVideoSticker, !file.isAnimated, !file.isAnimatedSticker, !file.isSticker, !file.isMusic { + shouldOpenUrl = false + } else if file.isMusic || file.isVoice { + shouldOpenUrl = false + } + } + + if shouldOpenUrl { + var isConcealed = true + if item.message.text.contains(content.url) { + isConcealed = false + } + if let attribute = item.message.webpagePreviewAttribute { + if attribute.isSafe { + isConcealed = false + } + } + item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: content.url, concealed: isConcealed, progress: strongSelf.contentNode.makeProgress())) + return + } + } } } let openChatMessageMode: ChatControllerInteractionOpenMessageMode @@ -58,7 +89,20 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { case .automaticPlayback: openChatMessageMode = .automaticPlayback } - let _ = item.controllerInteraction.openMessage(item.message, openChatMessageMode) + if !item.controllerInteraction.openMessage(item.message, openChatMessageMode) { + if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + var isConcealed = true + if item.message.text.contains(content.url) { + isConcealed = false + } + if let attribute = item.message.webpagePreviewAttribute { + if attribute.isSafe { + isConcealed = false + } + } + item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: content.url, concealed: isConcealed, progress: strongSelf.contentNode.makeProgress())) + } + } } } self.contentNode.activateAction = { [weak self] in @@ -78,8 +122,19 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let webpage = webPageContent { if webpage.story != nil { let _ = item.controllerInteraction.openMessage(item.message, .default) + } else if webpage.instantPage != nil { + strongSelf.contentNode.openMedia?(.default) } else { - item.controllerInteraction.openUrl(webpage.url, false, nil, nil) + var isConcealed = true + if item.message.text.contains(webpage.url) { + isConcealed = false + } + if let attribute = item.message.webpagePreviewAttribute { + if attribute.isSafe { + isConcealed = false + } + } + item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: webpage.url, concealed: isConcealed, progress: strongSelf.contentNode.makeProgress())) } } } @@ -90,14 +145,42 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) } } + self.contentNode.defaultContentAction = { [weak self] in + guard let self, let item = self.item, let webPage = self.webPage, case let .Loaded(content) = webPage.content else { + return ChatMessageBubbleContentTapAction(content: .none) + } + + if let file = content.file { + if !file.isVideo, !file.isVideoSticker, !file.isAnimated, !file.isAnimatedSticker, !file.isSticker, !file.isMusic { + return ChatMessageBubbleContentTapAction(content: .openMessage) + } + } + + var isConcealed = true + if item.message.text.contains(content.url) { + isConcealed = false + } + if let attribute = item.message.webpagePreviewAttribute { + if attribute.isSafe { + isConcealed = false + } + } + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: content.url, concealed: isConcealed, allowInlineWebpageResolution: true)), hasLongTapAction: false, activate: { [weak self] in + guard let self else { + return nil + } + return self.contentNode.makeProgress() + }) + } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - let contentNodeLayout = self.contentNode.asyncLayout() + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let currentWebpage = self.webPage + let currentContentNodeLayout = self.contentNode.asyncLayout() return { item, layoutConstants, preparePosition, _, constrainedSize, _ in var webPage: TelegramMediaWebpage? @@ -112,6 +195,16 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } + var updatedContentNode: ChatMessageAttachedContentNode? + let contentNodeLayout: ChatMessageAttachedContentNode.AsyncLayout + if currentWebpage == nil || currentWebpage?.webpageId == webPage?.id { + contentNodeLayout = currentContentNodeLayout + } else { + let updatedContentNodeValue = ChatMessageAttachedContentNode() + updatedContentNode = updatedContentNodeValue + contentNodeLayout = updatedContentNodeValue.asyncLayout() + } + var title: String? var subtitle: NSAttributedString? var text: String? @@ -345,6 +438,18 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { break } } + + if let webPageContent, let isMediaLargeByDefault = webPageContent.isMediaLargeByDefault, !isMediaLargeByDefault { + mediaAndFlags?.1.insert(.preferMediaInline) + } else if let attribute = item.message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { + if let forceLargeMedia = attribute.forceLargeMedia { + if forceLargeMedia { + mediaAndFlags?.1.remove(.preferMediaInline) + } else { + mediaAndFlags?.1.insert(.preferMediaInline) + } + } + } } else if let adAttribute = item.message.adAttribute { title = nil subtitle = nil @@ -395,13 +500,39 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { let (size, apply) = finalizeLayout(boundingWidth) return (size, { [weak self] animation, synchronousLoads, applyInfo in - if let strongSelf = self { - strongSelf.item = item - strongSelf.webPage = webPage + guard let self else { + return + } + self.item = item + self.webPage = webPage + + if let updatedContentNode { + let previousPosition = self.contentNode.position + let updatedPosition = CGPoint(x: size.width * 0.5, y: size.height * 0.5) - apply(animation, synchronousLoads, applyInfo) + do { + //animation.animator.updateScale(layer: self.contentNode.layer, scale: 0.9, completion: nil) + animation.animator.updatePosition(layer: self.contentNode.layer, position: updatedPosition, completion: nil) + animation.animator.updateAlpha(layer: self.contentNode.layer, alpha: 0.0, completion: { [weak contentNode] _ in + contentNode?.removeFromSupernode() + }) + } + + self.contentNode = updatedContentNode + self.addSubnode(updatedContentNode) - strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) + do { + apply(.None, synchronousLoads, applyInfo) + self.contentNode.frame = size.centered(around: previousPosition) + + //animation.animator.animateScale(layer: self.contentNode.layer, from: 0.9, to: 1.0, completion: nil) + self.contentNode.alpha = 0.0 + animation.animator.updateAlpha(layer: self.contentNode.layer, alpha: 1.0, completion: nil) + animation.animator.updatePosition(layer: self.contentNode.layer, position: updatedPosition, completion: nil) + } + } else { + self.contentNode.frame = CGRect(origin: CGPoint(), size: size) + apply(animation, synchronousLoads, applyInfo) } }) }) @@ -409,44 +540,44 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - 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.25, removeOnCompletion: false) } - override func animateInsertionIntoBubble(_ duration: Double) { + override public func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + override public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.contentNode.playMediaWithSound() } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { guard let item = self.item else { - return .none + return ChatMessageBubbleContentTapAction(content: .none) } if self.bounds.contains(point) { let contentNodeFrame = self.contentNode.frame let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating) if item.message.adAttribute != nil { - if case .none = result { + if case .none = result.content { if self.contentNode.hasActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } } return result } - switch result { + switch result.content { case .none: break case let .textMention(value): @@ -457,9 +588,9 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } switch websiteType(of: content.websiteName) { case .twitter: - return .url(url: "https://twitter.com/\(mention)", concealed: false) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://twitter.com/\(mention)", concealed: false))) case .instagram: - return .url(url: "https://instagram.com/\(mention)", concealed: false) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://instagram.com/\(mention)", concealed: false))) default: break } @@ -472,9 +603,9 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } switch websiteType(of: content.websiteName) { case .twitter: - return .url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false))) case .instagram: - return .url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false))) default: break } @@ -487,25 +618,25 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if content.instantPage != nil { switch websiteType(of: content.websiteName) { case .instagram, .twitter: - return .none + return ChatMessageBubbleContentTapAction(content: .none) default: - return .instantPage + return ChatMessageBubbleContentTapAction(content: .instantPage) } } else if content.type == "telegram_background" { - return .wallpaper + return ChatMessageBubbleContentTapAction(content: .wallpaper) } else if content.type == "telegram_theme" { - return .theme + return ChatMessageBubbleContentTapAction(content: .theme) } } if self.contentNode.hasActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) { - return .ignore + return ChatMessageBubbleContentTapAction(content: .ignore) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - return .none + return ChatMessageBubbleContentTapAction(content: .none) } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { if let media = media { var updatedMedia = media if let current = self.webPage, case let .Loaded(content) = current.content { @@ -537,7 +668,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } @@ -565,12 +696,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return nil } - override func updateTouchesAtPoint(_ point: CGPoint?) { + override public func updateTouchesAtPoint(_ point: CGPoint?) { let contentNodeFrame = self.contentNode.frame self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) }) } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { return self.contentNode.reactionTargetView(value: value) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatNavigationButton/BUILD b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/BUILD new file mode 100644 index 00000000000..7c7d0d30c1f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatNavigationButton", + module_name = "ChatNavigationButton", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift new file mode 100644 index 00000000000..3feb62f0d88 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift @@ -0,0 +1,27 @@ +import Foundation +import UIKit + +public enum ChatNavigationButtonAction: Equatable { + case openChatInfo(expandAvatar: Bool) + case clearHistory + case clearCache + case cancelMessageSelection + case search + case dismiss + case toggleInfoPanel + case spacer +} + +public struct ChatNavigationButton: Equatable { + public let action: ChatNavigationButtonAction + public let buttonItem: UIBarButtonItem + + public init(action: ChatNavigationButtonAction, buttonItem: UIBarButtonItem) { + self.action = action + self.buttonItem = buttonItem + } + + public static func ==(lhs: ChatNavigationButton, rhs: ChatNavigationButton) -> Bool { + return lhs.action == rhs.action && lhs.buttonItem === rhs.buttonItem + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD new file mode 100644 index 00000000000..f7164086390 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatOverscrollControl", + module_name = "ChatOverscrollControl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/ComponentFlow", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/AccountContext", + "//submodules/AvatarNode", + "//submodules/TextFormat", + "//submodules/Markdown", + "//submodules/WallpaperBackgroundNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift similarity index 98% rename from submodules/TelegramUI/Sources/ChatOverscrollControl.swift rename to submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift index 3d3ff123d88..d93210853ea 100644 --- a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift @@ -984,7 +984,7 @@ final class OverscrollContentsComponent: Component { } } -final class ChatOverscrollControl: CombinedComponent { +public final class ChatOverscrollControl: CombinedComponent { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? @@ -997,7 +997,7 @@ final class ChatOverscrollControl: CombinedComponent { let absoluteSize: CGSize let wallpaperNode: WallpaperBackgroundNode? - init( + public init( backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, @@ -1023,7 +1023,7 @@ final class ChatOverscrollControl: CombinedComponent { self.wallpaperNode = wallpaperNode } - static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool { + public static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool { if !lhs.backgroundColor.isEqual(rhs.backgroundColor) { return false } @@ -1060,7 +1060,7 @@ final class ChatOverscrollControl: CombinedComponent { return true } - static var body: Body { + public static var body: Body { let contents = Child(OverscrollContentsComponent.self) return { context in @@ -1093,12 +1093,12 @@ final class ChatOverscrollControl: CombinedComponent { } } -final class ChatInputPanelOverscrollNode: ASDisplayNode { - let text: (String, [(Int, NSRange)]) - let priority: Int +public final class ChatInputPanelOverscrollNode: ASDisplayNode { + public let text: (String, [(Int, NSRange)]) + public let priority: Int private let titleNode: ImmediateTextNode - init(text: (String, [(Int, NSRange)]), color: UIColor, priority: Int) { + public init(text: (String, [(Int, NSRange)]), color: UIColor, priority: Int) { self.text = text self.priority = priority self.titleNode = ImmediateTextNode() @@ -1113,7 +1113,7 @@ final class ChatInputPanelOverscrollNode: ASDisplayNode { self.addSubnode(self.titleNode) } - func update(size: CGSize) { + public func update(size: CGSize) { let titleSize = self.titleNode.updateLayout(size) self.titleNode.frame = titleSize.centered(in: CGRect(origin: CGPoint(), size: size)) } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD new file mode 100644 index 00000000000..afcda60459f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD @@ -0,0 +1,54 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatRecentActionsController", + module_name = "ChatRecentActionsController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AccountContext", + "//submodules/AlertUI", + "//submodules/AsyncDisplayKit", + "//submodules/BotPaymentsUI", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", + "//submodules/TelegramUI/Components/Chat/ChatNavigationButton", + "//submodules/TelegramUI/Components/Chat/ChatLoadingNode", + "//submodules/ChatPresentationInterfaceState", + "//submodules/ContextUI", + "//submodules/Display", + "//submodules/HashtagSearchUI", + "//submodules/InstantPageUI", + "//submodules/InviteLinksUI", + "//submodules/ItemListPeerItem", + "//submodules/ItemListUI", + "//submodules/JoinLinkPreviewUI", + "//submodules/LanguageLinkPreviewUI", + "//submodules/MergeLists", + "//submodules/OpenInExternalAppUI", + "//submodules/Pasteboard", + "//submodules/PeerInfoUI", + "//submodules/Postbox", + "//submodules/PresentationDataUtils", + "//submodules/SearchBarNode", + "//submodules/StickerPackPreviewUI", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramBaseController", + "//submodules/TelegramCallsUI", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TemporaryCachedPeerDataManager", + "//submodules/UndoUI", + "//submodules/WallpaperBackgroundNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift similarity index 96% rename from submodules/TelegramUI/Sources/ChatRecentActionsController.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 5f05832b3a4..bbe2be8fd04 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -10,8 +10,9 @@ import AccountContext import AlertUI import PresentationDataUtils import ChatPresentationInterfaceState +import ChatNavigationButton -final class ChatRecentActionsController: TelegramBaseController { +public final class ChatRecentActionsController: TelegramBaseController { private var controllerNode: ChatRecentActionsControllerNode { return self.displayNode as! ChatRecentActionsControllerNode } @@ -21,7 +22,7 @@ final class ChatRecentActionsController: TelegramBaseController { private let initialAdminPeerId: PeerId? private var presentationData: PresentationData private var presentationDataPromise = Promise() - override var updatedPresentationData: (PresentationData, Signal) { + override public var updatedPresentationData: (PresentationData, Signal) { return (self.presentationData, self.presentationDataPromise.get()) } private var presentationDataDisposable: Disposable? @@ -32,7 +33,7 @@ final class ChatRecentActionsController: TelegramBaseController { private let titleView: ChatRecentActionsTitleView - init(context: AccountContext, peer: Peer, adminPeerId: PeerId?) { + public init(context: AccountContext, peer: Peer, adminPeerId: PeerId?) { self.context = context self.peer = peer self.initialAdminPeerId = adminPeerId @@ -75,6 +76,8 @@ final class ChatRecentActionsController: TelegramBaseController { }, forwardMessages: { _ in }, updateForwardOptionsState: { _ in }, presentForwardOptions: { _ in + }, presentReplyOptions: { _ in + }, presentLinkOptions: { _ in }, shareSelectedMessages: { }, updateTextInputStateAndMode: { _ in }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in @@ -219,10 +222,14 @@ final class ChatRecentActionsController: TelegramBaseController { }) } - required init(coder aDecoder: NSCoder) { + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.presentationDataDisposable?.dispose() + } + private func updateThemeAndStrings() { self.titleView.color = self.presentationData.theme.rootController.navigationBar.primaryTextColor self.updateTitle() @@ -236,7 +243,7 @@ final class ChatRecentActionsController: TelegramBaseController { self.controllerNode.updatePresentationData(self.presentationData) } - override func loadDisplayNode() { + override public func loadDisplayNode() { self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, interaction: self.interaction, pushController: { [weak self] c in (self?.navigationController as? NavigationController)?.pushViewController(c) }, presentController: { [weak self] c, t, a in @@ -263,7 +270,7 @@ final class ChatRecentActionsController: TelegramBaseController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } - @objc func activateSearch() { + @objc private func activateSearch() { if let navigationBar = self.navigationBar { if !(navigationBar.contentNode is ChatRecentActionsSearchNavigationContentNode) { let searchNavigationNode = ChatRecentActionsSearchNavigationContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, cancel: { [weak self] in diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift similarity index 97% rename from submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index ae11529790c..fbeafdfb8e3 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -28,6 +28,8 @@ import ContextUI import Pasteboard import ChatControllerInteraction import ChatPresentationInterfaceState +import ChatMessageItemView +import ChatLoadingNode private final class ChatRecentActionsListOpaqueState { let entries: [ChatRecentActionsEntry] @@ -262,7 +264,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if peer.id != context.account.peerId { self?.openPeer(peer: peer) } - }, openPeerMention: { [weak self] name in + }, openPeerMention: { [weak self] name, _ in self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer @@ -272,17 +274,17 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url, _, _, _ in - self?.openUrl(url) + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in + self?.openUrl(url.url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { - openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) } }, openWallpaper: { [weak self] message in if let strongSelf = self{ - openChatWallpaper(context: strongSelf.context, message: message, present: { [weak self] c, a in + strongSelf.context.sharedContext.openChatWallpaper(context: strongSelf.context, message: message, present: { [weak self] c, a in self?.pushController(c) }) } @@ -294,6 +296,12 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { let resolveSignal: Signal if let peerName = peerName { resolveSignal = strongSelf.context.engine.peers.resolvePeerByName(name: peerName) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in return .single(peer?._asPeer()) } @@ -511,6 +519,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -526,7 +536,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in @@ -557,11 +567,14 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, activateAdAction: { _ in }, openRequestedPeerSelection: { _, _, _ in }, saveMediaToFiles: { _ in + }, openNoAdsDemo: { + }, displayGiveawayParticipationStatus: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction @@ -822,7 +835,12 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private func openPeerMention(_ name: String) { self.navigationActionDisposable.set((self.context.engine.peers.resolvePeerByName(name: name, ageLimit: 10) - |> take(1) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> deliverOnMainQueue).startStrict(next: { [weak self] peer in if let strongSelf = self { if let peer = peer { @@ -983,11 +1001,11 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case let .channelMessage(peer, messageId, timecode): if let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: true, timecode: timecode))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode))) } case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: .id(messageId), highlight: true, timecode: nil))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) } case let .replyThread(messageId): if let navigationController = strongSelf.getNavigationController() { @@ -1068,6 +1086,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .boost: break + case .premiumGiftCode: + break } } })) @@ -1184,3 +1204,17 @@ final class ChatRecentActionsMessageContextExtractedContentSource: ContextExtrac return result } } + +final class ChatMessageContextLocationContentSource: ContextLocationContentSource { + private let controller: ViewController + private let location: CGPoint + + init(controller: ViewController, location: CGPoint) { + self.controller = controller + self.location = location + } + + func transitionInfo() -> ContextControllerLocationViewInfo? { + return ContextControllerLocationViewInfo(location: self.location, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsEmptyNode.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatRecentActionsEmptyNode.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsEmptyNode.swift index 948ef2ff403..65a09f9d998 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsEmptyNode.swift @@ -13,7 +13,7 @@ import ChatPresentationInterfaceState private let titleFont = Font.medium(16.0) private let textFont = Font.regular(15.0) -final class ChatRecentActionsEmptyNode: ASDisplayNode { +public final class ChatRecentActionsEmptyNode: ASDisplayNode { private var theme: PresentationTheme private var chatWallpaper: TelegramWallpaper @@ -35,7 +35,7 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { private var title: String = "" private var text: String = "" - init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, chatBubbleCorners: PresentationChatBubbleCorners) { + public init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, chatBubbleCorners: PresentationChatBubbleCorners) { self.theme = theme self.chatWallpaper = chatWallpaper @@ -73,7 +73,7 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { } } - func updateLayout(presentationData: ChatPresentationData, backgroundNode: WallpaperBackgroundNode, size: CGSize, transition: ContainedViewLayoutTransition) { + public func updateLayout(presentationData: ChatPresentationData, backgroundNode: WallpaperBackgroundNode, size: CGSize, transition: ContainedViewLayoutTransition) { self.wallpaperBackgroundNode = backgroundNode self.layoutParams = (size, presentationData) @@ -150,14 +150,14 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { } // MARK: Nicegram Unblock - func setupButton(title: String?, handler: (() -> Void)?) { + public func setupButton(title: String?, handler: (() -> Void)?) { self.buttonNode.title = title self.buttonNode.isHidden = (title == nil) self.buttonNode.pressed = handler } // - func setup(title: String, text: String) { + public func setup(title: String, text: String) { if self.title != title || self.text != text { self.title = title self.text = text diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsFilterController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatRecentActionsFilterController.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift similarity index 82% rename from submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 71948a35e42..664f73eab7c 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -7,6 +7,8 @@ import TelegramPresentationData import MergeLists import AccountContext import ChatControllerInteraction +import ChatHistoryEntry +import ChatMessageItemImpl enum ChatRecentActionsEntryContentIndex: Int32 { case header = 0 @@ -67,7 +69,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) + 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) } return peer } @@ -115,7 +117,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.titleUpdated(title: new) 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 ChatMessageItem(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)) + 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 .changeAbout(prev, new): var peers = SimpleDictionary() var author: Peer? @@ -146,14 +148,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: let peers = SimpleDictionary() let attributes: [MessageAttribute] = [] let prevMessage = 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: prev, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) 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: new, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) + 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), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) } case let .changeUsername(prev, new): var peers = SimpleDictionary() @@ -184,7 +186,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] @@ -203,7 +205,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let prevMessage = Message(stableId: 0, 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: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) 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: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + 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), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) } case let .changeUsernames(prev, new): var peers = SimpleDictionary() @@ -234,7 +236,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] @@ -272,7 +274,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let prevMessage = Message(stableId: 0, 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: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) 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: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + 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), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) } case let .changePhoto(_, new): var peers = SimpleDictionary() @@ -291,7 +293,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.photoUpdated(image: photo) 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 ChatMessageItem(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)) + 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 .toggleInvites(value): var peers = SimpleDictionary() var author: Peer? @@ -318,7 +320,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } 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 ChatMessageItem(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)) + 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): var peers = SimpleDictionary() var author: Peer? @@ -345,7 +347,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } 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 ChatMessageItem(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)) + 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): switch self.id.contentIndex { case .header: @@ -376,7 +378,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: if let message = message { var peers = SimpleDictionary() @@ -394,7 +396,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: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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 { var peers = SimpleDictionary() var author: Peer? @@ -416,7 +418,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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: 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 ChatMessageItem(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)) + 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 .editMessage(prev, message): @@ -461,7 +463,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -478,7 +480,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: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) + 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: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) } case let .deleteMessage(message): switch self.id.contentIndex { @@ -504,7 +506,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -531,7 +533,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { peers[peer.id] = peer } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: message.id.id), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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 .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -549,7 +551,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { action = TelegramMediaActionType.removedMembers(peerIds: [self.entry.event.peerId]) } 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 ChatMessageItem(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)) + 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 .participantInvite(participant): var peers = SimpleDictionary() var author: Peer? @@ -566,7 +568,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action: TelegramMediaActionType action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) 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 ChatMessageItem(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)) + 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 .participantToggleBan(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -697,7 +699,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: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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 .participantToggleAdmin(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -934,7 +936,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: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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 .changeStickerPack(_, new): var peers = SimpleDictionary() var author: Peer? @@ -963,7 +965,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .togglePreHistoryHidden(value): var peers = SimpleDictionary() var author: Peer? @@ -993,7 +995,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .updateDefaultBannedRights(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1052,7 +1054,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: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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 .pollStopped(message): switch self.id.contentIndex { case .header: @@ -1080,7 +1082,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1097,7 +1099,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: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) + 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: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) } case let .linkedPeerUpdated(previous, updated): var peers = SimpleDictionary() @@ -1153,7 +1155,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .changeGeoLocation(_, updated): var peers = SimpleDictionary() var author: Peer? @@ -1175,12 +1177,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: 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: text, attributes: [], media: [mediaMap], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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 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 ChatMessageItem(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)) + 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 .updateSlowmode(_, newValue): var peers = SimpleDictionary() @@ -1211,7 +1213,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .startGroupCall, .endGroupCall: var peers = SimpleDictionary() var author: Peer? @@ -1248,7 +1250,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .groupCallUpdateParticipantMuteStatus(participantId, isMuted): var peers = SimpleDictionary() var author: Peer? @@ -1282,7 +1284,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .updateGroupCallSettings(joinMuted): var peers = SimpleDictionary() var author: Peer? @@ -1311,7 +1313,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .groupCallUpdateParticipantVolume(participantId, volume): var peers = SimpleDictionary() var author: Peer? @@ -1342,7 +1344,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .deleteExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1368,7 +1370,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .revokeExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1394,7 +1396,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .editExportedInvitation(_, updatedInvite): var peers = SimpleDictionary() var author: Peer? @@ -1420,7 +1422,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .participantJoinedViaInvite(invite, joinedViaFolderLink): var peers = SimpleDictionary() var author: Peer? @@ -1451,7 +1453,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .changeHistoryTTL(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1482,7 +1484,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .changeAvailableReactions(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1524,7 +1526,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .changeTheme(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1555,7 +1557,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .participantJoinByRequest(invite, approvedBy): var peers = SimpleDictionary() var author: Peer? @@ -1595,7 +1597,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 .toggleCopyProtection(value): var peers = SimpleDictionary() var author: Peer? @@ -1622,7 +1624,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } 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 ChatMessageItem(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)) + 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): switch self.id.contentIndex { case .header: @@ -1647,7 +1649,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) 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 ChatMessageItem(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)) + 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: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1664,7 +1666,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: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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 .createTopic(info): var peers = SimpleDictionary() @@ -1684,7 +1686,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItem(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)) + 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): var peers = SimpleDictionary() var author: Peer? @@ -1705,7 +1707,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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): var peers = SimpleDictionary() var author: Peer? @@ -1715,13 +1717,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } var text: String = "" var entities: [MessageTextEntity] = [] - /* - "Channel.AdminLog.TopicRenamed" = "%1$@ renamed topic %2$@ to %3$@"; - "Channel.AdminLog.TopicRenamedWithIcon" = "%1$@ renamed topic %2$@ to %3$@ and changed icon to %4$@"; - "Channel.AdminLog.TopicRenamedWithRemovedIcon" = "%1$@ renamed topic %2$@ to %3$@ and removed icon"; - "Channel.AdminLog.TopicChangedIcon" = "%1$@ changed topic %2$@ icon to %3$@"; - "Channel.AdminLog.TopicRemovedIcon" = "%1$@ removed topic %2$@ icon";*/ - + let authorTitle: String = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" if prevInfo.isHidden != newInfo.isHidden { appendAttributedText(text: newInfo.isHidden ? self.presentationData.strings.Channel_AdminLog_TopicHidden(authorTitle, newInfo.info.title) : self.presentationData.strings.Channel_AdminLog_TopicUnhidden(authorTitle, newInfo.info.title), generateEntities: { index in @@ -1784,7 +1780,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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): var peers = SimpleDictionary() var author: Peer? @@ -1822,7 +1818,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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): var peers = SimpleDictionary() var author: Peer? @@ -1843,7 +1839,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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): var peers = SimpleDictionary() var author: Peer? @@ -1864,7 +1860,71 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { 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 ChatMessageItem(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)) + 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 + } + + var text: String = "" + var entities: [MessageTextEntity] = [] + + let _ = updatedValue + + let rawText = self.presentationData.strings.Channel_AdminLog_MessageChangedNameColorSet(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)] + } else if index == 1 { + return [.Bold] + } + return [] + }, to: &text, entities: &entities) + + let action = TelegramMediaActionType.customText(text: text, entities: entities) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, 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 + } + + var text: String = "" + var entities: [MessageTextEntity] = [] + + 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 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)) } } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsInteraction.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsInteraction.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatRecentActionsInteraction.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsInteraction.swift diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsSearchNavigationContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsSearchNavigationContentNode.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatRecentActionsSearchNavigationContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsSearchNavigationContentNode.swift diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsTitleView.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsTitleView.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatRecentActionsTitleView.swift rename to submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsTitleView.swift diff --git a/submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer/BUILD b/submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer/BUILD new file mode 100644 index 00000000000..0627218e50b --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatSwipeToReplyRecognizer", + module_name = "ChatSwipeToReplyRecognizer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatSwipeToReplyRecognizer.swift b/submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer/Sources/ChatSwipeToReplyRecognizer.swift similarity index 62% rename from submodules/TelegramUI/Sources/ChatSwipeToReplyRecognizer.swift rename to submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer/Sources/ChatSwipeToReplyRecognizer.swift index db982376336..a3592134b20 100644 --- a/submodules/TelegramUI/Sources/ChatSwipeToReplyRecognizer.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer/Sources/ChatSwipeToReplyRecognizer.swift @@ -1,25 +1,26 @@ import Foundation import UIKit -class ChatSwipeToReplyRecognizer: UIPanGestureRecognizer { - var validatedGesture = false - var firstLocation: CGPoint = CGPoint() +public class ChatSwipeToReplyRecognizer: UIPanGestureRecognizer { + public var validatedGesture = false + public var firstLocation: CGPoint = CGPoint() + public var allowBothDirections: Bool = true - var shouldBegin: (() -> Bool)? + public var shouldBegin: (() -> Bool)? - override init(target: Any?, action: Selector?) { + override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) self.maximumNumberOfTouches = 1 } - override func reset() { + override public func reset() { super.reset() self.validatedGesture = false } - override func touchesBegan(_ touches: Set, with event: UIEvent) { + override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) if let shouldBegin = self.shouldBegin, !shouldBegin() { @@ -30,24 +31,24 @@ class ChatSwipeToReplyRecognizer: UIPanGestureRecognizer { } } - override func touchesMoved(_ touches: Set, with event: UIEvent) { + override public func touchesMoved(_ touches: Set, with event: UIEvent) { let location = touches.first!.location(in: self.view) let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y) let absTranslationX: CGFloat = abs(translation.x) let absTranslationY: CGFloat = abs(translation.y) - if !validatedGesture { - if translation.x > 0.0 { + if !self.validatedGesture { + if !self.allowBothDirections && translation.x > 0.0 { self.state = .failed } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { self.state = .failed } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { - validatedGesture = true + self.validatedGesture = true } } - if validatedGesture { + if self.validatedGesture { super.touchesMoved(touches, with: event) } } diff --git a/submodules/TelegramUI/Components/Chat/EditableTokenListNode/BUILD b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/BUILD new file mode 100644 index 00000000000..cb19c19e8ed --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EditableTokenListNode", + module_name = "EditableTokenListNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift similarity index 92% rename from submodules/TelegramUI/Sources/EditableTokenListNode.swift rename to submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift index 7e6ae6f971b..fd124ea0fa3 100644 --- a/submodules/TelegramUI/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift @@ -7,16 +7,23 @@ import TelegramPresentationData import AvatarNode import AccountContext -struct EditableTokenListToken { - enum Subject { +public struct EditableTokenListToken { + public enum Subject { case peer(EnginePeer) case category(UIImage?) } - let id: AnyHashable - let title: String - let fixedPosition: Int? - let subject: Subject + public let id: AnyHashable + public let title: String + public let fixedPosition: Int? + public let subject: Subject + + public init(id: AnyHashable, title: String, fixedPosition: Int?, subject: Subject) { + self.id = id + self.title = title + self.fixedPosition = fixedPosition + self.subject = subject + } } private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(rgb: 0x3350ee)) @@ -54,18 +61,18 @@ private func generateRemoveIcon(_ color: UIColor) -> UIImage? { }) } -final class EditableTokenListNodeTheme { - let backgroundColor: UIColor - let separatorColor: UIColor - let placeholderTextColor: UIColor - let primaryTextColor: UIColor - let tokenBackgroundColor: UIColor - let selectedTextColor: UIColor - let selectedBackgroundColor: UIColor - let accentColor: UIColor - let keyboardColor: PresentationThemeKeyboardColor +public final class EditableTokenListNodeTheme { + public let backgroundColor: UIColor + public let separatorColor: UIColor + public let placeholderTextColor: UIColor + public let primaryTextColor: UIColor + public let tokenBackgroundColor: UIColor + public let selectedTextColor: UIColor + public let selectedBackgroundColor: UIColor + public let accentColor: UIColor + public let keyboardColor: PresentationThemeKeyboardColor - init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { + public init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.placeholderTextColor = placeholderTextColor @@ -245,7 +252,7 @@ private final class CaretIndicatorNode: ASImageNode { } } -final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { +public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let context: AccountContext private let presentationTheme: PresentationTheme @@ -260,11 +267,11 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let caretIndicatorNode: CaretIndicatorNode private var selectedTokenId: AnyHashable? - var textUpdated: ((String) -> Void)? - var deleteToken: ((AnyHashable) -> Void)? - var textReturned: (() -> Void)? + public var textUpdated: ((String) -> Void)? + public var deleteToken: ((AnyHashable) -> Void)? + public var textReturned: (() -> Void)? - init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String) { + public init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String) { self.context = context self.presentationTheme = presentationTheme self.theme = theme @@ -326,7 +333,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + public func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let validTokens = Set(tokens.map { $0.id }) for i in (0 ..< self.tokenNodes.count).reversed() { @@ -466,7 +473,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { return nodeHeight } - @objc func textFieldChanged(_ textField: UITextField) { + @objc private func textFieldChanged(_ textField: UITextField) { let text = textField.text ?? "" self.placeholderNode.isHidden = !text.isEmpty self.updateSelectedTokenId(nil) @@ -476,24 +483,24 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { } } - func textFieldShouldReturn(_ textField: UITextField) -> Bool { + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { self.textReturned?() return false } - func textFieldDidBeginEditing(_ textField: UITextField) { + public func textFieldDidBeginEditing(_ textField: UITextField) { /*if self.caretIndicatorNode.supernode == self { self.caretIndicatorNode.removeFromSupernode() }*/ } - func textFieldDidEndEditing(_ textField: UITextField) { + public func textFieldDidEndEditing(_ textField: UITextField) { /*if self.caretIndicatorNode.supernode != self.scrollNode { self.scrollNode.addSubnode(self.caretIndicatorNode) }*/ } - func setText(_ text: String) { + public func setText(_ text: String) { self.textFieldNode.textField.text = text self.textFieldChanged(self.textFieldNode.textField) } diff --git a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift index f04add6746f..fb206e91ae7 100644 --- a/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode/Sources/ForwardAccessoryPanelNode.swift @@ -19,6 +19,7 @@ import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import AccessoryPanelNode +import AppBundle func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) { for media in message.media { @@ -91,7 +92,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { let closeButton: HighlightableButtonNode let lineNode: ASImageNode - let iconNode: ASImageNode + let iconView: UIImageView let titleNode: ImmediateTextNode let textNode: ImmediateTextNodeWithEntities private var originalText: NSAttributedString? @@ -127,10 +128,9 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.lineNode.displaysAsynchronously = false self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) - self.iconNode = ASImageNode() - self.iconNode.displayWithoutProcessing = false - self.iconNode.displaysAsynchronously = false - self.iconNode.image = PresentationResourcesChat.chatInputPanelForwardIconImage(theme) + self.iconView = UIImageView() + self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/ForwardSettingsIcon")?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 1 @@ -148,7 +148,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.closeButton) self.addSubnode(self.lineNode) - self.addSubnode(self.iconNode) + self.view.addSubview(self.iconView) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.actionArea) @@ -283,11 +283,11 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { } override public func animateIn() { - self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } override public func animateOut() { - self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + self.iconView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } override public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { @@ -302,7 +302,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) - self.iconNode.image = PresentationResourcesChat.chatInputPanelForwardIconImage(theme) + self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor let filteredMessages = self.messages @@ -350,8 +350,8 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) - if let icon = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) + if let icon = self.iconView.image { + self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) } let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) diff --git a/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/BUILD b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/BUILD new file mode 100644 index 00000000000..f36b5f7d006 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "InstantVideoRadialStatusNode", + module_name = "InstantVideoRadialStatusNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/LegacyComponents", + "//submodules/UIKitRuntimeUtils", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/InstantVideoRadialStatusNode.swift b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift similarity index 95% rename from submodules/TelegramUI/Sources/InstantVideoRadialStatusNode.swift rename to submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift index af53ab9b1dc..2d8a84006c6 100644 --- a/submodules/TelegramUI/Sources/InstantVideoRadialStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift @@ -40,7 +40,7 @@ private extension CGPoint { } } -final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDelegate { +public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDelegate { private let color: UIColor private let hasSeek: Bool private let hapticFeedback = HapticFeedback() @@ -88,7 +88,7 @@ final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDele private var statusDisposable: Disposable? private var statusValuePromise = Promise() - var duration: Double? { + public var duration: Double? { if let statusValue = self.statusValue { return statusValue.duration } else { @@ -96,7 +96,7 @@ final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDele } } - var status: Signal? { + public var status: Signal? { didSet { if let status = self.status { self.statusValuePromise.set(status |> map { $0 }) @@ -106,12 +106,12 @@ final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDele } } - var tapGestureRecognizer: UITapGestureRecognizer? - var panGestureRecognizer: UIPanGestureRecognizer? + public var tapGestureRecognizer: UITapGestureRecognizer? + public var panGestureRecognizer: UIPanGestureRecognizer? - var seekTo: ((Double, Bool) -> Void)? + public var seekTo: ((Double, Bool) -> Void)? - init(color: UIColor, hasSeek: Bool) { + public init(color: UIColor, hasSeek: Bool) { self.color = color self.hasSeek = hasSeek @@ -133,7 +133,7 @@ final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDele self.statusDisposable?.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() guard self.hasSeek else { @@ -149,11 +149,13 @@ final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDele self.view.addGestureRecognizer(panGestureRecognizer) } - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer === self.tapGestureRecognizer || gestureRecognizer === self.panGestureRecognizer { let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) let location = gestureRecognizer.location(in: self.view) - let distanceFromCenter = location.distanceTo(center) + + let distanceFromCenter = sqrt(pow(location.x - center.x, 2.0) + pow(location.y - center.y, 2.0)) + if distanceFromCenter < self.bounds.width * 0.2 { return false } @@ -256,11 +258,11 @@ final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDele } } - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress, dimProgress: self.effectiveDimProgress, playProgress: self.effectivePlayProgress, blinkProgress: self.effectiveBlinkProgress, hasSeek: self.hasSeek) } - @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc public override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if !isRasterizing { @@ -286,7 +288,7 @@ final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDele let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - let center = bounds.center + let center = CGPoint(x: bounds.midX, y: bounds.midY) context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: bounds.width / 2.0, options: .drawsAfterEndLocation) } } diff --git a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD new file mode 100644 index 00000000000..a06f08dcc1c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ManagedDiceAnimationNode", + module_name = "ManagedDiceAnimationNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/StickerResources", + "//submodules/ManagedAnimationNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift similarity index 93% rename from submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift rename to submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift index 5589c14e15a..d08dc54f6f3 100644 --- a/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift @@ -8,7 +8,7 @@ import AccountContext import StickerResources import ManagedAnimationNode -enum ManagedDiceAnimationState: Equatable { +public enum ManagedDiceAnimationState: Equatable { case rolling case value(Int32, Bool) } @@ -86,7 +86,7 @@ private struct InteractiveEmojiSuccessParameters { } public struct InteractiveEmojiConfiguration { - static var defaultValue: InteractiveEmojiConfiguration { + public static var defaultValue: InteractiveEmojiConfiguration { return InteractiveEmojiConfiguration(emojis: [], successParameters: [:]) } @@ -98,7 +98,7 @@ public struct InteractiveEmojiConfiguration { self.successParameters = successParameters } - static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration { + public static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration { if let data = appConfiguration.data, let emojis = data["emojies_send_dice"] as? [String] { var successParameters: [String: InteractiveEmojiSuccessParameters] = [:] if let success = data["emojies_send_dice_success"] as? [String: [String: Double]] { @@ -115,7 +115,7 @@ public struct InteractiveEmojiConfiguration { } } -final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStickerNode { +public final class ManagedDiceAnimationNode: ManagedAnimationNode { private let context: AccountContext private let emoji: String @@ -125,9 +125,9 @@ final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStick private let configuration = Promise() private let emojis = Promise<[TelegramMediaFile]>() - var success: (() -> Void)? + public var success: (() -> Void)? - init(context: AccountContext, emoji: String) { + public init(context: AccountContext, emoji: String) { self.context = context self.emoji = emoji @@ -157,7 +157,7 @@ final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStick self.disposable.dispose() } - func setState(_ diceState: ManagedDiceAnimationState) { + public func setState(_ diceState: ManagedDiceAnimationState) { let previousState = self.diceState self.diceState = diceState @@ -203,13 +203,13 @@ final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStick } } - func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) { + public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) { } - func setFrameIndex(_ frameIndex: Int) { + public func setFrameIndex(_ frameIndex: Int) { } - var currentFrameIndex: Int { + public var currentFrameIndex: Int { return 0 } } diff --git a/submodules/TelegramUI/Components/Chat/MessageHaptics/BUILD b/submodules/TelegramUI/Components/Chat/MessageHaptics/BUILD new file mode 100644 index 00000000000..499d48c1ab7 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MessageHaptics/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MessageHaptics", + module_name = "MessageHaptics", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/CoffinHaptic.swift b/submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/CoffinHaptic.swift similarity index 91% rename from submodules/TelegramUI/Sources/CoffinHaptic.swift rename to submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/CoffinHaptic.swift index b1e0713ec36..23132043d2f 100644 --- a/submodules/TelegramUI/Sources/CoffinHaptic.swift +++ b/submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/CoffinHaptic.swift @@ -5,11 +5,11 @@ import SwiftSignalKit private let firstImpactTime: Double = 0.4 private let secondImpactTime: Double = 0.6 -final class CoffinHaptic: EmojiHaptic { +public final class CoffinHaptic: EmojiHaptic { private var hapticFeedback = HapticFeedback() private var timer: SwiftSignalKit.Timer? private var time: Double = 0.0 - var enabled: Bool = false { + public var enabled: Bool = false { didSet { if !self.enabled { self.reset() @@ -17,9 +17,12 @@ final class CoffinHaptic: EmojiHaptic { } } - var active: Bool { + public var active: Bool { return self.timer != nil } + + public init() { + } private func reset() { if let timer = self.timer { @@ -36,7 +39,7 @@ final class CoffinHaptic: EmojiHaptic { } } - func start(time: Double) { + public func start(time: Double) { self.hapticFeedback.prepareImpact() if time > firstImpactTime { diff --git a/submodules/TelegramUI/Sources/HeartbeatHaptic.swift b/submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/HeartbeatHaptic.swift similarity index 92% rename from submodules/TelegramUI/Sources/HeartbeatHaptic.swift rename to submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/HeartbeatHaptic.swift index e70cc927886..f4a926e8aba 100644 --- a/submodules/TelegramUI/Sources/HeartbeatHaptic.swift +++ b/submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/HeartbeatHaptic.swift @@ -2,18 +2,18 @@ import Foundation import Display import SwiftSignalKit -protocol EmojiHaptic { +public protocol EmojiHaptic { var enabled: Bool { get set } var active: Bool { get } func start(time: Double) } -final class HeartbeatHaptic: EmojiHaptic { +public final class HeartbeatHaptic: EmojiHaptic { private var hapticFeedback = HapticFeedback() private var timer: SwiftSignalKit.Timer? private var time: Double = 0.0 - var enabled: Bool = false { + public var enabled: Bool = false { didSet { if !self.enabled { self.reset() @@ -21,9 +21,12 @@ final class HeartbeatHaptic: EmojiHaptic { } } - var active: Bool { + public var active: Bool { return self.timer != nil } + + public init() { + } private func reset() { if let timer = self.timer { @@ -42,7 +45,7 @@ final class HeartbeatHaptic: EmojiHaptic { } } - func start(time: Double) { + public func start(time: Double) { self.hapticFeedback.prepareImpact() if time > 2.0 { diff --git a/submodules/TelegramUI/Sources/PeachHaptic.swift b/submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/PeachHaptic.swift similarity index 91% rename from submodules/TelegramUI/Sources/PeachHaptic.swift rename to submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/PeachHaptic.swift index 10a6d757ab9..c68b34f57ed 100644 --- a/submodules/TelegramUI/Sources/PeachHaptic.swift +++ b/submodules/TelegramUI/Components/Chat/MessageHaptics/Sources/PeachHaptic.swift @@ -4,11 +4,11 @@ import SwiftSignalKit private let impactTime: Double = 0.6 -final class PeachHaptic: EmojiHaptic { +public final class PeachHaptic: EmojiHaptic { private var hapticFeedback = HapticFeedback() private var timer: SwiftSignalKit.Timer? private var time: Double = 0.0 - var enabled: Bool = false { + public var enabled: Bool = false { didSet { if !self.enabled { self.reset() @@ -16,9 +16,12 @@ final class PeachHaptic: EmojiHaptic { } } - var active: Bool { + public var active: Bool { return self.timer != nil } + + public init() { + } private func reset() { if let timer = self.timer { @@ -35,7 +38,7 @@ final class PeachHaptic: EmojiHaptic { } } - func start(time: Double) { + public func start(time: Double) { self.hapticFeedback.prepareImpact() if time > impactTime { diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD new file mode 100644 index 00000000000..beec5261216 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MessageInlineBlockBackgroundView", + module_name = "MessageInlineBlockBackgroundView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift new file mode 100644 index 00000000000..8b0ed49bf0a --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift @@ -0,0 +1,836 @@ +import Foundation +import UIKit +import Display +import HierarchyTrackingLayer +import Postbox +import TelegramCore +import AnimationCache +import MultiAnimationRenderer +import SwiftSignalKit +import AccountContext +import EmojiTextAttachmentView + +private let radius: CGFloat = 4.0 +private let lineWidth: CGFloat = 3.0 + +private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) { + context.saveGState() + context.translateBy(x: rect.minX, y: rect.minY) + context.scaleBy(x: radius, y: radius) + let fw = rect.width / radius + let fh = rect.height / radius + context.move(to: CGPoint(x: fw, y: fh / 2.0)) + context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1.0) + context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1) + context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1) + context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1) + context.closePath() + context.restoreGState() +} + +private func generateBackgroundTemplateImage(addStripe: Bool, isTransparent: Bool) -> UIImage { + return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size), radius: radius) + context.clip() + + context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) + if !isTransparent { + context.fill(CGRect(origin: CGPoint(), size: size)) + } + + if addStripe { + context.setFillColor(UIColor.white.withMultipliedAlpha(0.2).cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height))) + } + })!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate) +} + +private func generateProgressTemplateImage() -> UIImage { + return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size), radius: radius) + context.clip() + + context.setFillColor(UIColor.white.withMultipliedAlpha(0.4).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.white.withMultipliedAlpha(0.7).cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height))) + + context.resetClip() + + let borderWidth: CGFloat = 1.5 + addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size).insetBy(dx: borderWidth * 0.5, dy: borderWidth * 0.5), radius: radius) + context.setStrokeColor(UIColor.white.withAlphaComponent(0.7).cgColor) + context.strokePath() + + })!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate) +} + +private let backgroundSolidTemplateImage: UIImage = { + return generateBackgroundTemplateImage(addStripe: true, isTransparent: false) +}() + +private let backgroundDashTemplateImage: UIImage = { + return generateBackgroundTemplateImage(addStripe: false, isTransparent: false) +}() + +private let transparentBackgroundSolidTemplateImage: UIImage = { + return generateBackgroundTemplateImage(addStripe: true, isTransparent: true) +}() + +private let transparentBackgroundDashTemplateImage: UIImage = { + return generateBackgroundTemplateImage(addStripe: false, isTransparent: true) +}() + +private func generateDashBackgroundTemplateImage() -> UIImage { + return generateImage(CGSize(width: lineWidth, height: radius * 2.0 + 8.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: size.height)), cornerRadius: radius).cgPath) + context.clip() + + context.setFillColor(UIColor.white.withAlphaComponent(1.0).cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height))) + })!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate) +} + +private let dashBackgroundTemplateImage: UIImage = { + return generateDashBackgroundTemplateImage() +}() + +private func generateDashTemplateImage(isMonochrome: Bool, isTriple: Bool) -> UIImage { + return generateImage(CGSize(width: radius * 2.0, height: 18.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + + let dashOffset: CGFloat + if isTriple { + dashOffset = isMonochrome ? -7.0 : 5.0 + } else { + dashOffset = isMonochrome ? -4.0 : 5.0 + } + + let dashHeight: CGFloat = isTriple ? 6.0 : 9.0 + + context.translateBy(x: 0.0, y: dashOffset) + + for _ in 0 ..< 2 { + context.move(to: CGPoint(x: 0.0, y: 3.0)) + context.addLine(to: CGPoint(x: lineWidth, y: 0.0)) + context.addLine(to: CGPoint(x: lineWidth, y: dashHeight)) + context.addLine(to: CGPoint(x: 0.0, y: dashHeight + 3.0)) + context.closePath() + context.fillPath() + + context.translateBy(x: 0.0, y: size.height) + } + + context.clear(CGRect(origin: CGPoint(x: lineWidth, y: 0.0), size: CGSize(width: size.width - lineWidth, height: size.height))) + })!.resizableImage(withCapInsets: .zero, resizingMode: .tile).withRenderingMode(.alwaysTemplate) +} + +private let dashOpaqueTemplateImage: UIImage = { + return generateDashTemplateImage(isMonochrome: false, isTriple: false) +}() + +private let dashOpaqueTripleTemplateImage: UIImage = { + return generateDashTemplateImage(isMonochrome: false, isTriple: true) +}() + +private let dashMonochromeTemplateImage: UIImage = { + return generateDashTemplateImage(isMonochrome: true, isTriple: false) +}() + +private let dashMonochromeTripleTemplateImage: UIImage = { + return generateDashTemplateImage(isMonochrome: true, isTriple: true) +}() + +private func generateGradient(gradientWidth: CGFloat, baseAlpha: CGFloat) -> UIImage { + return generateImage(CGSize(width: gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) + + if let shadowImage = UIImage(named: "Stories/PanelGradient") { + UIGraphicsPushContext(context) + + for i in 0 ..< 2 { + let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) + + context.saveGState() + context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) + context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) + let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) + + context.clip(to: adjustedRect, mask: shadowImage.cgImage!) + context.setFillColor(foregroundColor.cgColor) + context.fill(adjustedRect) + + context.restoreGState() + } + + UIGraphicsPopContext() + } + })!.withRenderingMode(.alwaysTemplate) +} + +private final class PatternContentsTarget: MultiAnimationRenderTarget { + private let imageUpdated: () -> Void + + init(imageUpdated: @escaping () -> Void) { + self.imageUpdated = imageUpdated + + super.init() + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + override func transitionToContents(_ contents: AnyObject, didLoop: Bool) { + self.contents = contents + self.imageUpdated() + } +} + +private final class LineView: UIView { + private let backgroundView: UIImageView + private var dashBackgroundView: UIImageView? + private var dashThirdBackgroundView: UIImageView? + + private var params: Params? + private var isAnimating: Bool = false + + private struct Params: Equatable { + var size: CGSize + var primaryColor: UIColor + var secondaryColor: UIColor? + var thirdColor: UIColor? + var displayProgress: Bool + + init(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, thirdColor: UIColor?, displayProgress: Bool) { + self.size = size + self.primaryColor = primaryColor + self.secondaryColor = secondaryColor + self.thirdColor = thirdColor + self.displayProgress = displayProgress + } + } + + override init(frame: CGRect) { + self.backgroundView = UIImageView() + self.backgroundView.image = dashBackgroundTemplateImage + + super.init(frame: frame) + + self.layer.cornerRadius = radius + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .circular + } + + self.addSubview(self.backgroundView) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func updateAnimations() { + guard let params = self.params else { + return + } + + if params.displayProgress { + if let dashBackgroundView = self.dashBackgroundView { + if dashBackgroundView.layer.animation(forKey: "progress") == nil { + let animation = dashBackgroundView.layer.makeAnimation(from: 18.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = 1.0 + self.isAnimating = true + animation.completion = { [weak self] _ in + guard let self else { + return + } + self.isAnimating = false + self.updateAnimations() + } + dashBackgroundView.layer.add(animation, forKey: "progress") + } + if let dashThirdBackgroundView = self.dashThirdBackgroundView { + if dashThirdBackgroundView.layer.animation(forKey: "progress") == nil { + let animation = dashThirdBackgroundView.layer.makeAnimation(from: 18.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = 1.0 + self.isAnimating = true + animation.completion = { [weak self] _ in + guard let self else { + return + } + self.isAnimating = false + self.updateAnimations() + } + dashThirdBackgroundView.layer.add(animation, forKey: "progress") + } + } + } else { + let phaseDuration: Double = 1.0 + if self.backgroundView.layer.animation(forKey: "progress") == nil { + let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: -params.size.height as NSNumber, keyPath: "position.y", timingFunction: kCAMediaTimingFunctionSpring, duration: phaseDuration * 0.5, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: false, additive: true) + animation.repeatCount = 1.0 + self.isAnimating = true + animation.completion = { [weak self] _ in + guard let self else { + return + } + + let animation = self.backgroundView.layer.makeAnimation(from: params.size.height as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", timingFunction: kCAMediaTimingFunctionSpring, duration: phaseDuration * 0.5, delay: self.params?.displayProgress == true ? 0.1 : 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = 1.0 + self.isAnimating = true + animation.completion = { [weak self] _ in + guard let self else { + return + } + self.isAnimating = false + self.updateAnimations() + } + self.backgroundView.layer.add(animation, forKey: "progress") + } + self.backgroundView.layer.add(animation, forKey: "progress") + } + } + } + + if self.isAnimating && self.dashBackgroundView == nil { + self.backgroundView.backgroundColor = params.primaryColor + self.backgroundView.layer.masksToBounds = true + self.backgroundView.layer.cornerRadius = radius * 0.5 + } else { + self.backgroundView.backgroundColor = nil + self.backgroundView.layer.masksToBounds = false + } + + self.layer.masksToBounds = params.secondaryColor != nil || self.isAnimating + } + + func update(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, thirdColor: UIColor?, displayProgress: Bool, animation: ListViewItemUpdateAnimation) { + let params = Params( + size: size, + primaryColor: primaryColor, + secondaryColor: secondaryColor, + thirdColor: thirdColor, + displayProgress: displayProgress + ) + if self.params == params { + return + } + let previousParams = self.params + self.params = params + + let _ = previousParams + + + self.backgroundView.tintColor = primaryColor + + if let secondaryColor { + let dashBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: thirdColor != nil ? -12.0 : -18.0), size: CGSize(width: radius * 2.0, height: size.height + 18.0)) + + let dashBackgroundView: UIImageView + if let current = self.dashBackgroundView { + dashBackgroundView = current + + animation.animator.updateFrame(layer: dashBackgroundView.layer, frame: dashBackgroundFrame, completion: nil) + } else { + dashBackgroundView = UIImageView() + self.dashBackgroundView = dashBackgroundView + self.addSubview(dashBackgroundView) + + dashBackgroundView.frame = dashBackgroundFrame + } + + let templateImage: UIImage + let monochromeTemplateImage: UIImage + + if let thirdColor { + let thirdDashBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -18.0), size: CGSize(width: radius * 2.0, height: size.height + 18.0)) + templateImage = dashOpaqueTripleTemplateImage + monochromeTemplateImage = dashMonochromeTripleTemplateImage + + let dashThirdBackgroundView: UIImageView + if let current = self.dashThirdBackgroundView { + dashThirdBackgroundView = current + + animation.animator.updateFrame(layer: dashThirdBackgroundView.layer, frame: thirdDashBackgroundFrame, completion: nil) + } else { + dashThirdBackgroundView = UIImageView() + self.dashThirdBackgroundView = dashThirdBackgroundView + self.addSubview(dashThirdBackgroundView) + + dashThirdBackgroundView.frame = thirdDashBackgroundFrame + } + + if thirdColor.alpha == 0.0 { + dashThirdBackgroundView.alpha = 0.4 + dashThirdBackgroundView.image = monochromeTemplateImage + dashThirdBackgroundView.tintColor = primaryColor + } else { + dashThirdBackgroundView.alpha = 1.0 + dashThirdBackgroundView.image = templateImage + dashThirdBackgroundView.tintColor = thirdColor + } + } else { + templateImage = dashOpaqueTemplateImage + monochromeTemplateImage = dashMonochromeTemplateImage + if let dashThirdBackgroundView = self.dashThirdBackgroundView { + self.dashThirdBackgroundView = nil + dashThirdBackgroundView.removeFromSuperview() + } + } + + if secondaryColor.alpha == 0.0 { + self.backgroundView.alpha = 0.2 + dashBackgroundView.image = monochromeTemplateImage + dashBackgroundView.tintColor = primaryColor + } else { + self.backgroundView.alpha = 1.0 + dashBackgroundView.image = templateImage + dashBackgroundView.tintColor = secondaryColor + } + } else { + if let dashBackgroundView = self.dashBackgroundView { + self.dashBackgroundView = nil + dashBackgroundView.removeFromSuperview() + } + + self.backgroundView.alpha = 1.0 + } + + self.layer.masksToBounds = params.secondaryColor != nil || self.isAnimating + + animation.animator.updateFrame(layer: self.backgroundView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)), completion: nil) + + self.updateAnimations() + } +} + +public final class MessageInlineBlockBackgroundView: UIView { + public final class Pattern: Equatable { + public let context: AccountContext + public let fileId: Int64 + public let file: TelegramMediaFile? + + public init(context: AccountContext, fileId: Int64, file: TelegramMediaFile?) { + self.context = context + self.fileId = fileId + self.file = file + } + + public static func ==(lhs: Pattern, rhs: Pattern) -> Bool { + if lhs === rhs { + return true + } + if lhs.context !== rhs.context { + return false + } + if lhs.fileId != rhs.fileId { + return false + } + if lhs.file?.fileId != rhs.file?.fileId { + return false + } + + return true + } + } + + private struct Params: Equatable { + var size: CGSize + var isTransparent: Bool + var primaryColor: UIColor + var secondaryColor: UIColor? + var thirdColor: UIColor? + var pattern: Pattern? + var displayProgress: Bool + + init( + size: CGSize, + isTransparent: Bool, + primaryColor: UIColor, + secondaryColor: UIColor?, + thirdColor: UIColor?, + pattern: Pattern?, + displayProgress: Bool + ) { + self.size = size + self.isTransparent = isTransparent + self.primaryColor = primaryColor + self.secondaryColor = secondaryColor + self.thirdColor = thirdColor + self.pattern = pattern + self.displayProgress = displayProgress + } + } + + private var params: Params? + + public var displayProgress: Bool = false { + didSet { + if self.displayProgress != oldValue { + if let params = self.params { + self.update( + size: params.size, + isTransparent: params.isTransparent, + primaryColor: params.primaryColor, + secondaryColor: params.secondaryColor, + thirdColor: params.thirdColor, + pattern: params.pattern, + animation: .None + ) + } + } + } + } + + private let backgroundView: UIImageView + private var lineView: LineView + private var hierarchyTrackingLayer: HierarchyTrackingLayer? + + private var patternContentsTarget: PatternContentsTarget? + private var patternContentLayers: [SimpleLayer] = [] + private var patternFile: TelegramMediaFile? + private var patternFileDisposable: Disposable? + private var patternImage: UIImage? + private var patternImageDisposable: Disposable? + + private var progressBackgroundContentsView: UIImageView? + private var progressBackgroundMaskContainer: UIView? + private var progressBackgroundGradientView: UIImageView? + + override public init(frame: CGRect) { + self.backgroundView = UIImageView() + self.lineView = LineView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.addSubview(self.lineView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.patternFileDisposable?.dispose() + self.patternImageDisposable?.dispose() + } + + private func updateAnimations() { + guard let hierarchyTrackingLayer = self.hierarchyTrackingLayer, hierarchyTrackingLayer.isInHierarchy else { + return + } + guard let params = self.params else { + return + } + guard let progressBackgroundGradientView = self.progressBackgroundGradientView else { + return + } + let gradientWidth = progressBackgroundGradientView.bounds.width + + if progressBackgroundGradientView.layer.animation(forKey: "shimmer") != nil { + return + } + + let duration: Double = 1.0 + let animation = progressBackgroundGradientView.layer.makeAnimation(from: 0.0 as NSNumber, to: (params.size.width + gradientWidth + params.size.width * 0.1) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + progressBackgroundGradientView.layer.add(animation, forKey: "shimmer") + + self.lineView.updateAnimations() + } + + private func loadPatternFromFile() { + guard let pattern = self.params?.pattern else { + return + } + guard let patternContentsTarget = self.patternContentsTarget else { + return + } + guard let patternFile = self.patternFile else { + return + } + self.patternImageDisposable = pattern.context.animationRenderer.loadFirstFrame( + target: patternContentsTarget, + cache: pattern.context.animationCache, itemId: "reply-pattern-\(patternFile.fileId)", + size: CGSize(width: 64, height: 64), + fetch: animationCacheFetchFile( + postbox: pattern.context.account.postbox, + userLocation: .other, + userContentType: .sticker, + resource: .media(media: .standalone(media: patternFile), resource: patternFile.resource), + type: AnimationCacheAnimationType(file: patternFile), + keyframeOnly: false, + customColor: .white + ), + completion: { [weak self] _, _ in + guard let self else { + return + } + self.updatePatternLayerImages() + } + ) + } + + private func updatePatternLayerImages() { + let image = self.patternContentsTarget?.contents + for patternContentLayer in self.patternContentLayers { + patternContentLayer.contents = image + } + } + + public func update( + size: CGSize, + isTransparent: Bool, + primaryColor: UIColor, + secondaryColor: UIColor?, + thirdColor: UIColor?, + pattern: Pattern?, + animation: ListViewItemUpdateAnimation + ) { + let params = Params( + size: size, + isTransparent: isTransparent, + primaryColor: primaryColor, + secondaryColor: secondaryColor, + thirdColor: thirdColor, + pattern: pattern, + displayProgress: self.displayProgress + ) + if self.params == params { + return + } + let previousParams = self.params + self.params = params + + if previousParams?.primaryColor != params.primaryColor || previousParams?.secondaryColor != params.secondaryColor { + for patternContentLayer in self.patternContentLayers { + patternContentLayer.layerTintColor = primaryColor.cgColor + } + + if params.isTransparent { + if params.secondaryColor != nil { + self.backgroundView.image = transparentBackgroundDashTemplateImage + } else { + self.backgroundView.image = transparentBackgroundSolidTemplateImage + } + } else { + if params.secondaryColor != nil { + self.backgroundView.image = backgroundDashTemplateImage + } else { + self.backgroundView.image = backgroundSolidTemplateImage + } + } + self.backgroundView.tintColor = params.primaryColor + } + + if previousParams?.pattern != params.pattern { + if let pattern = params.pattern { + self.layer.masksToBounds = true + self.layer.cornerRadius = radius + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .circular + } + + if self.patternContentsTarget == nil { + self.patternContentsTarget = PatternContentsTarget(imageUpdated: { [weak self] in + guard let self else { + return + } + self.updatePatternLayerImages() + }) + } + + if previousParams?.pattern?.fileId != pattern.fileId { + self.patternFile = nil + self.patternFileDisposable?.dispose() + self.patternFileDisposable = nil + self.patternImageDisposable?.dispose() + + if let file = pattern.file { + self.patternFile = file + self.loadPatternFromFile() + } else { + let fileId = pattern.fileId + self.patternFileDisposable = (pattern.context.engine.stickers.resolveInlineStickers(fileIds: [pattern.fileId]) + |> deliverOnMainQueue).startStrict(next: { [weak self] files in + guard let self else { + return + } + if let file = files[fileId] { + self.patternFile = file + self.loadPatternFromFile() + } + }) + } + } + } else { + self.layer.masksToBounds = false + + self.patternContentsTarget = nil + self.patternFileDisposable?.dispose() + self.patternFileDisposable = nil + self.patternFile = nil + } + } + + animation.animator.updateFrame(layer: self.backgroundView.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil) + + let lineFrame = CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: size.height)) + self.lineView.update( + size: lineFrame.size, + primaryColor: params.primaryColor, + secondaryColor: params.secondaryColor, + thirdColor: params.thirdColor, + displayProgress: params.displayProgress, + animation: animation + ) + animation.animator.updateFrame(layer: lineView.layer, frame: lineFrame, completion: nil) + + if params.pattern != nil { + var maxIndex = 0 + + struct Placement { + var position: CGPoint + var size: CGFloat + + init(_ position: CGPoint, _ size: CGFloat) { + self.position = position + self.size = size + } + } + + let placements: [Placement] = [ + Placement(CGPoint(x: 180.0, y: 13.0), 38.0), + Placement(CGPoint(x: 55.0, y: 47.0), 58.0), + Placement(CGPoint(x: 364.0, y: 26.0), 58.0), + Placement(CGPoint(x: 133.0, y: 74.0), 46.0), + Placement(CGPoint(x: 262.0, y: 67.0), 54.0), + Placement(CGPoint(x: 62.0, y: 125.0), 44.0), + Placement(CGPoint(x: 171.0, y: 135.0), 47.0), + Placement(CGPoint(x: 320.0, y: 124.0), 47.0), + ] + + for placement in placements { + let patternContentLayer: SimpleLayer + if maxIndex < self.patternContentLayers.count { + patternContentLayer = self.patternContentLayers[maxIndex] + } else { + patternContentLayer = SimpleLayer() + patternContentLayer.layerTintColor = primaryColor.cgColor + self.layer.addSublayer(patternContentLayer) + self.patternContentLayers.append(patternContentLayer) + } + patternContentLayer.contents = self.patternContentsTarget?.contents + + 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) + var alphaFraction = abs(placement.position.x) / min(500.0, size.width) + alphaFraction = min(1.0, max(0.0, alphaFraction)) + patternContentLayer.opacity = 0.3 * Float(1.0 - alphaFraction) + + maxIndex += 1 + } + + if maxIndex < self.patternContentLayers.count { + for i in maxIndex ..< self.patternContentLayers.count { + self.patternContentLayers[i].removeFromSuperlayer() + } + self.patternContentLayers.removeSubrange(maxIndex ..< self.patternContentLayers.count) + } + } else { + for patternContentLayer in self.patternContentLayers { + patternContentLayer.removeFromSuperlayer() + } + self.patternContentLayers.removeAll() + } + + let gradientWidth: CGFloat = min(300.0, max(200.0, size.width * 0.9)) + + if previousParams?.displayProgress != params.displayProgress { + if params.displayProgress { + let progressBackgroundContentsView: UIImageView + if let current = self.progressBackgroundContentsView { + progressBackgroundContentsView = current + } else { + progressBackgroundContentsView = UIImageView() + progressBackgroundContentsView.image = generateProgressTemplateImage() + self.progressBackgroundContentsView = progressBackgroundContentsView + self.insertSubview(progressBackgroundContentsView, aboveSubview: self.backgroundView) + progressBackgroundContentsView.tintColor = primaryColor + } + + let progressBackgroundMaskContainer: UIView + if let current = self.progressBackgroundMaskContainer { + progressBackgroundMaskContainer = current + } else { + progressBackgroundMaskContainer = UIView() + self.progressBackgroundMaskContainer = progressBackgroundMaskContainer + progressBackgroundContentsView.mask = progressBackgroundMaskContainer + } + + let progressBackgroundGradientView: UIImageView + if let current = self.progressBackgroundGradientView { + progressBackgroundGradientView = current + } else { + progressBackgroundGradientView = UIImageView() + self.progressBackgroundGradientView = progressBackgroundGradientView + progressBackgroundMaskContainer.addSubview(progressBackgroundGradientView) + progressBackgroundGradientView.image = generateGradient(gradientWidth: 100.0, baseAlpha: 0.5) + } + + progressBackgroundContentsView.frame = CGRect(origin: CGPoint(), size: size) + progressBackgroundMaskContainer.frame = CGRect(origin: CGPoint(), size: size) + progressBackgroundGradientView.frame = CGRect(origin: CGPoint(x: -gradientWidth, y: 0.0), size: CGSize(width: gradientWidth, height: size.height)) + + if self.hierarchyTrackingLayer == nil { + let hierarchyTrackingLayer = HierarchyTrackingLayer() + self.hierarchyTrackingLayer = hierarchyTrackingLayer + self.layer.addSublayer(hierarchyTrackingLayer) + hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] _ in + self?.updateAnimations() + } + } + } else { + if let progressBackgroundContentsView = self.progressBackgroundContentsView { + self.progressBackgroundContentsView = nil + let transition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) + transition.updateAlpha(layer: progressBackgroundContentsView.layer, alpha: 0.0, completion: { [weak progressBackgroundContentsView] _ in + progressBackgroundContentsView?.removeFromSuperview() + }) + } + self.progressBackgroundMaskContainer = nil + self.progressBackgroundGradientView = nil + + if let hierarchyTrackingLayer = self.hierarchyTrackingLayer { + self.hierarchyTrackingLayer = nil + hierarchyTrackingLayer.isInHierarchyUpdated = nil + hierarchyTrackingLayer.removeFromSuperlayer() + } + } + } else { + if let progressBackgroundContentsView = self.progressBackgroundContentsView { + animation.animator.updateFrame(layer: progressBackgroundContentsView.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil) + progressBackgroundContentsView.tintColor = primaryColor + } + if let progressBackgroundMaskContainer = self.progressBackgroundMaskContainer { + animation.animator.updateFrame(layer: progressBackgroundMaskContainer.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil) + } + if let progressBackgroundGradientView = self.progressBackgroundGradientView { + animation.animator.updateFrame(layer: progressBackgroundGradientView.layer, frame: CGRect(origin: CGPoint(x: -gradientWidth, y: 0.0), size: CGSize(width: gradientWidth, height: size.height)), completion: nil) + } + } + + self.updateAnimations() + } +} diff --git a/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/BUILD b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/BUILD new file mode 100644 index 00000000000..0145b88d216 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MessageQuoteComponent", + module_name = "MessageQuoteComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/Sources/MessageQuoteComponent.swift b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/Sources/MessageQuoteComponent.swift new file mode 100644 index 00000000000..b459705bc63 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/Sources/MessageQuoteComponent.swift @@ -0,0 +1,67 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import TelegramCore + +private let lineImage: UIImage = { + let radius: CGFloat = 4.0 + return generateImage(CGSize(width: radius, height: radius * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + })!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) +}() + +public final class MessageQuoteView: UIView { + public struct Params { + let presentationData: ChatPresentationData + let authorName: String? + let text: String + let entities: [MessageTextEntity] + + public init( + presentationData: ChatPresentationData, + authorName: String?, + text: String, + entities: [MessageTextEntity] + ) { + self.presentationData = presentationData + self.authorName = authorName + self.text = text + self.entities = entities + } + } + + private let lineView: UIImageView + + override private init(frame: CGRect) { + self.lineView = UIImageView() + self.lineView.image = lineImage + + super.init(frame: frame) + + self.addSubview(self.lineView) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public static func asyncLayout(_ view: MessageQuoteView?) -> (Params) -> (CGSize, (CGSize) -> MessageQuoteView) { + return { params in + var minSize = CGSize() + + minSize.height = 100.0 + + return (minSize, { size in + let view = view ?? MessageQuoteView(frame: CGRect()) + + view.lineView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: lineImage.size.width, height: size.height)) + + return view + }) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/PollBubbleTimerNode/BUILD b/submodules/TelegramUI/Components/Chat/PollBubbleTimerNode/BUILD new file mode 100644 index 00000000000..1cc5965699e --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/PollBubbleTimerNode/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PollBubbleTimerNode", + module_name = "PollBubbleTimerNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift b/submodules/TelegramUI/Components/Chat/PollBubbleTimerNode/Sources/PollBubbleTimerNode.swift similarity index 97% rename from submodules/TelegramUI/Sources/PollBubbleTimerNode.swift rename to submodules/TelegramUI/Components/Chat/PollBubbleTimerNode/Sources/PollBubbleTimerNode.swift index 6d59a768646..646897984ac 100644 --- a/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift +++ b/submodules/TelegramUI/Components/Chat/PollBubbleTimerNode/Sources/PollBubbleTimerNode.swift @@ -39,7 +39,7 @@ private struct ContentParticle { } } -final class PollBubbleTimerNode: ASDisplayNode { +public final class PollBubbleTimerNode: ASDisplayNode { private struct Params: Equatable { var regularColor: UIColor var proximityColor: UIColor @@ -58,9 +58,9 @@ final class PollBubbleTimerNode: ASDisplayNode { private var currentParams: Params? - var reachedTimeout: (() -> Void)? + public var reachedTimeout: (() -> Void)? - override init() { + override public init() { var updateInHierarchy: ((Bool) -> Void)? self.hierarchyTrackingNode = HierarchyTrackingNode({ value in updateInHierarchy?(value) @@ -89,7 +89,7 @@ final class PollBubbleTimerNode: ASDisplayNode { self.animator?.invalidate() } - func update(regularColor: UIColor, proximityColor: UIColor, timeout: Int32, deadlineTimestamp: Int32?) { + public func update(regularColor: UIColor, proximityColor: UIColor, timeout: Int32, deadlineTimestamp: Int32?) { let params = Params( regularColor: regularColor, proximityColor: proximityColor, diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/BUILD new file mode 100644 index 00000000000..d027a20de9a --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ReplyAccessoryPanelNode", + module_name = "ReplyAccessoryPanelNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/Chat/AccessoryPanelNode", + "//submodules/TelegramUI/Components/CompositeTextNode", + "//submodules/TelegramNotices", + ], + visibility = [ + "//visibility:public", + ], +) + diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift similarity index 55% rename from submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift rename to submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index 725598e990f..61ef685756e 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -17,30 +17,39 @@ import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import AccessoryPanelNode +import TelegramNotices +import AppBundle +import CompositeTextNode -final class ReplyAccessoryPanelNode: AccessoryPanelNode { +public final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() - let messageId: MessageId + public let chatPeerId: EnginePeer.Id + public let messageId: MessageId + public let quote: EngineMessageReplyQuote? private var previousMediaReference: AnyMediaReference? - let closeButton: HighlightableButtonNode - let lineNode: ASImageNode - let iconNode: ASImageNode - let titleNode: ImmediateTextNode - let textNode: ImmediateTextNodeWithEntities - let imageNode: TransformImageNode + public let closeButton: HighlightableButtonNode + public let lineNode: ASImageNode + public let iconView: UIImageView + public let titleNode: CompositeTextNode + public let textNode: ImmediateTextNodeWithEntities + public let imageNode: TransformImageNode private let actionArea: AccessibilityAreaNode private let context: AccountContext - var theme: PresentationTheme - var strings: PresentationStrings + public var theme: PresentationTheme + public var strings: PresentationStrings + + private var textIsOptions: Bool = false private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? - init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { + public init(context: AccountContext, chatPeerId: EnginePeer.Id, messageId: MessageId, quote: EngineMessageReplyQuote?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { + self.chatPeerId = chatPeerId self.messageId = messageId + self.quote = quote self.context = context self.theme = theme @@ -57,15 +66,15 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.lineNode.displaysAsynchronously = false self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) - self.iconNode = ASImageNode() - self.iconNode.displayWithoutProcessing = false - self.iconNode.displaysAsynchronously = false - self.iconNode.image = PresentationResourcesChat.chatInputPanelReplyIconImage(theme) + self.iconView = UIImageView() + if quote != nil { + self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/ReplyQuoteIcon")?.withRenderingMode(.alwaysTemplate) + } else { + self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/ReplySettingsIcon")?.withRenderingMode(.alwaysTemplate) + } + self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor - self.titleNode = ImmediateTextNode() - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.displaysAsynchronously = false - self.titleNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) + self.titleNode = CompositeTextNode() self.textNode = ImmediateTextNodeWithEntities() self.textNode.maximumNumberOfLines = 1 @@ -96,7 +105,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.closeButton) self.addSubnode(self.lineNode) - self.addSubnode(self.iconNode) + self.view.addSubview(self.iconView) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.imageNode) @@ -107,7 +116,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { if let strongSelf = self { if messageView.message == nil { Queue.mainQueue().justDispatch { - strongSelf.interfaceInteraction?.setupReplyMessage(nil, { _ in }) + strongSelf.interfaceInteraction?.setupReplyMessage(nil, { _, _ in }) } return } @@ -142,7 +151,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { isMedia = false } - let textFont = Font.regular(14.0) + let textFont = Font.regular(15.0) let messageText: NSAttributedString if isText, let message = message { let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in @@ -227,17 +236,79 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { updateImageSignal = .single({ _ in return nil }) } } - - strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + + var titleText: [CompositeTextNode.Component] = [] + if let peer = message?.peers[strongSelf.messageId.peerId] as? TelegramChannel, case .broadcast = peer.info { + let icon: UIImage? + icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon"), color: theme.chat.inputPanel.panelControlAccentColor) + + if let icon { + let rawString: PresentationStrings.FormattedString + if strongSelf.quote != nil { + rawString = strongSelf.strings.Chat_ReplyPanel_ReplyToQuoteBy(peer.debugDisplayTitle) + } else { + rawString = strongSelf.strings.Chat_ReplyPanel_ReplyTo(peer.debugDisplayTitle) + } + if let nameRange = rawString.ranges.first { + titleText = [] + + let rawNsString = rawString.string as NSString + if nameRange.range.lowerBound != 0 { + titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: 0, length: nameRange.range.lowerBound)), font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))) + } + titleText.append(.icon(icon)) + titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))) + + if nameRange.range.upperBound != rawNsString.length { + titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: nameRange.range.upperBound, length: rawNsString.length - nameRange.range.upperBound)), font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))) + } + } else { + titleText.append(.text(NSAttributedString(string: rawString.string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))) + } + } + } else { + if let _ = strongSelf.quote { + let string = strongSelf.strings.Chat_ReplyPanel_ReplyToQuoteBy(authorName).string + titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))] + } else { + let string = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string + titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))] + } + + if strongSelf.messageId.peerId != strongSelf.chatPeerId { + if let peer = message?.peers[strongSelf.messageId.peerId], (peer is TelegramChannel || peer is TelegramGroup) { + let icon: UIImage? + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon") + } else { + icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon") + } + if let iconImage = generateTintedImage(image: icon, color: strongSelf.theme.chat.inputPanel.panelControlAccentColor) { + titleText.append(.icon(iconImage)) + titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))) + } + } + } + } + strongSelf.textNode.attributedText = messageText + + if let quote = strongSelf.quote { + let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor + let quoteText = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + + strongSelf.textNode.attributedText = quoteText + } + + strongSelf.titleNode.components = titleText let headerString: String if let message = message, message.flags.contains(.Incoming), let author = message.author { - headerString = "Reply to message. From: \(EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder))" + headerString = strongSelf.strings.Chat_ReplyPanel_AccessibilityReplyToMessageFrom(EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string } else if let message = message, !message.flags.contains(.Incoming) { - headerString = "Reply to your message" + headerString = strongSelf.strings.Chat_ReplyPanel_AccessibilityReplyToYourMessage } else { - headerString = "Reply to message" + headerString = strongSelf.strings.Chat_ReplyPanel_AccessibilityReplyToMessage } strongSelf.actionArea.accessibilityLabel = "\(headerString).\n\(text)" @@ -255,6 +326,38 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { if let (size, inset, interfaceState) = strongSelf.validLayout { strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState) } + + let _ = (ApplicationSpecificNotice.getChatReplyOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count in + if let strongSelf = self, count < 3 { + Queue.mainQueue().after(3.0) { + if let snapshotView = strongSelf.textNode.view.snapshotContentTree() { + let text: String + if let (size, _, _) = strongSelf.validLayout, size.width > 320.0 { + text = strongSelf.strings.Chat_ReplyPanel_HintReplyOptions + } else { + text = strongSelf.strings.Chat_ReplyPanel_HintReplyOptionsShort + } + strongSelf.textIsOptions = true + + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) + + strongSelf.view.addSubview(snapshotView) + + if let (size, inset, interfaceState) = strongSelf.validLayout { + strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState) + } + + strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + let _ = ApplicationSpecificNotice.incrementChatReplyOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).start() + } + } + }) } })) } @@ -263,35 +366,52 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.messageDisposable.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - override func animateIn() { - self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + override public func animateIn() { + self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } - override func animateOut() { - self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + override public func animateOut() { + self.iconView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } - override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - if self.theme !== theme { + override public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.updateThemeAndStrings(theme: theme, strings: strings, force: false) + } + + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, force: Bool) { + if self.theme !== theme || force { self.theme = theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) - self.iconNode.image = PresentationResourcesChat.chatInputPanelReplyIconImage(theme) + self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor - if let text = self.titleNode.attributedText?.string { - self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + self.titleNode.components = self.titleNode.components.map { item in + switch item { + case let .text(text): + let updatedText = NSMutableAttributedString(attributedString: text) + updatedText.addAttribute(.foregroundColor, value: theme.chat.inputPanel.panelControlAccentColor, range: NSRange(location: 0, length: updatedText.length)) + return .text(updatedText) + case let .icon(icon): + if let iconImage = generateTintedImage(image: icon, color: theme.chat.inputPanel.panelControlAccentColor) { + return .icon(iconImage) + } else { + return .icon(icon) + } + } } - if let text = self.textNode.attributedText?.string { - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + if let text = self.textNode.attributedText { + let updatedText = NSMutableAttributedString(attributedString: text) + updatedText.addAttribute(.foregroundColor, value: self.textIsOptions ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor, range: NSRange(location: 0, length: updatedText.length)) + self.textNode.attributedText = updatedText } self.textNode.spoilerColor = self.theme.chat.inputPanel.secondaryTextColor @@ -301,11 +421,11 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } - override func updateState(size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState) { + override public func updateState(size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState) { self.validLayout = (size, inset, interfaceState) let bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 45.0)) @@ -324,8 +444,8 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) } - if let icon = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) + if let icon = self.iconView.image { + self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) } var imageTextInset: CGFloat = 0.0 @@ -336,9 +456,9 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0)) } - let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) + let titleSize = self.titleNode.update(constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) if self.titleNode.supernode == self { - self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.titleNode.insets.left, y: 7.0 - self.titleNode.insets.top), size: titleSize) + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 7.0), size: titleSize) } let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) @@ -348,15 +468,32 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } } - @objc func closePressed() { + @objc private func closePressed() { if let dismiss = self.dismiss { dismiss() } } - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + private var previousTapTimestamp: Double? + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, .generic) + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() + self.interfaceInteraction?.presentReplyOptions(self) + Queue.mainQueue().after(1.5) { + self.updateThemeAndStrings(theme: self.theme, strings: self.strings, force: true) + } + + let _ = ApplicationSpecificNotice.incrementChatReplyOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start() } } + + /*@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, .generic) + } + }*/ } diff --git a/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/BUILD b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/BUILD new file mode 100644 index 00000000000..bdff7f82114 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ShimmeringLinkNode", + module_name = "ShimmeringLinkNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ShimmerEffect", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ShimmeringLinkNode.swift b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/Sources/ShimmeringLinkNode.swift similarity index 89% rename from submodules/TelegramUI/Sources/ShimmeringLinkNode.swift rename to submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/Sources/ShimmeringLinkNode.swift index 790633cf21a..c70a1c79538 100644 --- a/submodules/TelegramUI/Sources/ShimmeringLinkNode.swift +++ b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/Sources/ShimmeringLinkNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit import Display import ShimmerEffect -final class ShimmeringLinkNode: ASDisplayNode { +public final class ShimmeringLinkNode: ASDisplayNode { private let shimmerEffectNode: ShimmerEffectForegroundNode private let borderShimmerEffectNode: ShimmerEffectForegroundNode @@ -12,17 +12,17 @@ final class ShimmeringLinkNode: ASDisplayNode { private let borderMaskNode: ASImageNode private(set) var rects: [CGRect] = [] - var color: UIColor { + public var color: UIColor { didSet { self.backgroundColor = color } } - var innerRadius: CGFloat = 4.0 - var outerRadius: CGFloat = 4.0 - var inset: CGFloat = 2.0 + public var innerRadius: CGFloat = 4.0 + public var outerRadius: CGFloat = 4.0 + public var inset: CGFloat = 2.0 - init(color: UIColor) { + public init(color: UIColor) { self.color = color self.shimmerEffectNode = ShimmerEffectForegroundNode() @@ -46,14 +46,14 @@ final class ShimmeringLinkNode: ASDisplayNode { //self.addSubnode(self.borderShimmerEffectNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.shimmerEffectNode.layer.mask = self.maskNode.layer self.borderShimmerEffectNode.layer.mask = self.borderMaskNode.layer } - func updateRects(_ rects: [CGRect], color: UIColor? = nil) { + public func updateRects(_ rects: [CGRect], color: UIColor? = nil) { var updated = false if self.rects != rects { updated = true @@ -86,7 +86,7 @@ final class ShimmeringLinkNode: ASDisplayNode { } } - func updateLayout(_ size: CGSize) { + public func updateLayout(_ size: CGSize) { self.shimmerEffectNode.frame = CGRect(origin: .zero, size: size) self.borderShimmerEffectNode.frame = CGRect(origin: .zero, size: size) diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 08f58b227b8..97048e2f23e 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -20,13 +20,11 @@ import MultiAnimationRenderer public struct ChatInterfaceHighlightedState: Equatable { public let messageStableId: UInt32 + public let quote: String? - public init(messageStableId: UInt32) { + public init(messageStableId: UInt32, quote: String?) { self.messageStableId = messageStableId - } - - public static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool { - return lhs.messageStableId == rhs.messageStableId + self.quote = quote } } @@ -75,6 +73,18 @@ public protocol ChatMessageTransitionProtocol: ASDisplayNode { } +public struct NavigateToMessageParams { + public var timestamp: Double? + public var quote: String? + public var progress: Promise? + + public init(timestamp: Double?, quote: String?, progress: Promise? = nil) { + self.timestamp = timestamp + self.quote = quote + self.progress = progress + } +} + public final class ChatControllerInteraction { public enum OpenPeerSource { case `default` @@ -85,15 +95,34 @@ public final class ChatControllerInteraction { // MARK: Nicegram Translate public let onTranslateButtonLongTap: () -> Void // + + public struct OpenUrl { + public var url: String + public var concealed: Bool + public var external: Bool? + public var message: Message? + public var allowInlineWebpageResolution: Bool + public var progress: Promise? + + public init(url: String, concealed: Bool, external: Bool? = nil, message: Message? = nil, allowInlineWebpageResolution: Bool = false, progress: Promise? = nil) { + self.url = url + self.concealed = concealed + self.external = external + self.message = message + self.allowInlineWebpageResolution = allowInlineWebpageResolution + self.progress = progress + } + } + public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void - public let openPeerMention: (String) -> Void + public let openPeerMention: (String, Promise?) -> Void public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void public let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void public let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void public let activateMessagePinch: (PinchSourceContainerNode) -> Void public let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void - public let navigateToMessage: (MessageId, MessageId) -> Void + public let navigateToMessage: (MessageId, MessageId, NavigateToMessageParams) -> Void public let navigateToMessageStandalone: (MessageId) -> Void public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void public let tapMessage: ((Message) -> Void)? @@ -108,7 +137,7 @@ public final class ChatControllerInteraction { public let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void public let activateSwitchInline: (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void - public let openUrl: (String, Bool, Bool?, Message?) -> Void + public let openUrl: (OpenUrl) -> Void public let shareCurrentLocation: () -> Void public let shareAccountContact: () -> Void public let sendBotCommand: (MessageId?, String) -> Void @@ -130,6 +159,7 @@ public final class ChatControllerInteraction { public let openSearch: () -> Void public let setupReply: (MessageId) -> Void public let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction + public let canSendMessages: () -> Bool public let navigateToFirstDateMessage: (Int32, Bool) -> Void public let requestRedeliveryOfFailedMessages: (MessageId) -> Void public let addContact: (String) -> Void @@ -142,7 +172,7 @@ public final class ChatControllerInteraction { public let scheduleCurrentMessage: () -> Void public let sendScheduledMessagesNow: ([MessageId]) -> Void public let editScheduledMessagesTime: ([MessageId]) -> Void - public let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void + public let performTextSelectionAction: (Message?, Bool, NSAttributedString, TextSelectionAction) -> Void public let displayImportedMessageTooltip: (ASDisplayNode) -> Void public let displaySwipeToReplyHint: () -> Void public let dismissReplyMarkupMessage: (Message) -> Void @@ -171,12 +201,15 @@ public final class ChatControllerInteraction { public let activateAdAction: (EngineMessage.Id) -> Void public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void public let saveMediaToFiles: (EngineMessage.Id) -> Void + public let openNoAdsDemo: () -> Void + public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void public let dismissTextInput: () -> Void public let scrollToMessageId: (MessageIndex) -> Void public let navigateToStory: (Message, StoryId) -> Void + public let attemptedNavigationToPrivateQuote: (Peer?) -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] @@ -204,13 +237,13 @@ public final class ChatControllerInteraction { // openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void, - openPeerMention: @escaping (String) -> Void, + openPeerMention: @escaping (String, Promise?) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void, openMessageReactionContextMenu: @escaping (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void, updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void, activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, - navigateToMessage: @escaping (MessageId, MessageId) -> Void, + navigateToMessage: @escaping (MessageId, MessageId, NavigateToMessageParams) -> Void, navigateToMessageStandalone: @escaping (MessageId) -> Void, navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void, tapMessage: ((Message) -> Void)?, @@ -225,7 +258,7 @@ public final class ChatControllerInteraction { requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void, activateSwitchInline: @escaping (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void, - openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, + openUrl: @escaping (OpenUrl) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, @@ -247,6 +280,7 @@ public final class ChatControllerInteraction { openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, + canSendMessages: @escaping () -> Bool, navigateToFirstDateMessage: @escaping(Int32, Bool) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, @@ -259,7 +293,7 @@ public final class ChatControllerInteraction { scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, - performTextSelectionAction: @escaping (Bool, NSAttributedString, TextSelectionAction) -> Void, + performTextSelectionAction: @escaping (Message?, Bool, NSAttributedString, TextSelectionAction) -> Void, displayImportedMessageTooltip: @escaping (ASDisplayNode) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, @@ -288,11 +322,14 @@ public final class ChatControllerInteraction { activateAdAction: @escaping (EngineMessage.Id) -> Void, openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void, saveMediaToFiles: @escaping (EngineMessage.Id) -> Void, + openNoAdsDemo: @escaping () -> Void, + displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, scrollToMessageId: @escaping (MessageIndex) -> Void, navigateToStory: @escaping (Message, StoryId) -> Void, + attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, @@ -346,6 +383,7 @@ public final class ChatControllerInteraction { self.openSearch = openSearch self.setupReply = setupReply self.canSetupReply = canSetupReply + self.canSendMessages = canSendMessages self.navigateToFirstDateMessage = navigateToFirstDateMessage self.requestRedeliveryOfFailedMessages = requestRedeliveryOfFailedMessages self.addContact = addContact @@ -387,11 +425,14 @@ public final class ChatControllerInteraction { self.activateAdAction = activateAdAction self.openRequestedPeerSelection = openRequestedPeerSelection self.saveMediaToFiles = saveMediaToFiles + self.openNoAdsDemo = openNoAdsDemo + self.displayGiveawayParticipationStatus = displayGiveawayParticipationStatus self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.dismissTextInput = dismissTextInput self.scrollToMessageId = scrollToMessageId self.navigateToStory = navigateToStory + self.attemptedNavigationToPrivateQuote = attemptedNavigationToPrivateQuote self.automaticMediaDownloadSettings = automaticMediaDownloadSettings diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD index fa0c0f90a31..59374b81620 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -40,6 +40,7 @@ swift_library( "//submodules/StickerPackPreviewUI", "//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent", "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView:LegacyMessageInputPanelInputView", + "//submodules/AttachmentTextInputPanelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 35c8d423c7c..d407acf2d38 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -20,7 +20,6 @@ import AudioToolbox import UndoUI import ContextUI import GalleryUI -import AttachmentTextInputPanelNode import TelegramPresentationData import TelegramNotices import StickerPeekUI @@ -33,6 +32,7 @@ import Pasteboard import StickerPackPreviewUI import EntityKeyboardGifContent import LegacyMessageInputPanelInputView +import AttachmentTextInputPanelNode public final class EmptyInputView: UIView, UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { @@ -160,7 +160,20 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let animationCache = context.animationCache let animationRenderer = context.animationRenderer - let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, hasTrending: hasTrending, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId, hasSearch: hasSearch, hideBackground: hideBackground) + let emojiItems = EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + isStandalone: false, + subject: .emoji, + hasTrending: hasTrending, + topReactionItems: [], + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: areCustomEmojiEnabled, + chatPeerId: chatPeerId, + hasSearch: hasSearch, + hideBackground: hideBackground + ) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] @@ -2092,7 +2105,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback { +public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { private let context: AccountContext public var insertText: ((NSAttributedString) -> Void)? @@ -2267,7 +2280,19 @@ public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputVie let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium).start(next: { value in + let _ = EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + isStandalone: true, + subject: .generic, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: areCustomEmojiEnabled, + chatPeerId: nil, + forceHasPremium: forceHasPremium + ).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -2282,7 +2307,20 @@ public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputVie gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium, hideBackground: hideBackground) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + isStandalone: true, + subject: .generic, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: areCustomEmojiEnabled, + chatPeerId: nil, + forceHasPremium: forceHasPremium, + hideBackground: hideBackground + ) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, @@ -2354,7 +2392,9 @@ public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputVie hasActiveGroupCall: false, importState: nil, threadData: nil, - isGeneralThreadClosed: nil + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil ) let _ = inputNode.updateLayout( diff --git a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift index 6f0e822ad50..637cd200492 100644 --- a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift +++ b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift @@ -73,7 +73,7 @@ final class LockView: UIButton, TGModernConversationInputMicButtonLock { [ "Path.Path.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, - "Path.Path.Заливка 1": theme.chat.inputPanel.panelBackgroundColor, + "Path.Path.Заливка 1": theme.chat.inputPanel.panelBackgroundColor.withAlphaComponent(1.0), "Rectangle.Rectangle.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, "Rectangle.Заливка 1": theme.chat.inputPanel.panelControlAccentColor, "Path 4.Path 4.Обводка 1": theme.chat.inputPanel.panelControlAccentColor diff --git a/submodules/TelegramUI/Components/CompositeTextNode/BUILD b/submodules/TelegramUI/Components/CompositeTextNode/BUILD new file mode 100644 index 00000000000..cdba78a4dfc --- /dev/null +++ b/submodules/TelegramUI/Components/CompositeTextNode/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "CompositeTextNode", + module_name = "CompositeTextNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/CompositeTextNode/Sources/CompositeTextNode.swift b/submodules/TelegramUI/Components/CompositeTextNode/Sources/CompositeTextNode.swift new file mode 100644 index 00000000000..0ab1648802e --- /dev/null +++ b/submodules/TelegramUI/Components/CompositeTextNode/Sources/CompositeTextNode.swift @@ -0,0 +1,117 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit + +public class CompositeTextNode: ASDisplayNode { + public enum Component: Equatable { + case text(NSAttributedString) + case icon(UIImage) + } + + public var components: [Component] = [] + + private var textNodes: [Int: ImmediateTextNode] = [:] + private var iconViews: [Int: UIImageView] = [:] + + public var imageTintColor: UIColor? { + didSet { + for (_, textNode) in self.textNodes { + textNode.layer.layerTintColor = self.imageTintColor?.cgColor + } + for (_, iconView) in self.iconViews { + iconView.tintColor = self.imageTintColor + } + } + } + + public func update(constrainedSize: CGSize) -> CGSize { + var validTextIds: [Int] = [] + var validIconIds: [Int] = [] + + var size = CGSize() + + var nextTextId = 0 + var nextIconId = 0 + for component in self.components { + switch component { + case let .text(text): + let id = nextTextId + nextTextId += 1 + validTextIds.append(id) + + let textNode: ImmediateTextNode + if let current = self.textNodes[id] { + textNode = current + } else { + textNode = ImmediateTextNode() + textNode.maximumNumberOfLines = 1 + textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) + textNode.layer.layerTintColor = self.imageTintColor?.cgColor + self.textNodes[id] = textNode + self.addSubnode(textNode) + } + textNode.attributedText = text + + let textSize = textNode.updateLayout(CGSize(width: max(1.0, constrainedSize.width - size.width), height: constrainedSize.height)) + + textNode.frame = CGRect(origin: CGPoint(x: size.width - textNode.insets.left, y: -textNode.insets.top), size: textSize) + size.width += textSize.width - textNode.insets.left - textNode.insets.right + size.height = max(size.height, textSize.height - textNode.insets.top - textNode.insets.bottom) + case let .icon(icon): + let id = nextIconId + nextIconId += 1 + validIconIds.append(id) + + let iconView: UIImageView + if let current = self.iconViews[id] { + iconView = current + } else { + iconView = UIImageView() + self.iconViews[id] = iconView + self.view.addSubview(iconView) + } + iconView.image = icon + iconView.tintColor = self.imageTintColor + + let iconSize = icon.size + if size.width != 0.0 { + size.width += 3.0 + } + iconView.frame = CGRect(origin: CGPoint(x: size.width, y: 3.0 + UIScreenPixel), size: iconSize) + size.width += iconSize.width + size.width += 3.0 + size.height = max(size.height, iconSize.height) + } + + if size.width >= constrainedSize.width { + size.width = constrainedSize.width + break + } + } + + var removeTextIds: [Int] = [] + for (id, textNode) in self.textNodes { + if !validTextIds.contains(id) { + textNode.removeFromSupernode() + removeTextIds.append(id) + } + } + for id in removeTextIds { + self.textNodes.removeValue(forKey: id) + } + + var removeIconIds: [Int] = [] + for (id, iconView) in self.iconViews { + if !validIconIds.contains(id) { + iconView.removeFromSuperview() + removeIconIds.append(id) + } + } + for id in removeIconIds { + self.iconViews.removeValue(forKey: id) + } + + return size + } +} diff --git a/submodules/TelegramUI/Components/ContextMenuScreen/BUILD b/submodules/TelegramUI/Components/ContextMenuScreen/BUILD new file mode 100644 index 00000000000..4987cabf706 --- /dev/null +++ b/submodules/TelegramUI/Components/ContextMenuScreen/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ContextMenuScreen", + module_name = "ContextMenuScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/AccountContext", + "//submodules/AppBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Display/Source/ContextMenuActionNode.swift b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuActionNode.swift similarity index 76% rename from submodules/Display/Source/ContextMenuActionNode.swift rename to submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuActionNode.swift index 0cf21e1db28..4d803b4a960 100644 --- a/submodules/Display/Source/ContextMenuActionNode.swift +++ b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuActionNode.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import AsyncDisplayKit +import Display final private class ContextMenuActionButton: HighlightTrackingButton { override func convert(_ point: CGPoint, from view: UIView?) -> CGPoint { @@ -15,14 +16,14 @@ final private class ContextMenuActionButton: HighlightTrackingButton { final class ContextMenuActionNode: ASDisplayNode { private let textNode: ImmediateTextNode? private var textSize: CGSize? - private let iconNode: ASImageNode? + private let iconView: UIImageView? private let action: () -> Void private let button: ContextMenuActionButton private let actionArea: AccessibilityAreaNode var dismiss: (() -> Void)? - init(action: ContextMenuAction, blurred: Bool) { + init(action: ContextMenuAction, blurred: Bool, isDark: Bool) { self.actionArea = AccessibilityAreaNode() self.actionArea.accessibilityTraits = .button @@ -33,30 +34,30 @@ final class ContextMenuActionNode: ASDisplayNode { let textNode = ImmediateTextNode() textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false - textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: UIColor.white) + textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: isDark ? .white : .black) textNode.isAccessibilityElement = false self.textNode = textNode - self.iconNode = nil + self.iconView = nil case let .textWithIcon(title, icon): let textNode = ImmediateTextNode() textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false - textNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: UIColor.white) + textNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: isDark ? .white : .black) textNode.isAccessibilityElement = false - let iconNode = ASImageNode() - iconNode.displaysAsynchronously = false - iconNode.image = icon + let iconView = UIImageView() + iconView.tintColor = isDark ? .white : .black + iconView.image = icon self.textNode = textNode - self.iconNode = iconNode + self.iconView = iconView case let .icon(image): - let iconNode = ASImageNode() - iconNode.displaysAsynchronously = false - iconNode.image = image + let iconView = UIImageView() + iconView.tintColor = isDark ? .white : .black + iconView.image = image - self.iconNode = iconNode + self.iconView = iconView self.textNode = nil } self.action = action.action @@ -67,21 +68,25 @@ final class ContextMenuActionNode: ASDisplayNode { super.init() if !blurred { - self.backgroundColor = UIColor(rgb: 0x2f2f2f) + self.backgroundColor = isDark ? UIColor(rgb: 0x2f2f2f) : nil } if let textNode = self.textNode { self.addSubnode(textNode) } - if let iconNode = self.iconNode { - self.addSubnode(iconNode) + if let iconView = self.iconView { + self.view.addSubview(iconView) } self.button.highligthedChanged = { [weak self] highlighted in - if blurred { - self?.backgroundColor = highlighted ? UIColor(rgb: 0xffffff, alpha: 0.5) : .clear + if isDark { + if blurred { + self?.backgroundColor = highlighted ? UIColor(rgb: 0xffffff, alpha: 0.5) : .clear + } else { + self?.backgroundColor = highlighted ? UIColor(rgb: 0x8c8e8e) : UIColor(rgb: 0x2f2f2f) + } } else { - self?.backgroundColor = highlighted ? UIColor(rgb: 0x8c8e8e) : UIColor(rgb: 0x2f2f2f) + self?.backgroundColor = highlighted ? UIColor(rgb: 0xDCE3DC) : .clear } } self.view.addSubview(self.button) @@ -115,7 +120,7 @@ final class ContextMenuActionNode: ASDisplayNode { var totalWidth = 0.0 totalWidth += textSize.width - if let image = self.iconNode?.image { + if let image = self.iconView?.image { if totalWidth > 0.0 { totalWidth += 11.0 } @@ -126,7 +131,7 @@ final class ContextMenuActionNode: ASDisplayNode { } return CGSize(width: totalWidth, height: 54.0) - } else if let iconNode = self.iconNode, let image = iconNode.image { + } else if let iconView = self.iconView, let image = iconView.image { return CGSize(width: image.size.width + 36.0, height: 54.0) } else { return CGSize(width: 36.0, height: 54.0) @@ -143,7 +148,7 @@ final class ContextMenuActionNode: ASDisplayNode { if let textSize = self.textSize { totalWidth += textSize.width } - if let image = self.iconNode?.image { + if let image = self.iconView?.image { if totalWidth > 0.0 { totalWidth += 11.0 } @@ -153,9 +158,9 @@ final class ContextMenuActionNode: ASDisplayNode { if let textNode = self.textNode, let textSize = self.textSize { textNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - totalWidth) / 2.0), y: floor((self.bounds.size.height - textSize.height) / 2.0)), size: textSize) } - if let iconNode = self.iconNode, let image = iconNode.image { + if let iconView = self.iconView, let image = iconView.image { let iconSize = image.size - iconNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - totalWidth) / 2.0) + totalWidth - iconSize.width, y: floorToScreenPixels((self.bounds.size.height - iconSize.height) / 2.0)), size: iconSize) + iconView.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - totalWidth) / 2.0) + totalWidth - iconSize.width, y: floorToScreenPixels((self.bounds.size.height - iconSize.height) / 2.0)), size: iconSize) } } } diff --git a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuController.swift b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuController.swift new file mode 100644 index 00000000000..70ffcd512f4 --- /dev/null +++ b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuController.swift @@ -0,0 +1,111 @@ +import Foundation +import UIKit +import Display + +public final class ContextMenuControllerImpl: ViewController, KeyShortcutResponder, ContextMenuController { + private var contextMenuNode: ContextMenuNode { + return self.displayNode as! ContextMenuNode + } + + public var keyShortcuts: [KeyShortcut] { + return [KeyShortcut(input: UIKeyCommand.inputEscape, action: { [weak self] in + if let strongSelf = self { + strongSelf.dismiss() + } + })] + } + private let actions: [ContextMenuAction] + private let catchTapsOutside: Bool + private let hasHapticFeedback: Bool + private let blurred: Bool + private let skipCoordnateConversion: Bool + private let isDark: Bool + + private var layout: ContainerViewLayout? + + public var centerHorizontally = false + public var dismissed: (() -> Void)? + + public var dismissOnTap: ((UIView, CGPoint) -> Bool)? + + public init(_ arguments: ContextMenuControllerArguments) { + self.actions = arguments.actions + self.catchTapsOutside = arguments.catchTapsOutside + self.hasHapticFeedback = arguments.hasHapticFeedback + self.blurred = arguments.blurred + self.skipCoordnateConversion = arguments.skipCoordnateConversion + self.isDark = arguments.isDark + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = ContextMenuNode(actions: self.actions, dismiss: { [weak self] in + self?.dismissed?() + self?.contextMenuNode.animateOut(bounce: (self?.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { + self?.presentingViewController?.dismiss(animated: false) + }) + }, dismissOnTap: { [weak self] view, point in + guard let self, let dismissOnTap = self.dismissOnTap else { + return false + } + return dismissOnTap(view, point) + }, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback, blurred: self.blurred, isDark: self.isDark) + self.displayNodeDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.contextMenuNode.animateIn(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.dismissed?() + self.contextMenuNode.animateOut(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false) + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.contextMenuNode.centerHorizontally = self.centerHorizontally + if self.layout != nil && self.layout! != layout { + self.dismissed?() + self.contextMenuNode.animateOut(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false) + }) + } else { + self.layout = layout + + if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() { + if self.skipCoordnateConversion { + self.contextMenuNode.sourceRect = sourceRect + self.contextMenuNode.containerRect = containerRect + } else { + self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil) + self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil) + } + } else { + self.contextMenuNode.sourceRect = nil + self.contextMenuNode.containerRect = nil + } + + self.contextMenuNode.containerLayoutUpdated(layout, transition: transition) + } + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } +} + + + diff --git a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift new file mode 100644 index 00000000000..be2e5f3e761 --- /dev/null +++ b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift @@ -0,0 +1,320 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import AppBundle +import AsyncDisplayKit +import Display + +private final class ArrowNode: HighlightTrackingButtonNode { + private let isLeft: Bool + + private let iconView: UIImageView + private let separatorLayer: SimpleLayer + var action: (() -> Void)? + + init(isLeft: Bool, isDark: Bool) { + self.isLeft = isLeft + + self.iconView = UIImageView() + self.iconView.image = UIImage(bundleImageName: "Chat/Context Menu/Arrow")!.withRenderingMode(.alwaysTemplate) + if isLeft { + self.iconView.transform = CGAffineTransformMakeScale(-1.0, 1.0) + } + + self.separatorLayer = SimpleLayer() + + super.init() + + self.layer.addSublayer(self.separatorLayer) + self.view.addSubview(self.iconView) + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if isDark { + self.backgroundColor = highlighted ? UIColor(rgb: 0x8c8e8e) : nil + } else { + self.backgroundColor = highlighted ? UIColor(rgb: 0xDCE3DC) : nil + } + } + } + + @objc private func pressed() { + self.action?() + } + + func update(color: UIColor, separatorColor: UIColor, height: CGFloat) -> CGSize { + let size = CGSize(width: 33.0, height: height) + + self.iconView.tintColor = color + if let icon = self.iconView.image { + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) * 0.5), y: floor((size.height - icon.size.height) * 0.5)), size: icon.size) + self.iconView.center = CGPoint(x: iconFrame.midX, y: iconFrame.midY) + self.iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + } + + self.separatorLayer.backgroundColor = separatorColor.cgColor + self.separatorLayer.frame = CGRect(origin: CGPoint(x: self.isLeft ? (size.width - UIScreenPixel) : 0.0, y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height)) + + return size + } +} + +final class ContextMenuNode: ASDisplayNode { + private let isDark: Bool + + private let actions: [ContextMenuAction] + private let dismiss: () -> Void + private let dismissOnTap: (UIView, CGPoint) -> Bool + + private let containerNode: ContextMenuContainerNode + private let contentNode: ASDisplayNode + private var separatorNodes: [ASDisplayNode] = [] + private let actionNodes: [ContextMenuActionNode] + private let pageLeftNode: ArrowNode + private let pageRightNode: ArrowNode + + private var currentPageIndex: Int = 0 + private var pageCount: Int = 0 + + private var validLayout: ContainerViewLayout? + + var sourceRect: CGRect? + var containerRect: CGRect? + var arrowOnBottom: Bool = true + var centerHorizontally: Bool = false + + private var dismissedByTouchOutside = false + private let catchTapsOutside: Bool + + 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.isDark = isDark + + self.actions = actions + self.dismiss = dismiss + self.dismissOnTap = dismissOnTap + self.catchTapsOutside = catchTapsOutside + + self.containerNode = ContextMenuContainerNode(isBlurred: blurred, isDark: isDark) + self.contentNode = ASDisplayNode() + self.contentNode.clipsToBounds = true + + self.actionNodes = actions.map { action in + return ContextMenuActionNode(action: action, blurred: blurred, isDark: isDark) + } + + self.pageLeftNode = ArrowNode(isLeft: true, isDark: isDark) + self.pageRightNode = ArrowNode(isLeft: false, isDark: isDark) + + if hasHapticFeedback { + self.feedback = HapticFeedback() + self.feedback?.prepareImpact(.light) + } else { + self.feedback = nil + } + + super.init() + + self.containerNode.containerNode.addSubnode(self.contentNode) + + self.addSubnode(self.containerNode) + let dismissNode = { + dismiss() + } + for actionNode in self.actionNodes { + actionNode.dismiss = dismissNode + self.contentNode.addSubnode(actionNode) + } + + self.containerNode.containerNode.addSubnode(self.pageLeftNode) + self.containerNode.containerNode.addSubnode(self.pageRightNode) + + let navigatePage: (Bool) -> Void = { [weak self] isLeft in + guard let self else { + return + } + var index = self.currentPageIndex + if isLeft { + index -= 1 + } else { + index += 1 + } + index = max(0, min(index, self.pageCount - 1)) + if self.currentPageIndex != index { + self.currentPageIndex = index + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.35, curve: .spring)) + } + } + } + + self.pageLeftNode.action = { + navigatePage(true) + } + self.pageRightNode.action = { + navigatePage(false) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + struct Page { + var range: Range + var width: CGFloat + var offsetX: CGFloat + } + + let separatorColor = self.isDark ? UIColor(rgb: 0x8c8e8e) : UIColor(rgb: 0xDCE3DC) + + let height: CGFloat = 54.0 + + let pageLeftSize = self.pageLeftNode.update(color: self.isDark ? .white : .black, separatorColor: separatorColor, height: height) + let pageRightSize = self.pageRightNode.update(color: self.isDark ? .white : .black, separatorColor: separatorColor, height: height) + + let maxPageWidth = layout.size.width - 20.0 - pageLeftSize.width - pageRightSize.width + var absoluteActionOffsetX: CGFloat = 0.0 + + var pages: [Page] = [] + for i in 0 ..< self.actionNodes.count { + if i != 0 { + absoluteActionOffsetX += UIScreenPixel + } + let actionSize = self.actionNodes[i].measure(CGSize(width: layout.size.width, height: height)) + if pages.isEmpty || (pages[pages.count - 1].width + actionSize.width) > maxPageWidth { + pages.append(Page(range: i ..< (i + 1), width: actionSize.width, offsetX: absoluteActionOffsetX)) + } else { + pages[pages.count - 1].width += actionSize.width + } + let actionFrame = CGRect(origin: CGPoint(x: absoluteActionOffsetX, y: 0.0), size: actionSize) + self.actionNodes[i].frame = actionFrame + absoluteActionOffsetX += actionSize.width + + let separatorNode: ASDisplayNode + if i < self.separatorNodes.count { + separatorNode = self.separatorNodes[i] + } else { + separatorNode = ASDisplayNode() + separatorNode.isUserInteractionEnabled = false + self.separatorNodes.append(separatorNode) + self.contentNode.insertSubnode(separatorNode, at: 0) + } + separatorNode.backgroundColor = separatorColor + separatorNode.frame = CGRect(origin: CGPoint(x: actionFrame.maxX, y: 0.0), size: CGSize(width: UIScreenPixel, height: height)) + separatorNode.isHidden = i == self.actionNodes.count - 1 + } + + self.pageCount = pages.count + + if !pages.isEmpty { + var leftInset: CGFloat = 0.0 + if self.currentPageIndex > 0 { + leftInset = pageLeftSize.width + } + var rightInset: CGFloat = 0.0 + if self.currentPageIndex < pages.count - 1 { + rightInset = pageLeftSize.width + } + + let offsetX = -pages[self.currentPageIndex].offsetX + + let contentWidth = leftInset + rightInset + pages[self.currentPageIndex].width + + let contentNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: pages[self.currentPageIndex].width, height: height)) + transition.updatePosition(node: self.contentNode, position: CGPoint(x: contentNodeFrame.midX, y: contentNodeFrame.midY)) + transition.updateBounds(node: self.contentNode, bounds: CGRect(origin: CGPoint(x: -offsetX, y: 0.0), size: contentNodeFrame.size)) + + transition.updateFrame(node: self.pageLeftNode, frame: CGRect(origin: CGPoint(x: leftInset - pageLeftSize.width, y: 0.0), size: pageLeftSize)) + transition.updateFrame(node: self.pageRightNode, frame: CGRect(origin: CGPoint(x: contentWidth - rightInset, y: 0.0), size: pageRightSize)) + + let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize()) + let containerRect: CGRect = self.containerRect ?? CGRect(origin: CGPoint(), size: layout.size) + + let insets = layout.insets(options: [.statusBar, .input]) + + let verticalOrigin: CGFloat + var arrowOnBottom = true + if sourceRect.minY - height > containerRect.minY + insets.top { + verticalOrigin = sourceRect.minY - height + } else { + verticalOrigin = min(containerRect.maxY - insets.bottom - height, sourceRect.maxY) + arrowOnBottom = false + } + self.arrowOnBottom = arrowOnBottom + + let horizontalOrigin: CGFloat = floor(max(8.0, min(self.centerHorizontally ? sourceRect.midX - contentWidth / 2.0 : max(sourceRect.minX + 8.0, sourceRect.midX - contentWidth / 2.0), layout.size.width - contentWidth - 8.0))) + + let containerFrame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: CGSize(width: contentWidth, height: height)) + transition.updateFrame(node: self.containerNode, frame: containerFrame) + self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom) + self.containerNode.updateLayout(transition: transition) + } + } + + func animateIn(bounce: Bool) { + if bounce { + self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4) + let containerPosition = self.containerNode.layer.position + 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 + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in + self?.allowsGroupOpacity = false + self?.layer.shouldRasterize = false + }) + + if let feedback = self.feedback { + feedback.impact(.light) + } + } + + func animateOut(bounce: Bool, completion: @escaping () -> Void) { + 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 + completion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let event = event { + var eventIsPresses = false + if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { + eventIsPresses = event.type == .presses + } + if event.type == .touches || eventIsPresses { + if !self.containerNode.frame.contains(point) { + if self.dismissOnTap(self.view, point) { + self.dismiss() + if self.catchTapsOutside { + return self.view + } else { + return nil + } + } + if !self.dismissedByTouchOutside { + self.dismissedByTouchOutside = true + self.dismiss() + } + if self.catchTapsOutside { + return self.view + } + return nil + } + } + } + return super.hitTest(point, with: event) + } +} diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index d0f5381b105..347d40036f0 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -822,6 +822,8 @@ public final class EmojiStatusSelectionController: ViewController { dynamicColor: 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: diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 8c4bdd3667c..018d5d46469 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -606,11 +606,11 @@ public final class CustomEmojiContainerView: UIView { preconditionFailure() } - public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { + public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)]) { var nextIndexById: [Int64: Int] = [:] var validKeys = Set() - for (rect, emoji) in emojiRects { + for (rect, emoji, fontSize) in emojiRects { let index: Int if let nextIndex = nextIndexById[emoji.fileId] { index = nextIndex diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index c06c01e2c1c..131a3b47845 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1331,6 +1331,8 @@ private final class GroupEmbeddedView: UIScrollView, UIScrollViewDelegate, Pager } switch item.tintMode { + case let .custom(color): + itemLayer.layerTintColor = color.cgColor case .accent: itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor case .primary: @@ -2472,6 +2474,7 @@ public final class EmojiPagerContentComponent: Component { public enum Icon: Equatable, Hashable { case premiumStar case topic(String, Int32) + case stop } case animation(EntityKeyboardAnimationData) @@ -2497,10 +2500,11 @@ public final class EmojiPagerContentComponent: Component { case premium } - public enum TintMode { + public enum TintMode: Equatable { case none case accent case primary + case custom(UIColor) } public let animationData: EntityKeyboardAnimationData? @@ -2567,6 +2571,7 @@ public final class EmojiPagerContentComponent: Component { public let displayPremiumBadges: Bool public let headerItem: EntityKeyboardAnimationData? public let fillWithLoadingPlaceholders: Bool + public let customTintColor: UIColor? public let items: [Item] public init( @@ -2583,6 +2588,7 @@ public final class EmojiPagerContentComponent: Component { displayPremiumBadges: Bool, headerItem: EntityKeyboardAnimationData?, fillWithLoadingPlaceholders: Bool, + customTintColor: UIColor? = nil, items: [Item] ) { self.supergroupId = supergroupId @@ -2598,6 +2604,7 @@ public final class EmojiPagerContentComponent: Component { self.displayPremiumBadges = displayPremiumBadges self.headerItem = headerItem self.fillWithLoadingPlaceholders = fillWithLoadingPlaceholders + self.customTintColor = customTintColor self.items = items } @@ -2644,6 +2651,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.fillWithLoadingPlaceholders != rhs.fillWithLoadingPlaceholders { return false } + if lhs.customTintColor != rhs.customTintColor { + return false + } if lhs.items != rhs.items { return false } @@ -3445,10 +3455,15 @@ public final class EmojiPagerContentComponent: Component { let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } + case .stop: + if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/NoIcon"), color: .white) { + let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) + image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) + } } UIGraphicsPopContext() - }) + })?.withRenderingMode(icon == .stop ? .alwaysTemplate : .alwaysOriginal) self.contents = image?.cgImage } } @@ -5883,6 +5898,8 @@ public final class EmojiPagerContentComponent: Component { itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor) switch item.tintMode { + case let .custom(color): + itemLayer.layerTintColor = color.cgColor case .accent: itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemAccentColor.cgColor case .primary: @@ -5905,7 +5922,12 @@ public final class EmojiPagerContentComponent: Component { var isSelected = false if let itemFile = item.itemFile, component.selectedItems.contains(itemFile.fileId) { isSelected = true - } else if case let .icon(icon) = item.content.id, case .topic = icon, component.selectedItems.isEmpty { + } else if case let .icon(icon) = item.content.id, component.selectedItems.isEmpty { + if case .topic = icon { + isSelected = true + } else if case .stop = icon { + isSelected = true + } } if isSelected { @@ -5921,7 +5943,10 @@ public final class EmojiPagerContentComponent: Component { self.visibleItemSelectionLayers[itemId] = itemSelectionLayer } - if case .accent = item.tintMode { + if case let .custom(color) = item.tintMode { + itemSelectionLayer.backgroundColor = color.withMultipliedAlpha(0.1).cgColor + itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor + } else if case .accent = item.tintMode { itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor } else { @@ -5936,9 +5961,9 @@ public final class EmojiPagerContentComponent: Component { itemTransition.setFrame(layer: itemSelectionLayer, frame: baseItemFrame) - itemLayer.transform = CATransform3DMakeScale(0.8, 0.8, 1.0) +// itemLayer.transform = CATransform3DMakeScale(0.8, 0.8, 1.0) } else { - itemLayer.transform = CATransform3DIdentity +// itemLayer.transform = CATransform3DIdentity } if animateItemIn, !transition.animation.isImmediate, let contentAnimation = contentAnimation, case .groupExpanded(id: itemGroup.groupId) = contentAnimation.type, let placeholderView = self.visibleItemPlaceholderViews[itemId] { @@ -6114,6 +6139,7 @@ public final class EmojiPagerContentComponent: Component { break } if case let .icon(icon) = id.itemId, case .topic = icon, component.selectedItems.isEmpty { + } else if case let .icon(icon) = id.itemId, case .stop = icon, component.selectedItems.isEmpty { } else if let fileId = fileId, component.selectedItems.contains(fileId) { } else { itemSelectionLayer.removeFromSuperlayer() @@ -7157,19 +7183,25 @@ public final class EmojiPagerContentComponent: Component { return hasPremium } + public enum Subject { + case generic + case status + case reaction + case emoji + case topicIcon + case quickReaction + case profilePhoto + case groupPhoto + case backgroundIcon + } + public static func emojiInputData( context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, - isStatusSelection: Bool, - isReactionSelection: Bool, - isEmojiSelection: Bool, + subject: Subject, hasTrending: Bool, - isTopicIconSelection: Bool = false, - isQuickReactionSelection: Bool = false, - isProfilePhotoEmojiSelection: Bool = false, - isGroupPhotoEmojiSelection: Bool = false, topReactionItems: [EmojiComponentReactionItem], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, @@ -7178,6 +7210,7 @@ public final class EmojiPagerContentComponent: Component { topStatusTitle: String? = nil, topicTitle: String? = nil, topicColor: Int32? = nil, + backgroundIconColor: UIColor? = nil, hasSearch: Bool = true, forceHasPremium: Bool = false, premiumIfSavedMessages: Bool = true, @@ -7194,7 +7227,7 @@ public final class EmojiPagerContentComponent: Component { var iconStatusEmoji: Signal<[TelegramMediaFile], NoError> = .single([]) - if isStatusSelection { + if case .status = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) @@ -7208,10 +7241,10 @@ public final class EmojiPagerContentComponent: Component { } } |> take(1) - } else if isReactionSelection { + } else if [.reaction, .quickReaction].contains(subject) { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudTopReactions) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentReactions) - } else if isTopicIconSelection { + } else if case .topicIcon = subject { iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconTopicEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in switch result { @@ -7222,25 +7255,27 @@ public final class EmojiPagerContentComponent: Component { } } |> take(1) - } else if isProfilePhotoEmojiSelection { + } else if case .profilePhoto = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji) - } else if isGroupPhotoEmojiSelection { + } else if case .groupPhoto = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedGroupPhotoEmoji) + } else if case .backgroundIcon = subject { + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedBackgroundIconEmoji) } let availableReactions: Signal - if isReactionSelection { + if [.reaction, .quickReaction].contains(subject) { availableReactions = context.engine.stickers.availableReactions() } else { availableReactions = .single(nil) } let searchCategories: Signal - if isEmojiSelection || isReactionSelection { + if [.emoji, .reaction].contains(subject) { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .emoji) - } else if isStatusSelection { + } else if case .status = subject { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) - } else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection { + } else if [.profilePhoto, .groupPhoto].contains(subject) { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .avatar) } else { searchCategories = .single(nil) @@ -7381,6 +7416,7 @@ public final class EmojiPagerContentComponent: Component { var topReactions: OrderedItemListView? var recentReactions: OrderedItemListView? var featuredAvatarEmoji: OrderedItemListView? + var featuredBackgroundIconEmoji: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { recentEmoji = orderedView @@ -7396,10 +7432,12 @@ public final class EmojiPagerContentComponent: Component { featuredAvatarEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedGroupPhotoEmoji { featuredAvatarEmoji = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedBackgroundIconEmoji { + featuredBackgroundIconEmoji = orderedView } } - if isTopicIconSelection { + if case .topicIcon = subject { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.topic(String((topicTitle ?? "").prefix(1)), topicColor ?? 0)), @@ -7458,7 +7496,7 @@ public final class EmojiPagerContentComponent: Component { itemGroups[groupIndex].items.append(resultItem) } } - } else if isStatusSelection { + } else if case .status = subject { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.premiumStar), @@ -7618,7 +7656,7 @@ public final class EmojiPagerContentComponent: Component { } } } - } else if isReactionSelection { + } else if [.reaction, .quickReaction].contains(subject) { var existingIds = Set() var topReactionItems = topReactionItems @@ -7745,7 +7783,7 @@ public final class EmojiPagerContentComponent: Component { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) } } else { let groupId = "recent" @@ -7829,11 +7867,11 @@ public final class EmojiPagerContentComponent: Component { popularInsertIndex += 1 } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) } } } - } else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection { + } else if [.profilePhoto, .groupPhoto].contains(subject) { var existingIds = Set() let groupId = "recent" @@ -7881,6 +7919,80 @@ public final class EmojiPagerContentComponent: Component { tintMode: tintMode ) + if let groupIndex = itemGroupIndexById[groupId] { + if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { + break + } + + itemGroups[groupIndex].items.append(resultItem) + } + } + } + } else if case .backgroundIcon = subject { + var existingIds = Set() + + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .icon(.stop), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: backgroundIconColor.flatMap { .custom($0) } ?? .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: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + } + + if let featuredBackgroundIconEmoji { + for item in featuredBackgroundIconEmoji.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 { + if let backgroundIconColor { + tintMode = .custom(backgroundIconColor) + } else { + 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 @@ -7892,7 +8004,9 @@ public final class EmojiPagerContentComponent: Component { } } - if let recentEmoji = recentEmoji, !isReactionSelection, !isStatusSelection, !isProfilePhotoEmojiSelection, !isGroupPhotoEmojiSelection { + let hasRecentEmoji = ![.reaction, .quickReaction, .status, .profilePhoto, .groupPhoto, .topicIcon, .backgroundIcon].contains(subject) + + if let recentEmoji = recentEmoji, hasRecentEmoji { for item in recentEmoji.items { guard let item = item.contents.get(RecentEmojiItem.self) else { continue @@ -7947,7 +8061,8 @@ public final class EmojiPagerContentComponent: Component { if !hasPremium { maybeAppendUnicodeEmoji() } - + + var skippedCollectionIds = Set() if areCustomEmojiEnabled { for entry in view.entries { guard let item = entry.item as? StickerPackItem else { @@ -7955,17 +8070,35 @@ public final class EmojiPagerContentComponent: Component { } var icon: EmojiPagerContentComponent.Item.Icon = .none - if isReactionSelection, !hasPremium { + if [.reaction, .quickReaction].contains(subject), !hasPremium { icon = .locked } + let supergroupId = entry.index.collectionId + let groupId: AnyHashable = supergroupId + + if skippedCollectionIds.contains(groupId) { + continue + } + + var isTemplate = false var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { - if isStatusSelection { - tintMode = .accent + if [.status, .backgroundIcon].contains(subject) { + if let backgroundIconColor { + tintMode = .custom(backgroundIconColor) + } else { + tintMode = .accent + } } else { tintMode = .primary } + if case .backgroundIcon = subject { + isTemplate = true + } + } else if case .backgroundIcon = subject { + skippedCollectionIds.insert(groupId) + continue } let animationData = EntityKeyboardAnimationData(file: item.file) @@ -7978,8 +8111,6 @@ public final class EmojiPagerContentComponent: Component { tintMode: tintMode ) - let supergroupId = entry.index.collectionId - let groupId: AnyHashable = supergroupId let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium if isPremiumLocked && isPremiumDisabled { continue @@ -8012,7 +8143,7 @@ public final class EmojiPagerContentComponent: Component { dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, isReaction: false, - isTemplate: false + isTemplate: isTemplate ) } @@ -8028,15 +8159,29 @@ public final class EmojiPagerContentComponent: Component { if installedCollectionIds.contains(featuredEmojiPack.info.id) { continue } + + let supergroupId = featuredEmojiPack.info.id + let groupId: AnyHashable = supergroupId + + if skippedCollectionIds.contains(groupId) { + continue + } for item in featuredEmojiPack.topItems { var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { - if isStatusSelection { - tintMode = .accent + if [.status, .backgroundIcon].contains(subject) { + if let backgroundIconColor { + tintMode = .custom(backgroundIconColor) + } else { + tintMode = .accent + } } else { tintMode = .primary } + } else if case .backgroundIcon = subject { + skippedCollectionIds.insert(groupId) + continue } let animationData = EntityKeyboardAnimationData(file: item.file) @@ -8049,8 +8194,6 @@ public final class EmojiPagerContentComponent: Component { tintMode: tintMode ) - let supergroupId = featuredEmojiPack.info.id - let groupId: AnyHashable = supergroupId let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium if isPremiumLocked && isPremiumDisabled { continue @@ -8099,13 +8242,13 @@ public final class EmojiPagerContentComponent: Component { var displaySearchWithPlaceholder: String? let searchInitiallyHidden = true if hasSearch { - if isReactionSelection { + if [.reaction, .quickReaction].contains(subject) { displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder - } else if isStatusSelection { + } else if case .status = subject { displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder - } else if isEmojiSelection { + } else if case .emoji = subject { displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder - } else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection { + } else if [.profilePhoto, .groupPhoto].contains(subject) { displaySearchWithPlaceholder = strings.Common_Search } } @@ -8149,10 +8292,14 @@ public final class EmojiPagerContentComponent: Component { displayPremiumBadges: false, headerItem: headerItem, fillWithLoadingPlaceholders: false, + customTintColor: backgroundIconColor, items: group.items ) } + let warpContentsOnEdges = [.reaction, .quickReaction, .status, .profilePhoto, .groupPhoto, .backgroundIcon].contains(subject) + let enableLongPress = [.reaction, .status].contains(subject) + return EmojiPagerContentComponent( id: "emoji", context: context, @@ -8165,7 +8312,7 @@ public final class EmojiPagerContentComponent: Component { itemLayoutType: .compact, itemContentUniqueId: nil, searchState: .empty(hasResults: false), - warpContentsOnEdges: isReactionSelection || isStatusSelection || isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection, + warpContentsOnEdges: warpContentsOnEdges, hideBackground: hideBackground, displaySearchWithPlaceholder: displaySearchWithPlaceholder, searchCategories: searchCategories, @@ -8173,7 +8320,7 @@ public final class EmojiPagerContentComponent: Component { searchAlwaysActive: false, searchIsPlaceholderOnly: false, emptySearchResults: nil, - enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection, + enableLongPress: enableLongPress, selectedItems: selectedItems ) } @@ -8356,7 +8503,20 @@ public final class EmojiPagerContentComponent: Component { let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + itemGroups.append( + ItemGroup( + supergroupId: groupId, + id: groupId, + title: title, + subtitle: nil, + actionButtonTitle: nil, + isPremiumLocked: false, + isFeatured: false, + displayPremiumBadges: false, + headerItem: nil, + items: [resultItem] + ) + ) } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index cd8fd7eb052..7ca5c7da763 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -136,6 +136,7 @@ public final class EntityKeyboardComponent: Component { public let clipContentToTopPanel: Bool public let useExternalSearchContainer: Bool public let hidePanels: Bool + public let customTintColor: UIColor? public init( // MARK: Nicegram OpenGifsShortcut @@ -172,7 +173,8 @@ public final class EntityKeyboardComponent: Component { isExpanded: Bool, clipContentToTopPanel: Bool, useExternalSearchContainer: Bool, - hidePanels: Bool = false + hidePanels: Bool = false, + customTintColor: UIColor? = nil ) { // MARK: Nicegram OpenGifsShortcut self.defaultTab = defaultTab @@ -209,6 +211,7 @@ public final class EntityKeyboardComponent: Component { self.clipContentToTopPanel = clipContentToTopPanel self.useExternalSearchContainer = useExternalSearchContainer self.hidePanels = hidePanels + self.customTintColor = customTintColor } public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { @@ -275,7 +278,9 @@ public final class EntityKeyboardComponent: Component { if lhs.useExternalSearchContainer != rhs.useExternalSearchContainer { return false } - + if lhs.customTintColor != rhs.customTintColor { + return false + } return true } @@ -359,6 +364,7 @@ public final class EntityKeyboardComponent: Component { icon: icon, theme: component.theme, useAccentColor: false, + customTintColor: component.customTintColor, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil) @@ -394,6 +400,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardTopPanelComponent( id: "masks", theme: component.theme, + customTintColor: component.customTintColor, items: topMaskItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, defaultActiveItemId: maskContent.panelItemGroups.first?.groupId, @@ -448,6 +455,7 @@ public final class EntityKeyboardComponent: Component { icon: .featured, theme: component.theme, useAccentColor: false, + customTintColor: component.customTintColor, title: component.strings.Stickers_Trending, pressed: { [weak self] in self?.component?.stickerContent?.inputInteractionHolder.inputInteraction?.openFeatured?() @@ -493,6 +501,7 @@ public final class EntityKeyboardComponent: Component { icon: icon, theme: component.theme, useAccentColor: false, + customTintColor: component.customTintColor, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) @@ -529,6 +538,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( id: "stickers", theme: component.theme, + customTintColor: component.customTintColor, items: topStickerItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, defaultActiveItemId: stickerContent.panelItemGroups.first?.groupId, @@ -591,6 +601,7 @@ public final class EntityKeyboardComponent: Component { icon: icon, theme: component.theme, useAccentColor: false, + customTintColor: component.customTintColor, title: title, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) @@ -628,6 +639,7 @@ public final class EntityKeyboardComponent: Component { animationRenderer: emojiContent.animationRenderer, theme: component.theme, title: itemGroup.title ?? "", + customTintColor: itemGroup.customTintColor, pressed: { [weak self] in self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) } @@ -640,6 +652,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( id: "emoji", theme: component.theme, + customTintColor: component.customTintColor, items: topEmojiItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, activeContentItemIdUpdated: emojiContentItemIdUpdated, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 493ed6d3a3b..8f2186a215e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -24,6 +24,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer let theme: PresentationTheme + let customTintColor: UIColor? let title: String let pressed: () -> Void @@ -36,6 +37,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { animationRenderer: MultiAnimationRenderer, theme: PresentationTheme, title: String, + customTintColor: UIColor? = nil, pressed: @escaping () -> Void ) { self.context = context @@ -46,6 +48,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { self.animationRenderer = animationRenderer self.theme = theme self.title = title + self.customTintColor = customTintColor self.pressed = pressed } @@ -74,6 +77,9 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { if lhs.title != rhs.title { return false } + if lhs.customTintColor != rhs.customTintColor { + return false + } return true } @@ -109,6 +115,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { let displaySize = dimensions.aspectFitted(CGSize(width: 44.0, height: 44.0)) if self.itemLayer == nil { + let tintColor: EmojiPagerContentComponent.Item.TintMode = component.customTintColor.flatMap { .custom($0) } ?? .primary let itemLayer = EmojiPagerContentComponent.View.ItemLayer( item: EmojiPagerContentComponent.Item( animationData: component.item, @@ -116,7 +123,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - tintMode: component.item.isTemplate ? .primary : .none + tintMode: component.item.isTemplate ? tintColor : .none ), context: component.context, attemptSynchronousLoad: false, @@ -165,6 +172,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { itemLayer.layerTintColor = component.theme.list.itemPrimaryTextColor.cgColor case .accent: itemLayer.layerTintColor = component.theme.list.itemAccentColor.cgColor + case let .custom(color): + itemLayer.layerTintColor = component.customTintColor?.cgColor ?? color.cgColor } itemLayer.isVisibleForAnimations = itemEnvironment.isContentInFocus && component.context.sharedContext.energyUsageSettings.loopEmoji @@ -272,6 +281,7 @@ final class EntityKeyboardIconTopPanelComponent: Component { let icon: Icon let theme: PresentationTheme let useAccentColor: Bool + let customTintColor: UIColor? let title: String let pressed: () -> Void @@ -279,12 +289,14 @@ final class EntityKeyboardIconTopPanelComponent: Component { icon: Icon, theme: PresentationTheme, useAccentColor: Bool, + customTintColor: UIColor?, title: String, pressed: @escaping () -> Void ) { self.icon = icon self.theme = theme self.useAccentColor = useAccentColor + self.customTintColor = customTintColor self.title = title self.pressed = pressed } @@ -299,6 +311,9 @@ final class EntityKeyboardIconTopPanelComponent: Component { if lhs.useAccentColor != rhs.useAccentColor { return false } + if lhs.customTintColor != rhs.customTintColor { + return false + } if lhs.title != rhs.title { return false } @@ -374,14 +389,18 @@ final class EntityKeyboardIconTopPanelComponent: Component { self.component = component let color: UIColor - if itemEnvironment.isHighlighted { - if component.useAccentColor { - color = component.theme.list.itemAccentColor + if let customTintColor = component.customTintColor { + color = customTintColor + } else { + if itemEnvironment.isHighlighted { + if component.useAccentColor { + color = component.theme.list.itemAccentColor + } else { + color = component.theme.chat.inputMediaPanel.panelHighlightedIconColor + } } else { - color = component.theme.chat.inputMediaPanel.panelHighlightedIconColor + color = component.theme.chat.inputMediaPanel.panelIconColor } - } else { - color = component.theme.chat.inputMediaPanel.panelIconColor } if self.iconView.tintColor != color { @@ -1183,6 +1202,7 @@ public final class EntityKeyboardTopPanelComponent: Component { let id: AnyHashable let theme: PresentationTheme + let customTintColor: UIColor? let items: [Item] let containerSideInset: CGFloat let defaultActiveItemId: AnyHashable? @@ -1194,6 +1214,7 @@ public final class EntityKeyboardTopPanelComponent: Component { init( id: AnyHashable, theme: PresentationTheme, + customTintColor: UIColor?, items: [Item], containerSideInset: CGFloat, defaultActiveItemId: AnyHashable? = nil, @@ -1204,6 +1225,7 @@ public final class EntityKeyboardTopPanelComponent: Component { ) { self.id = id self.theme = theme + self.customTintColor = customTintColor self.items = items self.containerSideInset = containerSideInset self.defaultActiveItemId = defaultActiveItemId @@ -1220,6 +1242,9 @@ public final class EntityKeyboardTopPanelComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.customTintColor != rhs.customTintColor { + return false + } if lhs.items != rhs.items { return false } @@ -1838,8 +1863,12 @@ public final class EntityKeyboardTopPanelComponent: Component { } func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - if self.component?.theme !== component.theme { - self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor + if self.component?.theme !== component.theme || self.component?.customTintColor != component.customTintColor { + if let customTintColor = component.customTintColor { + self.highlightedIconBackgroundView.backgroundColor = customTintColor.withAlphaComponent(0.1) + } else { + self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor + } } self.component = component self.state = state diff --git a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift index 7085d8c9c0c..15c955e0b35 100644 --- a/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift +++ b/submodules/TelegramUI/Components/EntityKeyboardGifContent/Sources/GifContext.swift @@ -55,6 +55,12 @@ public func paneGifSearchForQuery(context: AccountContext, query: String, offset |> mapToSignal { searchBots -> Signal in let botName = searchBots.gifBotUsername ?? "gif" return context.engine.peers.resolvePeerByName(name: botName) + |> mapToSignal { result in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index d1f3df7b5b0..59e48bdfc0d 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -513,11 +513,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: false, - isEmojiSelection: false, + subject: .topicIcon, hasTrending: false, - isTopicIconSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, @@ -582,11 +579,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: false, - isEmojiSelection: false, + subject: .topicIcon, hasTrending: false, - isTopicIconSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, @@ -916,9 +910,6 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { } for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredEmojiPack.info.id == collectionId { - // if let strongSelf = self { - // strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) - // } let _ = accountContext.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() break diff --git a/submodules/TelegramUI/Components/ItemListDatePickerItem/BUILD b/submodules/TelegramUI/Components/ItemListDatePickerItem/BUILD new file mode 100644 index 00000000000..4fd87b12e81 --- /dev/null +++ b/submodules/TelegramUI/Components/ItemListDatePickerItem/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ItemListDatePickerItem", + module_name = "ItemListDatePickerItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ItemListUI:ItemListUI", + "//submodules/DatePickerNode:DatePickerNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/InviteLinksUI/Sources/ItemListDatePickerItem.swift b/submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift similarity index 95% rename from submodules/InviteLinksUI/Sources/ItemListDatePickerItem.swift rename to submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift index dec343cce69..4ea19727589 100644 --- a/submodules/InviteLinksUI/Sources/ItemListDatePickerItem.swift +++ b/submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift @@ -11,6 +11,8 @@ public class ItemListDatePickerItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let date: Int32? + let minDate: Int32? + let maxDate: Int32? public let sectionId: ItemListSectionId let style: ItemListStyle let updated: ((Int32) -> Void)? @@ -20,6 +22,8 @@ public class ItemListDatePickerItem: ListViewItem, ItemListItem { presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, date: Int32?, + minDate: Int32? = nil, + maxDate: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle, updated: ((Int32) -> Void)?, @@ -28,6 +32,8 @@ public class ItemListDatePickerItem: ListViewItem, ItemListItem { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.date = date + self.minDate = minDate + self.maxDate = maxDate self.sectionId = sectionId self.style = style self.updated = updated @@ -223,7 +229,15 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode { strongSelf.item?.updated?(Int32(date.timeIntervalSince1970)) } - datePickerNode.minimumDate = Date() + if let minDate = item.minDate { + datePickerNode.minimumDate = Date(timeIntervalSince1970: TimeInterval(minDate)) + } else { + datePickerNode.minimumDate = Date() + } + if let maxDate = item.maxDate { + datePickerNode.maximumDate = Date(timeIntervalSince1970: TimeInterval(maxDate)) + } + datePickerNode.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } let datePickerSize = CGSize(width: width, height: contentSize.height) diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index 01edc607de3..a056c15954d 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -226,13 +226,17 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo } if let previewImage = previewImage { - if let data = compressImageToJPEG(previewImage, quality: 0.7) { + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + if let data = compressImageToJPEG(previewImage, quality: 0.7, tempFilePath: tempFile.path) { context.account.postbox.mediaBox.storeCachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), data: data) } } let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil)]) - var message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + var message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index d6ac7a69e2a..3ea69b63a50 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -106,6 +106,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.currentIsVideo = isVideo } + public func activateInput() { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + view.activateInput() + } + } + public func dismissInput() -> Bool { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { if view.canDeactivateInput() { diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index e9ee252dc8f..b7111f6d384 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -233,14 +233,14 @@ public final class LottieComponent: Component { } } - public func playOnce(delay: Double = 0.0, completion: (() -> Void)? = nil) { + public func playOnce(delay: Double = 0.0, force: Bool = false, completion: (() -> Void)? = nil) { self.playOnceCompletion = completion guard let _ = self.animationInstance, let animationFrameRange = self.animationFrameRange else { self.scheduledPlayOnce = true return } - if !self.isEffectivelyVisible { + if !self.isEffectivelyVisible && !force { self.scheduledPlayOnce = true return } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index c4dbc9366cd..4713c45c55c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -36,6 +36,7 @@ import LocationUI import LegacyMediaPickerUI import ReactionSelectionNode import VolumeSliderContextItem +import TelegramStringFormatting enum DrawingScreenType { case drawing @@ -1016,7 +1017,9 @@ final class MediaEditorScreenComponent: Component { hasActiveGroupCall: false, importState: nil, threadData: nil, - isGeneralThreadClosed: nil + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil ) let availableInputMediaWidth = previewSize.width @@ -1959,9 +1962,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate animationCache: controller.context.animationCache, animationRenderer: controller.context.animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: false, - isEmojiSelection: true, + subject: .emoji, hasTrending: true, topReactionItems: [], areUnicodeEmojiEnabled: true, @@ -3065,16 +3066,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if !self.didSetupStaticEmojiPack { self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false)) } - - func flag(countryCode: String) -> String { - let base : UInt32 = 127397 - var flagString = "" - for v in countryCode.uppercased().unicodeScalars { - flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!) - } - return flagString - } - + var location: CLLocationCoordinate2D? if let subject = self.subject { if case let .asset(asset) = subject { @@ -3095,7 +3087,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { let emojiFile: Signal if let countryCode { - let flagEmoji = flag(countryCode: countryCode) + let flag = flagEmoji(countryCode: countryCode) emojiFile = self.staticEmojiPack.get() |> filter { result in if case .result = result { @@ -3114,7 +3106,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate break } } - if let displayText, displayText.hasPrefix(flagEmoji) { + if let displayText, displayText.hasPrefix(flag) { return true } else { return false @@ -3999,7 +3991,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate adminedChannels: self.adminedChannels.get(), blockedPeersContext: self.storiesBlockedPeers ) - let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in guard let self else { return } @@ -4257,7 +4249,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate content = .info( title: presentationData.strings.Story_Editor_TooltipReachedReactionLimitTitle, text: presentationData.strings.Story_Editor_TooltipReachedReactionLimitText(value).string, - timeout: nil + timeout: nil, + customUndoText: nil ) } else { let value = presentationData.strings.Story_Editor_TooltipPremiumReactionLimitValue(premiumLimits.maxStoriesSuggestedReactions) diff --git a/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift index 4b17e2b9aa8..e368bdfdc3d 100644 --- a/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputActionButtonComponent/Sources/MessageInputActionButtonComponent.swift @@ -449,6 +449,7 @@ public final class MessageInputActionButtonComponent: Component { fileId: animationFileId ?? reactionFile?.fileId.id ?? 0, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, + tintColor: nil, placeholderColor: UIColor(white: 1.0, alpha: 0.2), animateIdle: false, reaction: reaction, diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift index 4d85261323b..5bb42be0b11 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)), 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)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) index += 1 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index ea1d32e799e..b1b19c96058 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -117,7 +117,7 @@ final class PeerInfoStoryGridScreenComponent: Component { environment.controller()?.present(UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: text, timeout: nil), + content: .info(title: nil, text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false } @@ -361,7 +361,7 @@ final class PeerInfoStoryGridScreenComponent: Component { let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount)) environment.controller()?.present(UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: title, timeout: nil), + content: .info(title: nil, text: title, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false } @@ -384,7 +384,7 @@ final class PeerInfoStoryGridScreenComponent: Component { environment.controller()?.present(UndoOverlayController( presentationData: presentationData, - content: .info(title: title, text: text, timeout: nil), + content: .info(title: title, text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 9cd478df322..1e0b8d09b9a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1392,8 +1392,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, toggleMessagesSelection: { messageId, selected in chatControllerInteraction.toggleMessagesSelection(messageId, selected) }, - openUrl: { url, param1, param2, message in - chatControllerInteraction.openUrl(url, param1, param2, message) + openUrl: { url, concealed, external, message in + chatControllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed, external: external, message: message)) }, openInstantPage: { message, data in chatControllerInteraction.openInstantPage(message, data) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index fc4b7f9ffa8..c58a9ad81d5 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -124,7 +124,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) @@ -371,7 +371,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { let chatController = strongSelf.context.sharedContext.makeChatController( context: strongSelf.context, chatLocation: .peer(id: strongSelf.context.account.peerId), - subject: .forwardedMessages(peerIds: peerIds, ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], options: forwardOptions), + subject: .messageOptions(peerIds: peerIds, ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(previewing: true) ) @@ -545,6 +545,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { } contextController.immediateItemsTransitionAnimation = true strongSelf.controller?.presentInGlobalOverlay(contextController) + }, presentReplyOptions: { _ in + }, presentLinkOptions: { _ in }, shareSelectedMessages: { }, updateTextInputStateAndMode: { [weak self] f in if let strongSelf = self { @@ -684,7 +686,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { hasEntityKeyboard = true } - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, canSendWhenOnline: false, completion: { + let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, canSendWhenOnline: false, completion: { }, sendMessage: { [weak textInputPanelNode] mode in switch mode { case .generic: diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD new file mode 100644 index 00000000000..154b99fe701 --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PremiumGiftAttachmentScreen", + module_name = "PremiumGiftAttachmentScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/AttachmentUI", + "//submodules/PremiumUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift new file mode 100644 index 00000000000..fbcd865a8f6 --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift @@ -0,0 +1,58 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import PremiumUI +import AttachmentUI + +public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainable { + public var requestAttachmentMenuExpansion: () -> Void = {} + public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var cancelPanGesture: () -> Void = { } + public var isContainerPanning: () -> Bool = { return false } + public var isContainerExpanded: () -> Bool = { return false } + + public var mediaPickerContext: AttachmentMediaPickerContext? { + return PremiumGiftContext(controller: self) + } +} + +private final class PremiumGiftContext: AttachmentMediaPickerContext { + private weak var controller: PremiumGiftScreen? + + var selectionCount: Signal { + return .single(0) + } + + var caption: Signal { + return .single(nil) + } + + public var loadingProgress: Signal { + return .single(nil) + } + + public var mainButtonState: Signal { + return self.controller?.mainButtonStatePromise.get() ?? .single(nil) + } + + init(controller: PremiumGiftScreen) { + self.controller = controller + } + + func setCaption(_ caption: NSAttributedString) { + } + + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) { + } + + func schedule() { + } + + func mainButtonAction() { + self.controller?.mainButtonPressed() + } +} diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index 7eef2e59cb2..414a507ef34 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -510,7 +510,7 @@ private final class SendInviteLinkScreenComponent: Component { } else if let link = component.link { let selectedPeers = component.peers.filter { self.selectedItems.contains($0.id) } - let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() let text: String if selectedPeers.count == 1 { text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD new file mode 100644 index 00000000000..2dc1c20ba16 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerNameColorScreen", + module_name = "PeerNameColorScreen", + 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/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/UndoUI", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/EntityKeyboard", + "//submodules/SolidRoundedButtonNode", + "//submodules/AppBundle", + "//submodules/PremiumUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift new file mode 100644 index 00000000000..23e2352130f --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift @@ -0,0 +1,148 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import SolidRoundedButtonNode +import AppBundle + +final class ApplyColorFooterItem: ItemListControllerFooterItem { + let theme: PresentationTheme + let title: String + let locked: Bool + let inProgress: Bool + let action: () -> Void + + init(theme: PresentationTheme, title: String, locked: Bool, inProgress: Bool, action: @escaping () -> Void) { + self.theme = theme + self.title = title + self.locked = locked + self.inProgress = inProgress + self.action = action + } + + func isEqual(to: ItemListControllerFooterItem) -> Bool { + if let item = to as? ApplyColorFooterItem { + return self.theme === item.theme && self.title == item.title && self.locked == item.locked && self.inProgress == item.inProgress + } else { + return false + } + } + + func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + if let current = current as? ApplyColorFooterItemNode { + current.item = self + return current + } else { + return ApplyColorFooterItemNode(item: self) + } + } +} + +final class ApplyColorFooterItemNode: ItemListControllerFooterItemNode { + private let backgroundNode: NavigationBackgroundNode + private let separatorNode: ASDisplayNode + private let buttonNode: SolidRoundedButtonNode + + private var validLayout: ContainerViewLayout? + + var item: ApplyColorFooterItem { + didSet { + self.updateItem() + if let layout = self.validLayout { + let _ = self.updateLayout(layout: layout, transition: .immediate) + } + } + } + + init(item: ApplyColorFooterItem) { + self.item = item + + self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) + self.separatorNode = ASDisplayNode() + + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0) + self.buttonNode.icon = item.locked ? UIImage(bundleImageName: "Chat/Stickers/Lock") : nil + self.buttonNode.progressType = .embedded + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.updateItem() + } + + private var inProgress = false + private func updateItem() { + self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate) + self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor + + let backgroundColor = self.item.theme.list.itemCheckColors.fillColor + let textColor = self.item.theme.list.itemCheckColors.foregroundColor + + self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: backgroundColor, backgroundColors: [], foregroundColor: textColor), animated: true) + self.buttonNode.title = self.item.title + self.buttonNode.icon = self.item.locked ? UIImage(bundleImageName: "Chat/Stickers/Lock") : nil + + self.buttonNode.pressed = { [weak self] in + self?.item.action() + } + + if self.inProgress != self.item.inProgress { + self.inProgress = true + + if self.item.inProgress { + self.buttonNode.transitionToProgress() + } else { + self.buttonNode.transitionFromProgress() + } + } + } + + override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.backgroundNode, alpha: alpha) + transition.updateAlpha(node: self.separatorNode, alpha: alpha) + } + + override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = layout + + let buttonInset: CGFloat = 16.0 + let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0 + let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition) + let inset: CGFloat = 9.0 + + let insets = layout.insets(options: [.input]) + + var panelHeight: CGFloat = buttonHeight + inset * 2.0 + let totalPanelHeight: CGFloat + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + totalPanelHeight = panelHeight + insets.bottom + } else { + 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)) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: CGSize(width: buttonWidth, height: buttonHeight))) + + transition.updateFrame(node: self.backgroundNode, frame: panelFrame) + self.backgroundNode.update(size: panelFrame.size, transition: transition) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) + + return panelHeight + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.backgroundNode.frame.contains(point) { + return true + } else { + return false + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift new file mode 100644 index 00000000000..6ab20079ed4 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift @@ -0,0 +1,388 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import ComponentFlow +import ItemListUI +import TelegramPresentationData +import EntityKeyboard +import PagerComponent +import AccountContext + +final class EmojiPickerItem: ListViewItem, ItemListItem { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let emojiContent: EmojiPagerContentComponent + let backgroundIconColor: UIColor + let sectionId: ItemListSectionId + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + emojiContent: EmojiPagerContentComponent, + backgroundIconColor: UIColor, + sectionId: ItemListSectionId + ) { + self.context = context + self.theme = theme + self.strings = strings + self.emojiContent = emojiContent + self.backgroundIconColor = backgroundIconColor + 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) { + async { + let node = EmojiPickerItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? EmojiPickerItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +final class EmojiPickerItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let picker = ComponentView() + + private var item: EmojiPickerItem? + + private let disposable = MetaDisposable() + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + } + + deinit { + self.disposable.dispose() + } + + func asyncLayout() -> (_ item: EmojiPickerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let contentSize = CGSize(width: params.width, height: params.availableHeight - 452.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentSize.height - 20.0), insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let currentItem, currentItem.backgroundIconColor != item.backgroundIconColor { + 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) + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.25, removeOnCompletion: false, completion: { _ in + snapshot.removeFromSuperview() + }) + } + } + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + 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 + } + + let backgroundFrame = 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.backgroundNode.frame = backgroundFrame + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + 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)) + + let pickerSize = strongSelf.picker.update( + transition: .immediate, + component: AnyComponent( + EmojiSelectionComponent( + theme: item.theme, + strings: item.strings, + deviceMetrics: .iPhone14ProMax, + emojiContent: item.emojiContent, + backgroundIconColor: item.backgroundIconColor, + backgroundColor: item.theme.list.itemBlocksBackgroundColor, + separatorColor: item.theme.list.itemBlocksSeparatorColor + ) + ), + environment: {}, + containerSize: CGSize(width: params.width - params.leftInset - params.rightInset, height: contentSize.height) + ) + if let view = strongSelf.picker.view { + if view.superview == nil { + view.disablesInteractiveTransitionGestureRecognizer = true + strongSelf.view.insertSubview(view, at: 1) + } + view.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: pickerSize) + } + } + }) + } + } + + 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} + +private final class EmojiSelectionComponent: Component { + public typealias EnvironmentType = Empty + + public let theme: PresentationTheme + public let strings: PresentationStrings + public let deviceMetrics: DeviceMetrics + public let emojiContent: EmojiPagerContentComponent + public let backgroundIconColor: UIColor + public let backgroundColor: UIColor + public let separatorColor: UIColor + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + deviceMetrics: DeviceMetrics, + emojiContent: EmojiPagerContentComponent, + backgroundIconColor: UIColor, + backgroundColor: UIColor, + separatorColor: UIColor + ) { + self.theme = theme + self.strings = strings + self.deviceMetrics = deviceMetrics + self.emojiContent = emojiContent + self.backgroundIconColor = backgroundIconColor + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + } + + public static func ==(lhs: EmojiSelectionComponent, rhs: EmojiSelectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + if lhs.emojiContent != rhs.emojiContent { + return false + } + if lhs.backgroundIconColor != rhs.backgroundIconColor { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.separatorColor != rhs.separatorColor { + return false + } + return true + } + + public final class View: UIView { + private let keyboardView: ComponentView + private let keyboardClippingView: UIView + private let panelHostView: PagerExternalTopPanelContainer + private let panelBackgroundView: BlurredBackgroundView + private let panelSeparatorView: UIView + + private var component: EmojiSelectionComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.keyboardView = ComponentView() + self.keyboardClippingView = UIView() + self.panelHostView = PagerExternalTopPanelContainer() + self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.panelSeparatorView = UIView() + + super.init(frame: frame) + + self.addSubview(self.keyboardClippingView) + self.addSubview(self.panelBackgroundView) + self.addSubview(self.panelSeparatorView) + self.addSubview(self.panelHostView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func update(component: EmojiSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.backgroundColor = component.backgroundColor + let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) + self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) + self.panelSeparatorView.backgroundColor = component.separatorColor + + self.component = component + self.state = state + + let topPanelHeight: CGFloat = 42.0 + + let keyboardSize = self.keyboardView.update( + transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), + component: AnyComponent(EntityKeyboardComponent( + theme: component.theme, + strings: component.strings, + isContentInFocus: false, + 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, + stickerContent: nil, + maskContent: nil, + gifContent: nil, + hasRecentGifs: false, + availableGifSearchEmojies: [], + defaultToEmojiTab: true, + externalTopPanelContainer: self.panelHostView, + externalBottomPanelContainer: nil, + displayTopPanelBackground: .blur, + topPanelExtensionUpdated: { _, _ in }, + topPanelScrollingOffset: { _, _ in }, + hideInputUpdated: { _, _, _ in }, + hideTopPanelUpdated: { _, _ in }, + switchToTextInput: {}, + switchToGifSubject: { _ in }, + reorderItems: { _, _ in }, + makeSearchContainerNode: { _ in return nil }, + contentIdUpdated: { _ in }, + deviceMetrics: component.deviceMetrics, + hiddenInputHeight: 0.0, + inputHeight: 0.0, + displayBottomPanel: false, + isExpanded: true, + clipContentToTopPanel: false, + useExternalSearchContainer: false, + customTintColor: component.backgroundIconColor + )), + environment: {}, + containerSize: availableSize + ) + if let keyboardComponentView = self.keyboardView.view { + if keyboardComponentView.superview == nil { + self.keyboardClippingView.addSubview(keyboardComponentView) + } + + if panelBackgroundColor.alpha < 0.01 { + self.keyboardClippingView.clipsToBounds = true + } else { + self.keyboardClippingView.clipsToBounds = false + } + + transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight))) + + transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize)) + transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0))) + + transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight))) + self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) + transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift new file mode 100644 index 00000000000..a2fd9c8995c --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -0,0 +1,391 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WallpaperBackgroundNode + +final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem { + struct MessageItem: Equatable { + static func == (lhs: MessageItem, rhs: MessageItem) -> Bool { + if lhs.outgoing != rhs.outgoing { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.author != rhs.author { + return false + } + if lhs.photo != rhs.photo { + return false + } + if lhs.nameColor != rhs.nameColor { + return false + } + if lhs.backgroundEmojiId != rhs.backgroundEmojiId { + return false + } + if let lhsReply = lhs.reply, let rhsReply = rhs.reply, lhsReply.0 != rhsReply.0 || lhsReply.1 != rhsReply.1 { + return false + } else if (lhs.reply == nil) != (rhs.reply == nil) { + return false + } + if let lhsLinkPreview = lhs.linkPreview, let rhsLinkPreview = rhs.linkPreview, lhsLinkPreview.0 != rhsLinkPreview.0 || lhsLinkPreview.1 != rhsLinkPreview.1 || lhsLinkPreview.2 != rhsLinkPreview.2 { + return false + } else if (lhs.linkPreview == nil) != (rhs.linkPreview == nil) { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + let outgoing: Bool + let peerId: EnginePeer.Id + let author: String + let photo: [TelegramMediaImageRepresentation] + let nameColor: PeerNameColor + let backgroundEmojiId: Int64? + let reply: (String, String)? + let linkPreview: (String, String, String)? + let text: String + } + + let context: AccountContext + let theme: PresentationTheme + let componentTheme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners + let wallpaper: TelegramWallpaper + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let messageItems: [MessageItem] + + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [MessageItem]) { + self.context = context + self.theme = theme + self.componentTheme = componentTheme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners + self.wallpaper = wallpaper + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.messageItems = messageItems + } + + 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 = PeerNameColorChatPreviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? PeerNameColorChatPreviewItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +final class PeerNameColorChatPreviewItemNode: ListViewItemNode { + private var backgroundNode: WallpaperBackgroundNode? + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let containerNode: ASDisplayNode + private var messageNodes: [ListViewItemNode]? + private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:] + + private var item: PeerNameColorChatPreviewItem? + + private let disposable = MetaDisposable() + + init() { + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + self.addSubnode(self.containerNode) + } + + deinit { + self.disposable.dispose() + } + + func asyncLayout() -> (_ item: PeerNameColorChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentNodes = self.messageNodes + + var currentBackgroundNode = self.backgroundNode + + let currentItem = self.item + + return { item, params, neighbors in + if currentBackgroundNode == nil { + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + currentBackgroundNode?.update(wallpaper: item.wallpaper) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners) + } + + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)) + + var items: [ListViewItem] = [] + for messageItem in item.messageItems.reversed() { + let authorPeerId = messageItem.peerId + + var peers = SimpleDictionary() + var messages = SimpleDictionary() + + peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId) + + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + if let (_, text) = messageItem.reply { + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[authorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + } + + var media: [Media] = [] + if let (site, title, text) = messageItem.linkPreview { + media.append(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "", displayUrl: "", hash: 0, type: nil, websiteName: site, title: title, text: text, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))) + } + + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, isCentered: false)) + } + + var nodes: [ListViewItemNode] = [] + if let messageNodes = currentNodes { + nodes = 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: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + Queue.mainQueue().after(0.01) { + 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: false, 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!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + } + nodes = messageNodes + } + + var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) + for node in nodes { + contentSize.height += node.frame.size.height + } + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + let leftInset = params.leftInset + let rightInset = params.leftInset + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let currentBackgroundNode { + currentBackgroundNode.update(wallpaper: item.wallpaper) + currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + } + + 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 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) + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.25, removeOnCompletion: false, completion: { _ in + snapshot.removeFromSuperview() + }) + } + } + + strongSelf.messageNodes = nodes + var topOffset: CGFloat = 4.0 + for node in nodes { + if node.supernode == nil { + strongSelf.containerNode.addSubnode(node) + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize) + topOffset += node.frame.size.height + + if let header = node.headers()?.last { + let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: 7.0), size: CGSize(width: layoutSize.width, height: header.height)) + let stickLocationDistanceFactor: CGFloat = 0.0 + + let id = header.id + let headerNode: ListViewItemHeaderNode + if let current = strongSelf.itemHeaderNodes[id] { + headerNode = current + headerNode.updateFrame(headerFrame, within: layoutSize) + + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } else { + headerNode = header.node(synchronousLoad: false) + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.frame = headerFrame + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + strongSelf.itemHeaderNodes[id] = headerNode + + strongSelf.containerNode.addSubnode(headerNode) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } + } + } + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } + + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + 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 + + 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))) + + let displayMode: WallpaperDisplayMode + if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 { + displayMode = .halfAspectFill + } else { + if backgroundFrame.width > backgroundFrame.height * 4.0 { + if params.availableHeight < 700.0 { + displayMode = .halfAspectFill + } else { + displayMode = .aspectFill + } + } else { + displayMode = .aspectFill + } + } + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame + 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)) + } + }) + } + } + + 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift new file mode 100644 index 00000000000..edbd18161b3 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift @@ -0,0 +1,562 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import ItemListUI +import PresentationDataUtils +import AccountContext + +private enum PeerNameColorEntryId: Hashable { + case color(Int32) +} + +private enum PeerNameColorEntry: Comparable, Identifiable { + case color(Int, PeerNameColor, PeerNameColors.Colors, Bool) + + var stableId: PeerNameColorEntryId { + switch self { + case let .color(_, color, _, _): + return .color(color.rawValue) + } + } + + static func ==(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool { + switch lhs { + case let .color(lhsIndex, lhsColor, lhsAccentColor, lhsSelected): + if case let .color(rhsIndex, rhsColor, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsColor == rhsColor, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected { + return true + } else { + return false + } + } + } + + static func <(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool { + switch lhs { + case let .color(lhsIndex, _, _, _): + switch rhs { + case let .color(rhsIndex, _, _, _): + return lhsIndex < rhsIndex + } + } + } + + func item(action: @escaping (PeerNameColor) -> Void) -> ListViewItem { + switch self { + case let .color(_, index, colors, selected): + return PeerNameColorIconItem(index: index, colors: colors, selected: selected, action: action) + } + } +} + + +private class PeerNameColorIconItem: ListViewItem { + let index: PeerNameColor + let colors: PeerNameColors.Colors + let selected: Bool + let action: (PeerNameColor) -> Void + + public init(index: PeerNameColor, colors: PeerNameColors.Colors, selected: Bool, action: @escaping (PeerNameColor) -> Void) { + self.index = index + self.colors = colors + self.selected = selected + self.action = action + } + + 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 = PeerNameColorIconItemNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) + } + } + } + + 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 { + assert(node() is PeerNameColorIconItemNode) + if let nodeValue = node() as? PeerNameColorIconItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + let animated: Bool + if case .Crossfade = animation { + animated = true + } else { + animated = false + } + apply(animated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.index) + } +} + +private func generateRingImage(nameColor: PeerNameColors.Colors) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setStrokeColor(nameColor.main.cgColor) + context.setLineWidth(2.0) + context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) + }) +} + +private func generateFillImage(nameColor: PeerNameColors.Colors) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let circleBounds = bounds + context.addEllipse(in: circleBounds) + context.clip() + + if let secondColor = nameColor.secondary { + context.setFillColor(secondColor.cgColor) + context.fill(circleBounds) + + if let thirdColor = nameColor.tertiary { + context.move(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: size.width, y: size.height)) + context.addLine(to: CGPoint(x: 0.0, y: size.height)) + context.closePath() + context.setFillColor(nameColor.main.cgColor) + context.fillPath() + + context.setFillColor(thirdColor.cgColor) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: .pi / 4.0) + + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: -9.0, y: -9.0), size: CGSize(width: 18.0, height: 18.0)), cornerRadius: 4.0) + context.addPath(path.cgPath) + context.fillPath() + } else { + context.move(to: .zero) + context.addLine(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: 0.0, y: size.height)) + context.closePath() + context.setFillColor(nameColor.main.cgColor) + context.fillPath() + } + } else { + context.setFillColor(nameColor.main.cgColor) + context.fill(circleBounds) + } + }) +} + +private final class PeerNameColorIconItemNode : ListViewItemNode { + private let containerNode: ContextControllerSourceNode + private let fillNode: ASImageNode + private let ringNode: ASImageNode + + var item: PeerNameColorIconItem? + + init() { + self.containerNode = ContextControllerSourceNode() + + self.fillNode = ASImageNode() + self.fillNode.displaysAsynchronously = false + self.fillNode.displayWithoutProcessing = true + + self.ringNode = ASImageNode() + self.ringNode.displaysAsynchronously = false + self.ringNode.displayWithoutProcessing = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.ringNode) + self.containerNode.addSubnode(self.fillNode) + } + + override func didLoad() { + super.didLoad() + + self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } + + func setSelected(_ selected: Bool, animated: Bool = false) { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate + if selected { + transition.updateTransformScale(node: self.fillNode, scale: 0.8) + transition.updateTransformScale(node: self.ringNode, scale: 1.0) + } else { + transition.updateTransformScale(node: self.fillNode, scale: 1.0) + transition.updateTransformScale(node: self.ringNode, scale: 0.99) + } + } + + func asyncLayout() -> (PeerNameColorIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let currentItem = self.item + + return { [weak self] item, params in + var updatedAccentColor = false + var updatedSelected = false + + if currentItem == nil || currentItem?.colors != item.colors { + updatedAccentColor = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 56.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + if updatedAccentColor { + strongSelf.fillNode.image = generateFillImage(nameColor: item.colors) + strongSelf.ringNode.image = generateRingImage(nameColor: item.colors) + } + + let center = CGPoint(x: 30.0, y: 28.0) + let bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 40.0, height: 40.0)) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) + + strongSelf.fillNode.position = center + strongSelf.ringNode.position = center + + strongSelf.fillNode.bounds = bounds + strongSelf.ringNode.bounds = bounds + + if updatedSelected { + strongSelf.setSelected(item.selected, animated: !updatedAccentColor && currentItem != nil) + } + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +final class PeerNameColorItem: ListViewItem, ItemListItem { + var sectionId: ItemListSectionId + + let theme: PresentationTheme + let colors: PeerNameColors + let currentColor: PeerNameColor + let updated: (PeerNameColor) -> Void + let tag: ItemListItemTag? + + init(theme: PresentationTheme, colors: PeerNameColors, currentColor: PeerNameColor, updated: @escaping (PeerNameColor) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) { + self.theme = theme + self.colors = colors + self.currentColor = currentColor + self.updated = updated + self.tag = tag + 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) { + async { + let node = PeerNameColorItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? PeerNameColorItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +private struct PeerNameColorItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let updatePosition: Bool +} + +private func preparedTransition(action: @escaping (PeerNameColor) -> Void, from fromEntries: [PeerNameColorEntry], to toEntries: [PeerNameColorEntry], updatePosition: Bool) -> PeerNameColorItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: nil) } + + return PeerNameColorItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, updatePosition: updatePosition) +} + +private func ensureColorVisible(listNode: ListView, color: PeerNameColor, animated: Bool) -> Bool { + var resultNode: PeerNameColorIconItemNode? + listNode.forEachItemNode { node in + if resultNode == nil, let node = node as? PeerNameColorIconItemNode { + if node.item?.index == color { + resultNode = node + } + } + } + if let resultNode = resultNode { + listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 24.0) + return true + } else { + return false + } +} + +final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { + private let containerNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let listNode: ListView + private var entries: [PeerNameColorEntry]? + private var enqueuedTransitions: [PeerNameColorItemNodeTransition] = [] + private var initialized = false + + private var item: PeerNameColorItem? + private var layoutParams: ListViewItemLayoutParams? + + private var tapping = false + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.containerNode = ASDisplayNode() + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.listNode = ListView() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.containerNode) + self.addSubnode(self.listNode) + } + + override func didLoad() { + super.didLoad() + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true + } + + private func enqueueTransition(_ transition: PeerNameColorItemNodeTransition) { + self.enqueuedTransitions.append(transition) + + if let _ = self.item { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + guard let item = self.item, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + let options = ListViewDeleteAndInsertOptions() + var scrollToItem: ListViewScrollToItem? + if !self.initialized || transition.updatePosition || !self.tapping { + if let index = item.colors.displayOrder.firstIndex(where: { $0 == item.currentColor.rawValue }) { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-70.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + + func asyncLayout() -> (_ item: PeerNameColorItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: params.width, height: 60.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if themeUpdated { + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + } + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + 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 { + 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 = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + 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 + listInsets.bottom += params.rightInset + 8.0 + + strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) + strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0) + strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + var entries: [PeerNameColorEntry] = [] + + var i: Int = 0 + for index in item.colors.displayOrder { + let color = PeerNameColor(rawValue: index) + if let colors = item.colors.colors[index] { + entries.append(.color(i, color, colors, color == item.currentColor)) + } + i += 1 + } + + let action: (PeerNameColor) -> Void = { [weak self] color in + guard let self else { + return + } + self.tapping = true + item.updated(color) + Queue.mainQueue().after(0.4) { + self.tapping = false + } + let _ = ensureColorVisible(listNode: self.listNode, color: color, animated: true) + } + + let previousEntries = strongSelf.entries ?? [] + let updatePosition = currentItem != nil && previousEntries.count != entries.count + let transition = preparedTransition(action: action, from: previousEntries, to: entries, updatePosition: updatePosition) + strongSelf.enqueueTransition(transition) + + strongSelf.entries = entries + } + }) + } + } + + 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift new file mode 100644 index 00000000000..46dc8f2cf47 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -0,0 +1,642 @@ +import Foundation +import UIKit +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 + +private final class PeerNameColorScreenArguments { + let context: AccountContext + let updateNameColor: (PeerNameColor) -> Void + let updateBackgroundEmojiId: (Int64?) -> Void + + init( + context: AccountContext, + updateNameColor: @escaping (PeerNameColor) -> Void, + updateBackgroundEmojiId: @escaping (Int64?) -> Void + ) { + self.context = context + self.updateNameColor = updateNameColor + self.updateBackgroundEmojiId = updateBackgroundEmojiId + } +} + +private enum PeerNameColorScreenSection: Int32 { + case nameColor + case backgroundEmoji +} + +private enum PeerNameColorScreenEntry: ItemListNodeEntry { + enum StableId: Hashable { + case colorHeader + case colorMessage + case colorPicker + case colorDescription + case backgroundEmojiHeader + case backgroundEmoji + } + + case colorHeader(String) + case colorMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, items: [PeerNameColorChatPreviewItem.MessageItem]) + case colorPicker(colors: PeerNameColors, currentColor: PeerNameColor) + case colorDescription(String) + case backgroundEmojiHeader(String, String?) + case backgroundEmoji(EmojiPagerContentComponent, UIColor) + + var section: ItemListSectionId { + switch self { + case .colorHeader, .colorMessage, .colorPicker, .colorDescription: + return PeerNameColorScreenSection.nameColor.rawValue + case .backgroundEmojiHeader, .backgroundEmoji: + return PeerNameColorScreenSection.backgroundEmoji.rawValue + } + } + + var stableId: StableId { + switch self { + case .colorHeader: + return .colorHeader + case .colorMessage: + return .colorMessage + case .colorPicker: + return .colorPicker + case .colorDescription: + return .colorDescription + case .backgroundEmojiHeader: + return .backgroundEmojiHeader + case .backgroundEmoji: + return .backgroundEmoji + } + } + + var sortId: Int { + switch self { + case .colorHeader: + return 0 + case .colorMessage: + return 1 + case .colorPicker: + return 2 + case .colorDescription: + return 3 + case .backgroundEmojiHeader: + return 4 + case .backgroundEmoji: + return 5 + } + } + + static func ==(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool { + switch lhs { + case let .colorHeader(text): + if case .colorHeader(text) = rhs { + return true + } else { + return false + } + case let .colorMessage(lhsWallpaper, lhsFontSize, lhsBubbleCorners, lhsDateTimeFormat, lhsNameDisplayOrder, lhsItems): + if case let .colorMessage(rhsWallpaper, rhsFontSize, rhsBubbleCorners, rhsDateTimeFormat, rhsNameDisplayOrder, rhsItems) = rhs, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsBubbleCorners == rhsBubbleCorners, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsItems == rhsItems { + return true + } else { + return false + } + case let .colorPicker(lhsColors, lhsCurrentColor): + if case let .colorPicker(rhsColors, rhsCurrentColor) = rhs, lhsColors == rhsColors, lhsCurrentColor == rhsCurrentColor { + return true + } else { + return false + } + case let .colorDescription(text): + if case .colorDescription(text) = rhs { + return true + } else { + return false + } + case let .backgroundEmojiHeader(text, action): + if case .backgroundEmojiHeader(text, action) = rhs { + return true + } else { + return false + } + case let .backgroundEmoji(lhsEmojiContent, lhsBackgroundIconColor): + if case let .backgroundEmoji(rhsEmojiContent, rhsBackgroundIconColor) = rhs, lhsEmojiContent == rhsEmojiContent, lhsBackgroundIconColor == rhsBackgroundIconColor { + return true + } else { + return false + } + } + } + + static func <(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! PeerNameColorScreenArguments + switch self { + case let .colorHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .colorMessage(wallpaper, fontSize, chatBubbleCorners, dateTimeFormat, nameDisplayOrder, items): + return PeerNameColorChatPreviewItem( + context: arguments.context, + theme: presentationData.theme, + componentTheme: presentationData.theme, + strings: presentationData.strings, + sectionId: self.section, + fontSize: fontSize, + chatBubbleCorners: chatBubbleCorners, + wallpaper: wallpaper, + dateTimeFormat: dateTimeFormat, + nameDisplayOrder: nameDisplayOrder, + messageItems: items) + case let .colorPicker(colors, currentColor): + return PeerNameColorItem( + theme: presentationData.theme, + colors: colors, + currentColor: currentColor, + updated: { color in + arguments.updateNameColor(color) + }, + sectionId: self.section + ) + case let .colorDescription(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .backgroundEmojiHeader(text, action): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, actionText: action, action: action != nil ? { + arguments.updateBackgroundEmojiId(0) + } : nil, sectionId: self.section) + case let .backgroundEmoji(emojiContent, backgroundIconColor): + return EmojiPickerItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, emojiContent: emojiContent, backgroundIconColor: backgroundIconColor, sectionId: self.section) + } + } +} + +private struct PeerNameColorScreenState: Equatable { + var updatedNameColor: PeerNameColor? + var updatedBackgroundEmojiId: Int64? + var inProgress: Bool = false +} + +private func peerNameColorScreenEntries( + nameColors: PeerNameColors, + presentationData: PresentationData, + state: PeerNameColorScreenState, + peer: EnginePeer?, + isPremium: Bool, + emojiContent: EmojiPagerContentComponent? +) -> [PeerNameColorScreenEntry] { + var entries: [PeerNameColorScreenEntry] = [] + + if let peer { + let nameColor: PeerNameColor + if let updatedNameColor = state.updatedNameColor { + nameColor = updatedNameColor + } else if let peerNameColor = peer.nameColor { + nameColor = peerNameColor + } else { + nameColor = .blue + } + + let colors = nameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance) + + let backgroundEmojiId: Int64? + if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId { + if updatedBackgroundEmojiId == 0 { + backgroundEmojiId = nil + } else { + backgroundEmojiId = updatedBackgroundEmojiId + } + } else if let emojiId = peer.backgroundEmojiId { + backgroundEmojiId = emojiId + } else { + backgroundEmojiId = nil + } + + let replyText: String + let messageText: String + if case .channel = peer { + replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Channel + messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Channel + } else { + replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Account + messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Account + } + let messageItem = PeerNameColorChatPreviewItem.MessageItem( + outgoing: false, + peerId: peer.id, + author: peer.compactDisplayTitle, + photo: peer.profileImageRepresentations, + nameColor: nameColor, + backgroundEmojiId: backgroundEmojiId, + reply: (peer.compactDisplayTitle, replyText), + linkPreview: (presentationData.strings.NameColor_ChatPreview_LinkSite, presentationData.strings.NameColor_ChatPreview_LinkTitle, presentationData.strings.NameColor_ChatPreview_LinkText), + text: messageText + ) + entries.append(.colorMessage( + wallpaper: presentationData.chatWallpaper, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + items: [messageItem] + )) + entries.append(.colorPicker( + colors: nameColors, + currentColor: nameColor + )) + entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Account)) + + if let emojiContent { + entries.append(.backgroundEmojiHeader(presentationData.strings.NameColor_BackgroundEmoji_Title, backgroundEmojiId != nil ? presentationData.strings.NameColor_BackgroundEmoji_Remove : nil)) + entries.append(.backgroundEmoji(emojiContent, colors.main)) + } + } + + return entries +} + +public enum PeerNameColorScreenSubject { + case account + case channel(EnginePeer.Id) +} + +public func PeerNameColorScreen( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + subject: PeerNameColorScreenSubject +) -> ViewController { + let statePromise = ValuePromise(PeerNameColorScreenState(), ignoreRepeated: true) + let stateValue = Atomic(value: PeerNameColorScreenState()) + let updateState: ((PeerNameColorScreenState) -> PeerNameColorScreenState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var presentImpl: ((ViewController) -> Void)? + var pushImpl: ((ViewController) -> Void)? + var dismissImpl: (() -> Void)? + var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)? + var applyChangesImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let arguments = PeerNameColorScreenArguments( + context: context, + updateNameColor: { color in + updateState { state in + var updatedState = state + updatedState.updatedNameColor = color + return updatedState + } + }, + updateBackgroundEmojiId: { emojiId in + updateState { state in + var updatedState = state + updatedState.updatedBackgroundEmojiId = emojiId + return updatedState + } + } + ) + + let peerId: EnginePeer.Id + switch subject { + case .account: + peerId = context.account.peerId + case let .channel(channelId): + peerId = channelId + } + + let emojiContent = combineLatest( + context.sharedContext.presentationData, + statePromise.get(), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + ) + |> mapToSignal { presentationData, state, peer -> Signal in + var selectedEmojiId: Int64? + if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId { + selectedEmojiId = updatedBackgroundEmojiId + } else { + selectedEmojiId = peer?.backgroundEmojiId + } + let nameColor: PeerNameColor + if let updatedNameColor = state.updatedNameColor { + nameColor = updatedNameColor + } else { + nameColor = (peer?.nameColor ?? .blue) + } + let color = context.peerNameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance) + + let selectedItems: [EngineMedia.Id] + if let selectedEmojiId, selectedEmojiId != 0 { + selectedItems = [EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: selectedEmojiId)] + } else { + selectedItems = [] + } + + return EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + isStandalone: false, + subject: .backgroundIcon, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: context.account.peerId, + selectedItems: Set(selectedItems), + backgroundIconColor: color.main + ) + } + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get(), + context.engine.stickers.availableReactions(), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), + emojiContent + ) + |> deliverOnMainQueue + |> map { presentationData, state, availableReactions, peer, emojiContent -> (ItemListControllerState, (ItemListNodeState, Any)) in + let isPremium = peer?.isPremium ?? false + let title: String + let buttonTitle: String + let isLocked: Bool + switch subject { + case .account: + title = presentationData.strings.NameColor_Title_Account + isLocked = !isPremium + case .channel: + title = presentationData.strings.NameColor_Title_Channel + isLocked = false + } + + let backgroundEmojiId: Int64 + if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId { + backgroundEmojiId = updatedBackgroundEmojiId + } else if let emojiId = peer?.backgroundEmojiId { + backgroundEmojiId = emojiId + } else { + backgroundEmojiId = 0 + } + if backgroundEmojiId != 0 { + buttonTitle = presentationData.strings.NameColor_ApplyColorAndBackgroundEmoji + } else { + buttonTitle = presentationData.strings.NameColor_ApplyColor + } + + let footerItem = ApplyColorFooterItem( + theme: presentationData.theme, + title: buttonTitle, + locked: isLocked, + inProgress: state.inProgress, + action: { + if !isLocked { + applyChangesImpl?() + } else { + HapticFeedback().impact(.light) + let controller = UndoOverlayController( + presentationData: presentationData, + content: .premiumPaywall( + title: nil, + text: presentationData.strings.NameColor_TooltipPremium_Account, + customUndoText: nil, + timeout: nil, + linkAction: nil + ), + elevatedLayout: false, + action: { action in + if case .info = action { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesSuggestedReactions, forceDark: false, dismissed: nil) + pushImpl?(controller) + } + return true + } + ) + presentImpl?(controller) + } + } + ) + + emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { _, item, _, _, _, _ in + var selectedFileId: Int64? + if let fileId = item.itemFile?.fileId.id { + selectedFileId = fileId + } else { + selectedFileId = 0 + } + arguments.updateBackgroundEmojiId(selectedFileId) + }, + deleteBackwards: { + }, + openStickerSettings: { + }, + openFeatured: { + }, + openSearch: { + }, + addGroupAction: { groupId, isPremiumLocked, _ in + guard let collectionId = groupId.base as? ItemCollectionId else { + return + } + + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredEmojiPack.info.id == collectionId { + let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() + + break + } + } + }) + }, + clearGroup: { _ in + }, + pushController: { c in + }, + presentController: { c in + }, + presentGlobalOverlayController: { c in + }, + navigationController: { + return nil + }, + requestUpdate: { _ in + }, + updateSearchQuery: { _ in + }, + updateScrollingToItemGroup: { + }, + onScroll: {}, + chatPeerId: nil, + peekBehavior: nil, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + customContentView: nil, + useOpaqueTheme: true, + hideBackground: false, + stateContext: nil, + addImage: nil + ) + + let entries = peerNameColorScreenEntries( + nameColors: context.peerNameColors, + presentationData: presentationData, + state: state, + peer: peer, + isPremium: isPremium, + emojiContent: emojiContent + ) + + let controllerState = ItemListControllerState( + presentationData: ItemListPresentationData(presentationData), + title: .text(title), + leftNavigationButton: nil, + rightNavigationButton: nil, + backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), + animateChanges: false + ) + let listState = ItemListNodeState( + presentationData: ItemListPresentationData(presentationData), + entries: entries, + style: .blocks, + footerItem: footerItem, + animateChanges: false + ) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + presentImpl = { [weak controller] c in + guard let controller else { + return + } + controller.present(c, in: .current) + } + pushImpl = { [weak controller] c in + guard let controller else { + return + } + controller.push(c) + } + dismissImpl = { [weak controller] in + guard let controller else { + return + } + controller.dismiss() + } + controller.attemptNavigation = { f in + return attemptNavigationImpl?(f) ?? true + } + attemptNavigationImpl = { f in + if !context.isPremium { + f() + return true + } + let state = stateValue.with({ $0 }) + var hasChanges = false + if state.updatedNameColor != nil || state.updatedBackgroundEmojiId != nil { + hasChanges = true + } + if hasChanges { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.NameColor_UnsavedChanges_Title, text: presentationData.strings.NameColor_UnsavedChanges_Text, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.NameColor_UnsavedChanges_Discard, action: { + f() + dismissImpl?() + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.NameColor_UnsavedChanges_Apply, action: { + applyChangesImpl?() + }) + ])) + return false + } else { + f() + return true + } + } + applyChangesImpl = { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer else { + return + } + let state = stateValue.with { $0 } + + let nameColor = state.updatedNameColor ?? peer.nameColor + let backgroundEmojiId = state.updatedBackgroundEmojiId ?? peer.backgroundEmojiId + + switch subject { + case .account: + let _ = context.engine.accountData.updateNameColorAndEmoji(nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0).startStandalone() + dismissImpl?() + case let .channel(peerId): + updateState { state in + var updatedState = state + updatedState.inProgress = true + return updatedState + } + let _ = (context.engine.peers.updatePeerNameColorAndEmoji(peerId: peerId, nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0) + |> deliverOnMainQueue).startStandalone(next: { + }, error: { error in + if case .channelBoostRequired = error { + let _ = combineLatest( + queue: Queue.mainQueue(), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), + context.engine.peers.getChannelBoostStatus(peerId: peerId) + ).startStandalone(next: { peer, status in + guard let peer, let status else { + return + } + + 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: { + 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 })) + return true + }, openStats: nil, openGift: { + let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) + pushImpl?(controller) + }) + pushImpl?(controller) + + HapticFeedback().impact(.light) + }) + } else { + + } + updateState { state in + var updatedState = state + updatedState.inProgress = false + return updatedState + } + }, completed: { + dismissImpl?() + }) + } + }) + } + return controller +} diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD index b3ca3713588..8a0c3ecf8dc 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD +++ b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD @@ -38,6 +38,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods", "//submodules/TelegramUI/Components/PeerSelectionController", + "//submodules/TelegramUI/Components/ContextMenuScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift index 09397ebaedc..6b41c597c8a 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift @@ -30,6 +30,7 @@ import MultiAnimationRenderer import TelegramUIDeclareEncodables import TelegramAccountAuxiliaryMethods import PeerSelectionController +import ContextMenuScreen private var installedSharedLogger = false @@ -129,8 +130,9 @@ public struct ShareRootControllerInitializationData { public let appVersion: String public let bundleData: Data? public let useBetaFeatures: Bool + public let makeTempContext: (AccountManager, AppLockContext, TelegramApplicationBindings, InitialPresentationDataAndSettings, NetworkInitializationArguments) -> Signal - public init(appBundleId: String, appBuildType: TelegramAppBuildType, appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?, useBetaFeatures: Bool) { + public init(appBundleId: String, appBuildType: TelegramAppBuildType, appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?, useBetaFeatures: Bool, makeTempContext: @escaping (AccountManager, AppLockContext, TelegramApplicationBindings, InitialPresentationDataAndSettings, NetworkInitializationArguments) -> Signal) { self.appBundleId = appBundleId self.appBuildType = appBuildType self.appGroupPath = appGroupPath @@ -141,6 +143,7 @@ public struct ShareRootControllerInitializationData { self.appVersion = appVersion self.bundleData = bundleData self.useBetaFeatures = useBetaFeatures + self.makeTempContext = makeTempContext } } @@ -353,6 +356,21 @@ public class ShareRootControllerImpl { let environment = ShareControllerEnvironmentExtension(presentationData: presentationData) let initializationData = self.initializationData + let networkArguments = NetworkInitializationArguments( + apiId: initializationData.apiId, + apiHash: initializationData.apiHash, + languagesCategory: initializationData.languagesCategory, + appVersion: initializationData.appVersion, + voipMaxLayer: 0, + voipVersions: [], + appData: .single(nil), + autolockDeadine: .single(nil), + encryptionProvider: OpenSSLEncryptionProvider(), + deviceModelName: nil, + useBetaFeatures: initializationData.useBetaFeatures, + isICloudEnabled: false + ) + let accountData: Signal<(ShareControllerEnvironment, ShareControllerAccountContext, [ShareControllerSwitchableAccount]), NoError> = accountManager.accountRecords() |> take(1) |> mapToSignal { view -> Signal<(ShareControllerEnvironment, ShareControllerAccountContext, [ShareControllerSwitchableAccount]), NoError> in @@ -368,21 +386,6 @@ public class ShareRootControllerImpl { continue } - let networkArguments = NetworkInitializationArguments( - apiId: initializationData.apiId, - apiHash: initializationData.apiHash, - languagesCategory: initializationData.languagesCategory, - appVersion: initializationData.appVersion, - voipMaxLayer: 0, - voipVersions: [], - appData: .single(nil), - autolockDeadine: .single(nil), - encryptionProvider: OpenSSLEncryptionProvider(), - deviceModelName: nil, - useBetaFeatures: initializationData.useBetaFeatures, - isICloudEnabled: false - ) - signals.append(standaloneStateManager( accountManager: accountManager, networkArguments: networkArguments, @@ -462,6 +465,9 @@ public class ShareRootControllerImpl { (environment as? ShareControllerEnvironmentExtension)?.accounts = otherAccounts.compactMap { $0.account as? ShareControllerAccountContextExtension } initializeLegacyComponents(application: nil, currentSizeClassGetter: { return .compact }, currentHorizontalClassGetter: { return .compact }, documentsPath: "", currentApplicationBounds: { return CGRect() }, canOpenUrl: { _ in return false}, openUrl: { _ in }) + setContextMenuControllerProvider { arguments in + return ContextMenuControllerImpl(arguments) + } let displayShare: () -> Void = { var cancelImpl: (() -> Void)? @@ -598,12 +604,9 @@ public class ShareRootControllerImpl { strongSelf.currentShareController = shareController strongSelf.mainWindow?.present(shareController, on: .root) } - - //TODO - //context.account.resetStateManagement() } - if !"".isEmpty {/* if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, inputItems.count == 1, let item = inputItems[0] as? NSExtensionItem, let attachments = item.attachments { + if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, inputItems.count == 1, let item = inputItems[0] as? NSExtensionItem, let attachments = item.attachments { for attachment in attachments { if attachment.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) { attachment.loadItem(forTypeIdentifier: kUTTypeFileURL as String, completionHandler: { result, error in @@ -619,28 +622,12 @@ public class ShareRootControllerImpl { let fileExtension = (fileName as NSString).pathExtension var archivePathValue: String? + let _ = archivePathValue var otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)] = [] var mainFile: TempBoxFile? let appConfiguration = context.appConfiguration - /* - history_import_filters: { - "zip": { - "main_file_patterns": [ - "_chat\\.txt", - "KakaoTalkChats\\.txt", - "Talk_.*?\\.txt" - ] - }, - "txt": { - "patterns": [ - "^\\[LINE\\]" - ] - } - } - */ - if fileExtension.lowercased() == "zip" { let archivePath = url.path archivePathValue = archivePath @@ -766,489 +753,37 @@ public class ShareRootControllerImpl { } } - if let mainFile = mainFile, let mainFileHeader = extractTextFileHeader(path :mainFile.path) { - final class TempController: ViewController { - override public var _presentedInModal: Bool { - get { - return true - } set(value) { - } - } - - private let activityIndicator: ActivityIndicator - - init(environment: ShareControllerEnvironment) { - let presentationData = environment.presentationData - - self.activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.list.itemAccentColor, 22.0, 1.0, false)) - - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) - - self.title = presentationData.strings.ChatImport_Title - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func cancelPressed() { - //self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - } - - override func displayNodeDidLoad() { - super.displayNodeDidLoad() - - self.displayNode.addSubnode(self.activityIndicator) - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) - let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY - transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: navigationHeight + floor((layout.size.height - navigationHeight - indicatorSize.height) / 2.0)), size: indicatorSize)) - } - } + if let mainFile = mainFile, let mainFileHeader = extractTextFileHeader(path: mainFile.path) { + let _ = mainFileHeader let presentationData = environment.presentationData let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) strongSelf.navigationController = navigationController - navigationController.viewControllers = [TempController(environment: environment)] + navigationController.viewControllers = [ChatImportTempController(presentationData: environment.presentationData)] strongSelf.mainWindow?.present(navigationController, on: .root) - let _ = (TelegramEngine.HistoryImport(postbox: context.stateManager.postbox, network: context.stateManager.network).getInfo(header: mainFileHeader) - |> deliverOnMainQueue).start(next: { parseInfo in - switch parseInfo { - case let .group(groupTitle): - var attemptSelectionImpl: ((EnginePeer) -> Void)? - var createNewGroupImpl: (() -> Void)? - let controller = PeerSelectionControllerImpl(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled, .doNotSearchMessages], hasContactSelector: false, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in - attemptSelectionImpl?(peer) - }, createNewGroup: { - createNewGroupImpl?() - }, pretendPresentedInModal: true, selectForumThreads: false)) - - controller.customDismiss = { - //inForeground.set(false) - self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - } - - controller.peerSelected = { peer, _ in - attemptSelectionImpl?(peer) - } - - controller.navigationPresentation = .default - - let beginWithPeer: (PeerId) -> Void = { peerId in - navigationController.view.endEditing(true) - navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: { - //inForeground.set(false) - self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - }, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries)) - } - - attemptSelectionImpl = { peer in - var errorText: String? - if case let .channel(channel) = peer { - if channel.hasPermission(.changeInfo), (channel.flags.contains(.isCreator) || channel.adminRights != nil) { - } else { - errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin - } - } else if case let .legacyGroup(group) = peer { - switch group.role { - case .creator: - break - default: - errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin - } - } else { - errorText = presentationData.strings.ChatImport_SelectionErrorGroupGeneric - } - - if let errorText = errorText { - let presentationData = environment.presentationData - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]) - strongSelf.mainWindow?.present(controller, on: .root) - } else { - controller.inProgress = true - let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) - |> deliverOnMainQueue).start(next: { result in - controller.inProgress = false - - let presentationData = environment.presentationData - - var errorText: String? - if case let .channel(channel) = peer { - if channel.hasPermission(.changeInfo), (channel.flags.contains(.isCreator) || channel.adminRights != nil) { - } else { - errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin - } - } else if case let .legacyGroup(group) = peer { - switch group.role { - case .creator: - break - default: - errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin - } - } else if case .user = peer { - } else { - errorText = presentationData.strings.ChatImport_SelectionErrorGroupGeneric - } - - if let errorText = errorText { - let presentationData = environment.presentationData - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]) - strongSelf.mainWindow?.present(controller, on: .root) - } else { - let presentationData = environment.presentationData - let text: String - switch result { - case .allowed: - if let groupTitle = groupTitle { - text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithTitle(groupTitle, peer.debugDisplayTitle).string - } else { - text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithoutTitle(peer.debugDisplayTitle).string - } - case let .alert(textValue): - text = textValue - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { - beginWithPeer(peer.id) - })], parseMarkdown: true) - strongSelf.mainWindow?.present(controller, on: .root) - } - }, error: { error in - controller.inProgress = false - - let presentationData = environment.presentationData - let errorText: String - switch error { - case .generic: - errorText = presentationData.strings.Login_UnknownError - case .chatAdminRequired: - errorText = presentationData.strings.ChatImportActivity_ErrorNotAdmin - case .invalidChatType: - errorText = presentationData.strings.ChatImportActivity_ErrorInvalidChatType - case .userBlocked: - errorText = presentationData.strings.ChatImportActivity_ErrorUserBlocked - case .limitExceeded: - errorText = presentationData.strings.ChatImportActivity_ErrorLimitExceeded - case .notMutualContact: - errorText = presentationData.strings.ChatImport_UserErrorNotMutual - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]) - strongSelf.mainWindow?.present(controller, on: .root) - }) - } - } - - createNewGroupImpl = { - let presentationData = environment.presentationData - let resolvedGroupTitle: String - if let groupTitle = groupTitle { - resolvedGroupTitle = groupTitle - } else { - resolvedGroupTitle = "Group" - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_CreateGroupAlertTitle, text: presentationData.strings.ChatImport_CreateGroupAlertText(resolvedGroupTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_CreateGroupAlertImportAction, action: { - var signal: Signal = _internal_createSupergroup(postbox: context.stateManager.postbox, network: context.stateManager.network, stateManager: context.stateManager, title: resolvedGroupTitle, description: nil, username: nil, isForum: false, isForHistoryImport: true) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - if let strongSelf = self { - strongSelf.mainWindow?.present(controller, on: .root) - } - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - signal = signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - let _ = (signal - |> deliverOnMainQueue).start(next: { peerId in - if let peerId = peerId { - beginWithPeer(peerId) - } else { - } - }) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - })], actionLayout: .vertical, parseMarkdown: true) - strongSelf.mainWindow?.present(controller, on: .root) - } - - navigationController.viewControllers = [controller] - case let .privateChat(title): - let presentationData = environment.presentationData - - var attemptSelectionImpl: ((EnginePeer) -> Void)? - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeDisabled, .doNotSearchMessages, .excludeSecretChats], hasChatListSelector: false, hasContactSelector: true, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in - attemptSelectionImpl?(peer) - }, pretendPresentedInModal: true, selectForumThreads: true)) - - controller.customDismiss = { - //inForeground.set(false) - self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - } - - controller.peerSelected = { peer, _ in - attemptSelectionImpl?(peer) - } - - controller.navigationPresentation = .default - - let beginWithPeer: (PeerId) -> Void = { peerId in - navigationController.view.endEditing(true) - navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: { - //inForeground.set(false) - self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - }, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries)) - } - - attemptSelectionImpl = { [weak controller] peer in - controller?.inProgress = true - let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) - |> deliverOnMainQueue).start(next: { result in - controller?.inProgress = false - - let presentationData = environment.presentationData - let text: String - switch result { - case .allowed: - if let title = title { - text = presentationData.strings.ChatImport_SelectionConfirmationUserWithTitle(title, peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string - } else { - text = presentationData.strings.ChatImport_SelectionConfirmationUserWithoutTitle(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string - } - case let .alert(textValue): - text = textValue - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { - beginWithPeer(peer.id) - })], parseMarkdown: true) - strongSelf.mainWindow?.present(controller, on: .root) - }, error: { error in - controller?.inProgress = false - - let presentationData = environment.presentationData - let errorText: String - switch error { - case .generic: - errorText = presentationData.strings.Login_UnknownError - case .chatAdminRequired: - errorText = presentationData.strings.ChatImportActivity_ErrorNotAdmin - case .invalidChatType: - errorText = presentationData.strings.ChatImportActivity_ErrorInvalidChatType - case .userBlocked: - errorText = presentationData.strings.ChatImportActivity_ErrorUserBlocked - case .limitExceeded: - errorText = presentationData.strings.ChatImportActivity_ErrorLimitExceeded - case .notMutualContact: - errorText = presentationData.strings.ChatImport_UserErrorNotMutual - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]) - strongSelf.mainWindow?.present(controller, on: .root) - }) - } - - navigationController.viewControllers = [controller] - case let .unknown(peerTitle): - var attemptSelectionImpl: ((EnginePeer) -> Void)? - var createNewGroupImpl: (() -> Void)? - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.excludeDisabled, .doNotSearchMessages], hasContactSelector: true, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in - attemptSelectionImpl?(peer) - }, createNewGroup: { - createNewGroupImpl?() - }, pretendPresentedInModal: true, selectForumThreads: true)) - - controller.customDismiss = { - //inForeground.set(false) - self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - } - - controller.peerSelected = { peer, _ in - attemptSelectionImpl?(peer) - } - - controller.navigationPresentation = .default - - let beginWithPeer: (EnginePeer.Id) -> Void = { peerId in - navigationController.view.endEditing(true) - navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: { - //inForeground.set(false) - self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - }, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries)) - } - - attemptSelectionImpl = { [weak controller] peer in - controller?.inProgress = true - let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) - |> deliverOnMainQueue).start(next: { result in - controller?.inProgress = false - - let presentationData = environment.presentationData - - var errorText: String? - if case let .channel(channel) = peer { - if channel.hasPermission(.changeInfo), (channel.flags.contains(.isCreator) || channel.adminRights != nil) { - } else { - errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin - } - } else if case let .legacyGroup(group) = peer { - switch group.role { - case .creator: - break - default: - errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin - } - } else if case .user = peer { - } else { - errorText = presentationData.strings.ChatImport_SelectionErrorGroupGeneric - } - - if let errorText = errorText { - let presentationData = environment.presentationData - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]) - strongSelf.mainWindow?.present(controller, on: .root) - } else { - let presentationData = environment.presentationData - if case .user = peer { - let text: String - switch result { - case .allowed: - if let title = peerTitle { - text = presentationData.strings.ChatImport_SelectionConfirmationUserWithTitle(title, peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string - } else { - text = presentationData.strings.ChatImport_SelectionConfirmationUserWithoutTitle(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string - } - case let .alert(textValue): - text = textValue - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { - beginWithPeer(peer.id) - })], parseMarkdown: true) - strongSelf.mainWindow?.present(controller, on: .root) - } else { - let text: String - switch result { - case .allowed: - if let groupTitle = peerTitle { - text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithTitle(groupTitle, peer.debugDisplayTitle).string - } else { - text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithoutTitle(peer.debugDisplayTitle).string - } - case let .alert(textValue): - text = textValue - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { - beginWithPeer(peer.id) - })], parseMarkdown: true) - strongSelf.mainWindow?.present(controller, on: .root) - } - } - }, error: { error in - controller?.inProgress = false - - let presentationData = environment.presentationData - let errorText: String - switch error { - case .generic: - errorText = presentationData.strings.Login_UnknownError - case .chatAdminRequired: - errorText = presentationData.strings.ChatImportActivity_ErrorNotAdmin - case .invalidChatType: - errorText = presentationData.strings.ChatImportActivity_ErrorInvalidChatType - case .userBlocked: - errorText = presentationData.strings.ChatImportActivity_ErrorUserBlocked - case .limitExceeded: - errorText = presentationData.strings.ChatImportActivity_ErrorLimitExceeded - case .notMutualContact: - errorText = presentationData.strings.ChatImport_UserErrorNotMutual - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]) - strongSelf.mainWindow?.present(controller, on: .root) - }) - } - - createNewGroupImpl = { - let presentationData = environment.presentationData - let resolvedGroupTitle: String - if let groupTitle = peerTitle { - resolvedGroupTitle = groupTitle - } else { - resolvedGroupTitle = "Group" - } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_CreateGroupAlertTitle, text: presentationData.strings.ChatImport_CreateGroupAlertText(resolvedGroupTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_CreateGroupAlertImportAction, action: { - var signal: Signal = _internal_createSupergroup(postbox: context.stateManager.postbox, network: context.stateManager.network, stateManager: context.stateManager, title: resolvedGroupTitle, description: nil, username: nil, isForum: false, isForHistoryImport: true) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - if let strongSelf = self { - strongSelf.mainWindow?.present(controller, on: .root) - } - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - signal = signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - let _ = (signal - |> deliverOnMainQueue).start(next: { peerId in - if let peerId = peerId { - beginWithPeer(peerId) - } else { - } - }) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - })], actionLayout: .vertical, parseMarkdown: true) - strongSelf.mainWindow?.present(controller, on: .root) - } - - navigationController.viewControllers = [controller] - } - }, error: { _ in + if let mainWindow = strongSelf.mainWindow { + attemptChatImport( + context: context, + getExtensionContext: strongSelf.getExtensionContext, + accountManager: accountManager, + appLockContext: appLockContext, + applicationBindings: applicationBindings, + initialPresentationDataAndSettings: initialPresentationDataAndSettings!, + networkInitializationArguments: networkArguments, + presentationData: environment.presentationData, + makeTempContext: initializationData.makeTempContext, + mainWindow: mainWindow, + navigationController: navigationController, + archivePathValue: archivePathValue, + mainFileHeader: mainFileHeader, + mainFile: mainFile, + otherEntries: otherEntries, + beginShare: beginShare + ) + } else { beginShare() - }) + } } else { beginShare() return @@ -1258,7 +793,7 @@ public class ShareRootControllerImpl { return } } - beginShare()*/ + beginShare() } else { beginShare() } @@ -1317,3 +852,447 @@ public class ShareRootControllerImpl { } } } + +private func attemptChatImport( + context: ShareControllerAccountContext, + getExtensionContext: @escaping () -> NSExtensionContext?, + accountManager: AccountManager, + appLockContext: AppLockContext, + applicationBindings: TelegramApplicationBindings, + initialPresentationDataAndSettings: InitialPresentationDataAndSettings, + networkInitializationArguments: NetworkInitializationArguments, + presentationData: PresentationData, + makeTempContext: @escaping (AccountManager, AppLockContext, TelegramApplicationBindings, InitialPresentationDataAndSettings, NetworkInitializationArguments) -> Signal, + mainWindow: Window1, + navigationController: NavigationController, + archivePathValue: String?, + mainFileHeader: String, + mainFile: TempBoxFile, + otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)], + beginShare: @escaping () -> Void +) { + let _ = (makeTempContext( + accountManager, + appLockContext, + applicationBindings, + initialPresentationDataAndSettings, + networkInitializationArguments + ) + |> deliverOnMainQueue).start(next: { context in + context.account.resetStateManagement() + context.account.shouldBeServiceTaskMaster.set(.single(.now)) + + let _ = (TelegramEngine.HistoryImport(postbox: context.account.stateManager.postbox, network: context.account.stateManager.network).getInfo(header: mainFileHeader) + |> deliverOnMainQueue).start(next: { [weak mainWindow] parseInfo in + switch parseInfo { + case let .group(groupTitle): + var attemptSelectionImpl: ((EnginePeer) -> Void)? + var createNewGroupImpl: (() -> Void)? + + let controller = PeerSelectionControllerImpl(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled, .doNotSearchMessages], hasContactSelector: false, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in + attemptSelectionImpl?(peer) + }, createNewGroup: { + createNewGroupImpl?() + }, pretendPresentedInModal: true, selectForumThreads: false)) + + controller.customDismiss = { + //inForeground.set(false) + getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + } + + controller.peerSelected = { peer, _ in + attemptSelectionImpl?(peer) + } + + controller.navigationPresentation = .default + + let beginWithPeer: (PeerId) -> Void = { peerId in + navigationController.view.endEditing(true) + navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: { + //inForeground.set(false) + getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + }, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries)) + } + + attemptSelectionImpl = { peer in + var errorText: String? + if case let .channel(channel) = peer { + if channel.hasPermission(.changeInfo), (channel.flags.contains(.isCreator) || channel.adminRights != nil) { + } else { + errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin + } + } else if case let .legacyGroup(group) = peer { + switch group.role { + case .creator: + break + default: + errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin + } + } else { + errorText = presentationData.strings.ChatImport_SelectionErrorGroupGeneric + } + + if let errorText = errorText { + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]) + mainWindow?.present(controller, on: .root) + } else { + controller.inProgress = true + let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) + |> deliverOnMainQueue).start(next: { result in + controller.inProgress = false + + var errorText: String? + if case let .channel(channel) = peer { + if channel.hasPermission(.changeInfo), (channel.flags.contains(.isCreator) || channel.adminRights != nil) { + } else { + errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin + } + } else if case let .legacyGroup(group) = peer { + switch group.role { + case .creator: + break + default: + errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin + } + } else if case .user = peer { + } else { + errorText = presentationData.strings.ChatImport_SelectionErrorGroupGeneric + } + + if let errorText = errorText { + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]) + mainWindow?.present(controller, on: .root) + } else { + let text: String + switch result { + case .allowed: + if let groupTitle = groupTitle { + text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithTitle(groupTitle, peer.debugDisplayTitle).string + } else { + text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithoutTitle(peer.debugDisplayTitle).string + } + case let .alert(textValue): + text = textValue + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { + beginWithPeer(peer.id) + })], parseMarkdown: true) + mainWindow?.present(controller, on: .root) + } + }, error: { error in + controller.inProgress = false + + let errorText: String + switch error { + case .generic: + errorText = presentationData.strings.Login_UnknownError + case .chatAdminRequired: + errorText = presentationData.strings.ChatImportActivity_ErrorNotAdmin + case .invalidChatType: + errorText = presentationData.strings.ChatImportActivity_ErrorInvalidChatType + case .userBlocked: + errorText = presentationData.strings.ChatImportActivity_ErrorUserBlocked + case .limitExceeded: + errorText = presentationData.strings.ChatImportActivity_ErrorLimitExceeded + case .notMutualContact: + errorText = presentationData.strings.ChatImport_UserErrorNotMutual + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]) + mainWindow?.present(controller, on: .root) + }) + } + } + + createNewGroupImpl = { + let resolvedGroupTitle: String + if let groupTitle = groupTitle { + resolvedGroupTitle = groupTitle + } else { + resolvedGroupTitle = "Group" + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_CreateGroupAlertTitle, text: presentationData.strings.ChatImport_CreateGroupAlertText(resolvedGroupTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_CreateGroupAlertImportAction, action: { + var signal: Signal = _internal_createSupergroup(postbox: context.account.stateManager.postbox, network: context.account.stateManager.network, stateManager: context.account.stateManager, title: resolvedGroupTitle, description: nil, username: nil, isForum: false, isForHistoryImport: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + mainWindow?.present(controller, on: .root) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + beginWithPeer(peerId) + } else { + } + }) + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + })], actionLayout: .vertical, parseMarkdown: true) + mainWindow?.present(controller, on: .root) + } + + navigationController.viewControllers = [controller] + case let .privateChat(title): + var attemptSelectionImpl: ((EnginePeer) -> Void)? + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeDisabled, .doNotSearchMessages, .excludeSecretChats], hasChatListSelector: false, hasContactSelector: true, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in + attemptSelectionImpl?(peer) + }, pretendPresentedInModal: true, selectForumThreads: true)) + + controller.customDismiss = { + //inForeground.set(false) + getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + } + + controller.peerSelected = { peer, _ in + attemptSelectionImpl?(peer) + } + + controller.navigationPresentation = .default + + let beginWithPeer: (PeerId) -> Void = { peerId in + navigationController.view.endEditing(true) + navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: { + //inForeground.set(false) + getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + }, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries)) + } + + attemptSelectionImpl = { [weak controller] peer in + controller?.inProgress = true + let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) + |> deliverOnMainQueue).start(next: { result in + controller?.inProgress = false + + let text: String + switch result { + case .allowed: + if let title = title { + text = presentationData.strings.ChatImport_SelectionConfirmationUserWithTitle(title, peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = presentationData.strings.ChatImport_SelectionConfirmationUserWithoutTitle(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + case let .alert(textValue): + text = textValue + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { + beginWithPeer(peer.id) + })], parseMarkdown: true) + mainWindow?.present(controller, on: .root) + }, error: { error in + controller?.inProgress = false + + let errorText: String + switch error { + case .generic: + errorText = presentationData.strings.Login_UnknownError + case .chatAdminRequired: + errorText = presentationData.strings.ChatImportActivity_ErrorNotAdmin + case .invalidChatType: + errorText = presentationData.strings.ChatImportActivity_ErrorInvalidChatType + case .userBlocked: + errorText = presentationData.strings.ChatImportActivity_ErrorUserBlocked + case .limitExceeded: + errorText = presentationData.strings.ChatImportActivity_ErrorLimitExceeded + case .notMutualContact: + errorText = presentationData.strings.ChatImport_UserErrorNotMutual + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]) + mainWindow?.present(controller, on: .root) + }) + } + + navigationController.viewControllers = [controller] + case let .unknown(peerTitle): + var attemptSelectionImpl: ((EnginePeer) -> Void)? + var createNewGroupImpl: (() -> Void)? + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.excludeDisabled, .doNotSearchMessages], hasContactSelector: true, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in + attemptSelectionImpl?(peer) + }, createNewGroup: { + createNewGroupImpl?() + }, pretendPresentedInModal: true, selectForumThreads: true)) + + controller.customDismiss = { + //inForeground.set(false) + getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + } + + controller.peerSelected = { peer, _ in + attemptSelectionImpl?(peer) + } + + controller.navigationPresentation = .default + + let beginWithPeer: (EnginePeer.Id) -> Void = { peerId in + navigationController.view.endEditing(true) + navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: { + //inForeground.set(false) + getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + }, peerId: peerId, archivePath: archivePathValue, mainEntry: mainFile, otherEntries: otherEntries)) + } + + attemptSelectionImpl = { [weak controller] peer in + controller?.inProgress = true + let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) + |> deliverOnMainQueue).start(next: { result in + controller?.inProgress = false + + var errorText: String? + if case let .channel(channel) = peer { + if channel.hasPermission(.changeInfo), (channel.flags.contains(.isCreator) || channel.adminRights != nil) { + } else { + errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin + } + } else if case let .legacyGroup(group) = peer { + switch group.role { + case .creator: + break + default: + errorText = presentationData.strings.ChatImport_SelectionErrorNotAdmin + } + } else if case .user = peer { + } else { + errorText = presentationData.strings.ChatImport_SelectionErrorGroupGeneric + } + + if let errorText = errorText { + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]) + mainWindow?.present(controller, on: .root) + } else { + if case .user = peer { + let text: String + switch result { + case .allowed: + if let title = peerTitle { + text = presentationData.strings.ChatImport_SelectionConfirmationUserWithTitle(title, peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + } else { + text = presentationData.strings.ChatImport_SelectionConfirmationUserWithoutTitle(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + } + case let .alert(textValue): + text = textValue + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { + beginWithPeer(peer.id) + })], parseMarkdown: true) + mainWindow?.present(controller, on: .root) + } else { + let text: String + switch result { + case .allowed: + if let groupTitle = peerTitle { + text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithTitle(groupTitle, peer.debugDisplayTitle).string + } else { + text = presentationData.strings.ChatImport_SelectionConfirmationGroupWithoutTitle(peer.debugDisplayTitle).string + } + case let .alert(textValue): + text = textValue + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_SelectionConfirmationAlertTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_SelectionConfirmationAlertImportAction, action: { + beginWithPeer(peer.id) + })], parseMarkdown: true) + mainWindow?.present(controller, on: .root) + } + } + }, error: { error in + controller?.inProgress = false + + let errorText: String + switch error { + case .generic: + errorText = presentationData.strings.Login_UnknownError + case .chatAdminRequired: + errorText = presentationData.strings.ChatImportActivity_ErrorNotAdmin + case .invalidChatType: + errorText = presentationData.strings.ChatImportActivity_ErrorInvalidChatType + case .userBlocked: + errorText = presentationData.strings.ChatImportActivity_ErrorUserBlocked + case .limitExceeded: + errorText = presentationData.strings.ChatImportActivity_ErrorLimitExceeded + case .notMutualContact: + errorText = presentationData.strings.ChatImport_UserErrorNotMutual + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + })]) + mainWindow?.present(controller, on: .root) + }) + } + + createNewGroupImpl = { + let resolvedGroupTitle: String + if let groupTitle = peerTitle { + resolvedGroupTitle = groupTitle + } else { + resolvedGroupTitle = "Group" + } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_CreateGroupAlertTitle, text: presentationData.strings.ChatImport_CreateGroupAlertText(resolvedGroupTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_CreateGroupAlertImportAction, action: { + var signal: Signal = _internal_createSupergroup(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, title: resolvedGroupTitle, description: nil, username: nil, isForum: false, isForHistoryImport: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + mainWindow?.present(controller, on: .root) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + beginWithPeer(peerId) + } else { + } + }) + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + })], actionLayout: .vertical, parseMarkdown: true) + mainWindow?.present(controller, on: .root) + } + + navigationController.viewControllers = [controller] + } + }, error: { _ in + beginShare() + }) + }) +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD index f53e1721b9e..b5a0f4e9597 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD @@ -40,6 +40,8 @@ swift_library( "//submodules/TooltipUI", "//submodules/OverlayStatusController", "//submodules/UndoUI", + "//submodules/TemporaryCachedPeerDataManager", + "//submodules/CountrySelectionUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift new file mode 100644 index 00000000000..a4d93ce24f2 --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -0,0 +1,1196 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import Postbox +import MultilineTextComponent +import PresentationDataUtils +import ButtonComponent +import AnimatedCounterComponent +import TokenListTextField +import TelegramStringFormatting +import LottieComponent +import UndoUI +import CountrySelectionUI + +final class CountriesMultiselectionScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let stateContext: CountriesMultiselectionScreen.StateContext + let completion: ([String]) -> Void + + init( + context: AccountContext, + stateContext: CountriesMultiselectionScreen.StateContext, + completion: @escaping ([String]) -> Void + ) { + self.context = context + self.stateContext = stateContext + self.completion = completion + } + + static func ==(lhs: CountriesMultiselectionScreenComponent, rhs: CountriesMultiselectionScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.stateContext !== rhs.stateContext { + return false + } + return true + } + + private struct ItemLayout: Equatable { + struct Section: Equatable { + var id: Int + var insets: UIEdgeInsets + var itemHeight: CGFloat + var itemCount: Int + + var totalHeight: CGFloat + + init( + id: Int, + insets: UIEdgeInsets, + itemHeight: CGFloat, + itemCount: Int + ) { + self.id = id + self.insets = insets + self.itemHeight = itemHeight + self.itemCount = itemCount + + self.totalHeight = insets.top + itemHeight * CGFloat(itemCount) + insets.bottom + } + } + + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + var sideInset: CGFloat + var navigationHeight: CGFloat + var sections: [Section] + + var contentHeight: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, navigationHeight: CGFloat, sections: [Section]) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + self.sideInset = sideInset + self.navigationHeight = navigationHeight + self.sections = sections + + var contentHeight: CGFloat = 0.0 + contentHeight += navigationHeight + for section in sections { + contentHeight += section.totalHeight + } + contentHeight += bottomInset + self.contentHeight = contentHeight + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class AnimationHint { + let contentReloaded: Bool + + init( + contentReloaded: Bool + ) { + self.contentReloaded = contentReloaded + } + } + + final class View: UIView, UIScrollViewDelegate { + private let containerView: UIView + private let backgroundView: UIImageView + + private let navigationContainerView: UIView + private let navigationBackgroundView: BlurredBackgroundView + private let navigationTitle = ComponentView() + private let navigationLeftButton = ComponentView() + private let navigationRightButton = ComponentView() + private let navigationSeparatorLayer: SimpleLayer + private let navigationTextFieldState = TokenListTextField.ExternalState() + private let navigationTextField = ComponentView() + private let textFieldSeparatorLayer: SimpleLayer + + private let emptyResultsTitle = ComponentView() + private let emptyResultsText = ComponentView() + private let emptyResultsAnimation = ComponentView() + + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let indexNode: CollectionIndexNode + + private let bottomBackgroundView: BlurredBackgroundView + private let bottomSeparatorLayer: SimpleLayer + private let actionButton = ComponentView() + + private let countryTemplateItem = ComponentView() + + private let itemContainerView: UIView + private var visibleSectionHeaders: [Int: ComponentView] = [:] + private var visibleItems: [AnyHashable: ComponentView] = [:] + + private var ignoreScrolling: Bool = false + private var isDismissed: Bool = false + + private var selectedCountries: [String] = [] + + private var component: CountriesMultiselectionScreenComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? + + private var topOffsetDistance: CGFloat? + + private var defaultStateValue: CountriesMultiselectionScreen.State? + private var stateDisposable: Disposable? + + private var searchStateContext: CountriesMultiselectionScreen.StateContext? + private var searchStateDisposable: Disposable? + + private let postingAvailabilityDisposable = MetaDisposable() + + private let hapticFeedback = HapticFeedback() + + private var effectiveStateValue: CountriesMultiselectionScreen.State? { + return self.searchStateContext?.stateValue ?? self.defaultStateValue + } + + override init(frame: CGRect) { + self.containerView = SparseContainerView() + + self.backgroundView = UIImageView() + + self.navigationContainerView = SparseContainerView() + self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.navigationSeparatorLayer = SimpleLayer() + self.textFieldSeparatorLayer = SimpleLayer() + + self.bottomBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.bottomSeparatorLayer = SimpleLayer() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.itemContainerView = UIView() + self.itemContainerView.clipsToBounds = true + self.itemContainerView.layer.cornerRadius = 10.0 + + self.indexNode = CollectionIndexNode() + + super.init(frame: frame) + + self.addSubview(self.containerView) + self.containerView.addSubview(self.backgroundView) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.containerView.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.scrollContentView.addSubview(self.itemContainerView) + + self.containerView.addSubview(self.navigationContainerView) + self.navigationContainerView.addSubview(self.navigationBackgroundView) + self.navigationContainerView.layer.addSublayer(self.navigationSeparatorLayer) + + self.containerView.addSubview(self.bottomBackgroundView) + self.containerView.layer.addSublayer(self.bottomSeparatorLayer) + + self.containerView.addSubnode(self.indexNode) + + self.indexNode.indexSelected = { [weak self] section in + guard let self, let sections = self.effectiveStateValue?.sections, let itemLayout = self.itemLayout else { + return + } + + guard let index = sections.firstIndex(where: { $0.0 == section }) else { + return + } + + var contentOffset: CGFloat = 0.0 + for i in 0 ..< index { + let section = itemLayout.sections[i] + contentOffset += section.totalHeight + } + + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: min(contentOffset, self.scrollView.contentSize.height - self.scrollView.bounds.height + self.scrollView.contentInset.bottom)), animated: false) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.stateDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { + return + } + + if scrollView.contentOffset.y <= -100.0 && velocity.y <= -2.0 { + } else { + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + if topOffset > 0.0 { + topOffset = max(0.0, topOffset) + + if topOffset < topOffsetDistance { + //targetContentOffset.pointee.y = scrollView.contentOffset.y + //scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + } + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + + if let result = self.navigationContainerView.hitTest(self.convert(point, to: self.navigationContainerView), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() as? CountriesMultiselectionScreen else { + return + } + controller.requestDismiss() + } + } + + private func updateScrolling(transition: Transition) { + guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { + return + } + guard let stateValue = self.effectiveStateValue else { + return + } + + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundView.layer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + transition.setPosition(view: self.navigationContainerView, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let bottomDistance = itemLayout.contentHeight - self.scrollView.bounds.maxY + let bottomAlphaDistance: CGFloat = 30.0 + var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance + bottomAlpha = max(0.0, min(1.0, bottomAlpha)) + + var visibleBounds = self.scrollView.bounds + visibleBounds.origin.y -= itemLayout.topInset + visibleBounds.size.height += itemLayout.topInset + + var visibleFrame = self.scrollView.frame + visibleFrame.origin.x = 0.0 + visibleFrame.origin.y -= itemLayout.topInset + visibleFrame.size.height += itemLayout.topInset + + var validIds: [AnyHashable] = [] + var validSectionHeaders: [AnyHashable] = [] + var sectionOffset: CGFloat = itemLayout.navigationHeight + + for sectionIndex in 0 ..< itemLayout.sections.count { + let section = itemLayout.sections[sectionIndex] + + var minSectionHeader: UIView? + do { + var sectionHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top)) + + let sectionHeaderMinY = topOffset + itemLayout.containerInset + itemLayout.navigationHeight + let sectionHeaderMaxY = itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset + section.totalHeight - 28.0 + + sectionHeaderFrame.origin.y = max(sectionHeaderFrame.origin.y, sectionHeaderMinY) + sectionHeaderFrame.origin.y = min(sectionHeaderFrame.origin.y, sectionHeaderMaxY) + + if visibleFrame.intersects(sectionHeaderFrame), self.searchStateContext == nil { + validSectionHeaders.append(section.id) + let sectionHeader: ComponentView + var sectionHeaderTransition = transition + if let current = self.visibleSectionHeaders[section.id] { + sectionHeader = current + } else { + if !transition.animation.isImmediate { + sectionHeaderTransition = .immediate + } + sectionHeader = ComponentView() + self.visibleSectionHeaders[section.id] = sectionHeader + } + + let sectionTitle = stateValue.sections[sectionIndex].0 + let _ = sectionHeader.update( + transition: sectionHeaderTransition, + component: AnyComponent(SectionHeaderComponent( + theme: environment.theme, + style: .plain, + title: sectionTitle, + actionTitle: nil, + action: nil + )), + environment: {}, + containerSize: sectionHeaderFrame.size + ) + if let sectionHeaderView = sectionHeader.view { + if sectionHeaderView.superview == nil { + self.scrollContentClippingView.addSubview(sectionHeaderView) + + if !transition.animation.isImmediate { + sectionHeaderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + let sectionXOffset = self.scrollView.frame.minX + if minSectionHeader == nil { + minSectionHeader = sectionHeaderView + } + sectionHeaderTransition.setFrame(view: sectionHeaderView, frame: sectionHeaderFrame.offsetBy(dx: sectionXOffset, dy: 0.0)) + } + } + } + + let (_, countries) = stateValue.sections[sectionIndex] + for i in 0 ..< countries.count { + let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + let country = countries[i] + let itemId = AnyHashable(country.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + let isSelected = self.selectedCountries.contains(country.id) + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent(CountryListItemComponent( + context: component.context, + theme: environment.theme, + title: "\(country.flag) \(country.name)", + selectionState: .editing(isSelected: isSelected, isTinted: false), + hasNext: true, + action: { [weak self] in + guard let self, let environment = self.environment, let controller = environment.controller() as? CountriesMultiselectionScreen else { + return + } + let update = { + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + + if self.searchStateContext != nil { + if let navigationTextFieldView = self.navigationTextField.view as? TokenListTextField.View { + navigationTextFieldView.clearText() + } + } + } + + let index = self.selectedCountries.firstIndex(of: country.id) + let toggleCountry = { + if let index { + self.selectedCountries.remove(at: index) + } else { + self.selectedCountries.append(country.id) + } + update() + } + + let limit = component.context.userLimits.maxGiveawayCountriesCount + if self.selectedCountries.count >= limit, index == nil { + self.hapticFeedback.error() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "You can select maximum \(limit) countries.", timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) + return + } + toggleCountry() + }) + ), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + sectionOffset += section.totalHeight + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + + var removeSectionHeaderIds: [Int] = [] + for (id, item) in self.visibleSectionHeaders { + if !validSectionHeaders.contains(id) { + removeSectionHeaderIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeSectionHeaderIds { + self.visibleSectionHeaders.removeValue(forKey: id) + } + + let fadeTransition = Transition.easeInOut(duration: 0.25) + if let searchStateContext = self.searchStateContext, case let .countriesSearch(query) = searchStateContext.subject, let value = searchStateContext.stateValue, value.sections.isEmpty { + let sideInset: CGFloat = 44.0 + let emptyAnimationHeight = 148.0 + let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0 + let bottomInset: CGFloat = max(environment.safeInsets.bottom, environment.inputHeight) + let visibleHeight = visibleFrame.height + let emptyAnimationSpacing: CGFloat = 8.0 + let emptyTextSpacing: CGFloat = 8.0 + + let emptyResultsTitleSize = self.emptyResultsTitle.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Contacts_Search_NoResults, font: Font.semibold(17.0), textColor: environment.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: visibleFrame.size + ) + let emptyResultsTextSize = self.emptyResultsText.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Contacts_Search_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + ) + ), + environment: {}, + containerSize: CGSize(width: visibleFrame.width - sideInset * 2.0, height: visibleFrame.height) + ) + let emptyResultsAnimationSize = self.emptyResultsAnimation.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "ChatListNoResults") + )), + environment: {}, + containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight) + ) + + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsTextSize.height + emptyTextSpacing + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) + + let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((visibleFrame.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize) + + let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((visibleFrame.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize) + + let emptyResultsTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((visibleFrame.width - emptyResultsTextSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsTextSize) + + if let view = self.emptyResultsAnimation.view as? LottieComponent.View { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.scrollView.addSubview(view) + view.playOnce() + } + view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) + transition.setPosition(view: view, position: emptyResultsAnimationFrame.center) + } + if let view = self.emptyResultsTitle.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.scrollView.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) + transition.setPosition(view: view, position: emptyResultsTitleFrame.center) + } + if let view = self.emptyResultsText.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.scrollView.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsTextFrame.size) + transition.setPosition(view: view, position: emptyResultsTextFrame.center) + } + } else { + if let view = self.emptyResultsAnimation.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + if let view = self.emptyResultsTitle.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + if let view = self.emptyResultsText.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + } + } + + func update(component: CountriesMultiselectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + guard !self.isDismissed else { + return availableSize + } + let animationHint = transition.userData(AnimationHint.self) + + var contentTransition = transition + if let animationHint, animationHint.contentReloaded, !transition.animation.isImmediate { + contentTransition = .immediate + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 0.0 + + let containerWidth: CGFloat + if environment.metrics.isTablet { + containerWidth = 414.0 + } else { + containerWidth = availableSize.width + } + let containerSideInset = floorToScreenPixels((availableSize.width - containerWidth) / 2.0) + + if self.component == nil { + var applyState = false + self.defaultStateValue = component.stateContext.stateValue + self.selectedCountries = Array(component.stateContext.initialSelectedCountries) + + self.stateDisposable = (component.stateContext.state + |> deliverOnMainQueue).start(next: { [weak self] stateValue in + guard let self else { + return + } + self.defaultStateValue = stateValue + if applyState { + self.state?.updated(transition: .immediate) + } + }) + applyState = true + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.scrollView.indicatorStyle = environment.theme.overallDarkAppearance ? .white : .black + + self.backgroundView.image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(environment.theme.list.plainBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height * 0.5))) + })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 19) + + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + self.bottomBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + + self.textFieldSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + } + + + let itemsContainerWidth = containerWidth + + var tokens: [TokenListTextField.Token] = [] + for countryId in self.selectedCountries { + guard let stateValue = self.defaultStateValue else { + continue + } + + var tokenCountry: CountriesMultiselectionScreen.CountryItem? + outer: for (_, countries) in stateValue.sections { + for country in countries { + if country.id == countryId { + tokenCountry = country + break outer + } + } + } + + guard let tokenCountry else { + continue + } + + tokens.append(TokenListTextField.Token( + id: AnyHashable(countryId), + title: tokenCountry.name, + fixedPosition: nil, + content: .emoji(tokenCountry.flag) + )) + } + + let placeholder: String = "Search" + self.navigationTextField.parentState = state + let navigationTextFieldSize = self.navigationTextField.update( + transition: transition, + component: AnyComponent(TokenListTextField( + externalState: self.navigationTextFieldState, + context: component.context, + theme: environment.theme, + placeholder: placeholder, + tokens: tokens, + sideInset: sideInset, + deleteToken: { [weak self] tokenId in + guard let self else { + return + } + if let countryId = tokenId.base as? String { + self.selectedCountries.removeAll(where: { $0 == countryId }) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } + )), + environment: {}, + containerSize: CGSize(width: containerWidth, height: 1000.0) + ) + + if !self.navigationTextFieldState.text.isEmpty { + if let searchStateContext = self.searchStateContext, searchStateContext.subject == .countriesSearch(query: self.navigationTextFieldState.text) { + } else { + self.searchStateDisposable?.dispose() + let searchStateContext = CountriesMultiselectionScreen.StateContext(context: component.context, subject: .countriesSearch(query: self.navigationTextFieldState.text)) + var applyState = false + self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + self.searchStateContext = searchStateContext + if applyState { + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) + } + }) + applyState = true + } + } else if let _ = self.searchStateContext { + self.searchStateContext = nil + self.searchStateDisposable?.dispose() + self.searchStateDisposable = nil + + contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true)) + } + + let countryItemSize = self.countryTemplateItem.update( + transition: transition, + component: AnyComponent(CountryListItemComponent( + context: component.context, + theme: environment.theme, + title: "Title", + selectionState: .editing(isSelected: false, isTinted: false), + hasNext: true, + action: {} + )), + environment: {}, + containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) + ) + + var sections: [ItemLayout.Section] = [] + if let stateValue = self.effectiveStateValue { + + var id: Int = 0 + for (_, countries) in stateValue.sections { + sections.append(ItemLayout.Section( + id: id, + insets: UIEdgeInsets(top: self.searchStateContext != nil ? 0.0 : 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: countryItemSize.height, + itemCount: countries.count + )) + id += 1 + } + } + + let containerInset: CGFloat = environment.statusBarHeight + + var navigationHeight: CGFloat = 56.0 + let navigationSideInset: CGFloat = 16.0 + var navigationButtonsWidth: CGFloat = 0.0 + + let navigationLeftButtonSize = self.navigationLeftButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self, let environment = self.environment, let controller = environment.controller() as? CountriesMultiselectionScreen else { + return + } + controller.requestDismiss() + } + ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: navigationHeight) + ) + let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: containerSideInset + navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) + if let navigationLeftButtonView = self.navigationLeftButton.view { + if navigationLeftButtonView.superview == nil { + self.navigationContainerView.addSubview(navigationLeftButtonView) + } + transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) + } + navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset + + let actionButtonTitle = "Save Countries" + let title = "Select Countries" + let subtitle = "select up to \(component.context.userLimits.maxGiveawayCountriesCount) countries" + + let titleComponent = AnyComponent( + List([ + AnyComponentWithIdentity( + id: "title", + component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)) + ), + AnyComponentWithIdentity( + id: "subtitle", + component: AnyComponent(Text(text: subtitle, font: Font.regular(13.0), color: environment.theme.rootController.navigationBar.secondaryTextColor)) + ) + ], + centerAlignment: true) + ) + + let navigationTitleSize = self.navigationTitle.update( + transition: .immediate, + component: titleComponent, + environment: {}, + containerSize: CGSize(width: containerWidth - navigationButtonsWidth, height: navigationHeight) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: containerSideInset + floor((containerWidth - navigationTitleSize.width) * 0.5), y: floor((navigationHeight - navigationTitleSize.height) * 0.5)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + self.navigationContainerView.addSubview(navigationTitleView) + } + transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center) + navigationTitleView.bounds = CGRect(origin: CGPoint(), size: navigationTitleFrame.size) + } + + let navigationTextFieldFrame = CGRect(origin: CGPoint(x: containerSideInset, y: navigationHeight), size: navigationTextFieldSize) + if let navigationTextFieldView = self.navigationTextField.view { + if navigationTextFieldView.superview == nil { + self.navigationContainerView.addSubview(navigationTextFieldView) + self.navigationContainerView.layer.addSublayer(self.textFieldSeparatorLayer) + } + transition.setFrame(view: navigationTextFieldView, frame: navigationTextFieldFrame) + transition.setFrame(layer: self.textFieldSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset, y: navigationTextFieldFrame.maxY), size: CGSize(width: navigationTextFieldFrame.width, height: UIScreenPixel))) + } + navigationHeight += navigationTextFieldFrame.height + + self.navigationBackgroundView.update(size: CGSize(width: containerWidth, height: navigationHeight), cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.navigationBackgroundView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: navigationHeight))) + + transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset, y: navigationHeight), size: CGSize(width: containerWidth, height: UIScreenPixel))) + + var bottomPanelHeight: CGFloat = 0.0 + var bottomPanelInset: CGFloat = 0.0 + + let badge = self.selectedCountries.count + + let actionButtonSize = 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.9) + ), + content: AnyComponentWithIdentity( + id: actionButtonTitle, + component: AnyComponent(ButtonTextContentComponent( + text: actionButtonTitle, + badge: badge, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor, + combinedAlignment: true + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() as? CountriesMultiselectionScreen else { + return + } + + component.completion(self.selectedCountries) + + controller.dismissAllTooltips() + controller.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: containerWidth - navigationSideInset * 2.0, height: 50.0) + ) + + if environment.inputHeight != 0.0 { + bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height + } else { + bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height + } + let actionButtonFrame = CGRect(origin: CGPoint(x: containerSideInset + navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.containerView.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + bottomPanelInset = 8.0 + transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: containerWidth, height: bottomPanelHeight + bottomPanelInset))) + self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) + + let itemContainerSize = CGSize(width: itemsContainerWidth, height: availableSize.height) + let itemLayout = ItemLayout(containerSize: itemContainerSize, containerInset: containerInset, bottomInset: 0.0, topInset: 0.0, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections) + self.itemLayout = itemLayout + + contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: itemLayout.contentHeight))) + + let scrollContentHeight = max(itemLayout.contentHeight + containerInset, availableSize.height - containerInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: containerWidth, height: itemLayout.contentHeight))) + + transition.setPosition(view: self.backgroundView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) + + let scrollClippingInset: CGFloat = 0.0 + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset + scrollClippingInset), size: CGSize(width: availableSize.width, height: availableSize.height - scrollClippingInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + transition.setFrame(view: self.containerView, frame: CGRect(origin: .zero, size: availableSize)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) + let contentSize = CGSize(width: containerWidth, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + let contentInset: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelHeight + bottomPanelInset, right: 0.0) + let indicatorInset = UIEdgeInsets(top: max(itemLayout.containerInset, environment.safeInsets.top + navigationHeight), left: 0.0, bottom: contentInset.bottom, right: 0.0) + if indicatorInset != self.scrollView.scrollIndicatorInsets { + self.scrollView.scrollIndicatorInsets = indicatorInset + } + if contentInset != self.scrollView.contentInset { + self.scrollView.contentInset = contentInset + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height)) + } + self.ignoreScrolling = false + self.updateScrolling(transition: contentTransition) + + let indexNodeFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 20.0, y: navigationHeight), size: CGSize(width: 20.0, height: availableSize.height - navigationHeight - contentInset.bottom)) + self.indexNode.frame = indexNodeFrame + + if let stateValue = self.effectiveStateValue { + let indexSections = stateValue.sections.map { $0.0 } + self.indexNode.update(size: CGSize(width: indexNodeFrame.width, height: indexNodeFrame.height), color: environment.theme.list.itemAccentColor, sections: indexSections, transition: .animated(duration: 0.2, curve: .easeInOut)) + self.indexNode.isUserInteractionEnabled = !indexSections.isEmpty + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class CountriesMultiselectionScreen: ViewControllerComponentContainer { + private let context: AccountContext + + private var isCustomModal = true + private var isDismissed: Bool = false + + public var dismissed: () -> Void = {} + + public init( + context: AccountContext, + stateContext: StateContext, + completion: @escaping ([String]) -> Void + ) { + self.context = context + + super.init(context: context, component: CountriesMultiselectionScreenComponent( + context: context, + stateContext: stateContext, + completion: completion + ), navigationBarAppearance: .none, theme: .default) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .modal + self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false + self.lockOrientation = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if !self.isDismissed { + self.isDismissed = true + self.dismissed() + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + var updatedLayout = layout + updatedLayout.intrinsicInsets.bottom += 66.0 + self.presentationContext.containerLayoutUpdated(updatedLayout, transition: transition) + } + + fileprivate func dismissAllTooltips() { + self.window?.forEachController { controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + } + self.forEachController { controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + } + } + + func requestDismiss() { + self.dismissAllTooltips() + self.dismissed() + self.dismiss() + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + self.view.endEditing(true) + + self.dismiss(animated: true) + } + } +} + +public extension CountriesMultiselectionScreen { + struct CountryItem { + let id: String + let flag: String + let name: String + } + + final class State { + let sections: [(String, [CountryItem])] + + fileprivate init( + sections: [(String, [CountryItem])] + ) { + self.sections = sections + } + } + + final class StateContext { + public enum Subject: Equatable { + case countries + case countriesSearch(query: String) + } + + var stateValue: State? + + public let subject: Subject + public let initialSelectedCountries: [String] + + private var stateDisposable: Disposable? + private let stateSubject = Promise() + public var state: Signal { + return self.stateSubject.get() + } + + private let readySubject = ValuePromise(false, ignoreRepeated: true) + public var ready: Signal { + return self.readySubject.get() + } + + public init( + context: AccountContext, + subject: Subject = .countries, + initialSelectedCountries: [String] = [] + ) { + self.subject = subject + self.initialSelectedCountries = initialSelectedCountries + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let countries = localizedCountryNamesAndCodes(strings: presentationData.strings).sorted { lhs, rhs in + return lhs.0.1.lowercased() < rhs.0.1.lowercased() + } + + switch subject { + case .countries: + var sections: [(String, [CountryItem])] = [] + + var currentSection: String? + var currentCountries: [CountryItem] = [] + for country in countries { + let section = String(country.0.1.prefix(1)) + if currentSection != section { + if let currentSection { + sections.append((currentSection, currentCountries)) + } + currentSection = section + currentCountries = [] + } + + currentCountries.append(CountryItem( + id: country.1, + flag: flagEmoji(countryCode: country.1), + name: country.0.1 + )) + } + + if let currentSection { + sections.append((currentSection, currentCountries)) + } + + let state = State( + sections: sections + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + case let .countriesSearch(query): + let results = searchCountries(items: countries, query: query) + + var resultCountries: [CountryItem] = [] + var existingIds = Set() + for country in results { + guard !existingIds.contains(country.1) else { + continue + } + resultCountries.append(CountryItem( + id: country.1, + flag: flagEmoji(countryCode: country.1), + name: country.0.1 + )) + existingIds.insert(country.1) + } + let state = State( + sections: [("", resultCountries)] + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + } + } + + deinit { + self.stateDisposable?.dispose() + } + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift new file mode 100644 index 00000000000..66d8648e20c --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountryListItemComponent.swift @@ -0,0 +1,225 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import CheckNode +import BundleIconComponent + +final class CountryListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool, isTinted: Bool) + } + + let context: AccountContext + let theme: PresentationTheme + let title: String + let selectionState: SelectionState + let hasNext: Bool + let action: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + title: String, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.title = title + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: CountryListItemComponent, rhs: CountryListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let separatorLayer: SimpleLayer + + private var checkLayer: CheckLayer? + + private var component: CountryListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + func update(component: CountryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + self.component = component + self.state = state + + let contextInset: CGFloat = 0.0 + + let height: CGFloat = 44.0 + let verticalInset: CGFloat = 0.0 + var leftInset: CGFloat = 16.0 + let rightInset: CGFloat = contextInset * 2.0 + 8.0 + + if case let .editing(isSelected, isTinted) = component.selectionState { + leftInset += 44.0 + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + var theme = CheckNodeTheme(theme: component.theme, style: .plain) + if isTinted { + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) + } + checkLayer.theme = theme + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + var theme = CheckNodeTheme(theme: component.theme, style: .plain) + if isTinted { + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) + } + checkLayer = CheckLayer(theme: theme) + self.checkLayer = checkLayer + self.containerButton.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated && !"".isEmpty { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let centralContentHeight: CGFloat = titleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + let separatorLeftInset = leftInset + 44.0 + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: separatorLeftInset, y: height), size: CGSize(width: availableSize.width - separatorLeftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index d95b22fc747..a14f0398f93 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -11,11 +11,8 @@ import AccountContext import TelegramCore import Postbox import MultilineTextComponent -import SolidRoundedButtonComponent import PresentationDataUtils import ButtonComponent -import PlainButtonComponent -import AnimatedCounterComponent import TokenListTextField import AvatarNode import LocalizedPeerData @@ -26,6 +23,7 @@ import OverlayStatusController import Markdown import TelegramUIPreferences import UndoUI +import TelegramStringFormatting final class ShareWithPeersScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -236,26 +234,6 @@ final class ShareWithPeersScreenComponent: Component { } } - final class PeerItem: Equatable { - let id: EnginePeer.Id - let peer: EnginePeer? - - init( - id: EnginePeer.Id, - peer: EnginePeer? - ) { - self.id = id - self.peer = peer - } - - static func ==(lhs: PeerItem, rhs: PeerItem) -> Bool { - if lhs === rhs { - return true - } - return false - } - } - enum OptionId: Int, Hashable { case screenshot = 0 case pin = 1 @@ -494,7 +472,7 @@ final class ShareWithPeersScreenComponent: Component { } @objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) { - guard let controller = self.environment?.controller() as? ShareWithPeersScreen else { + guard let controller = self.environment?.controller() as? ShareWithPeersScreen, let component = self.component else { return } switch recognizer.state { @@ -518,8 +496,12 @@ final class ShareWithPeersScreenComponent: Component { if translation.y > 100.0 || velocity.y > 10.0 { controller.requestDismiss() - Queue.mainQueue().justDispatch { - controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring)) + if case .members = component.stateContext.subject { + } else if case .channels = component.stateContext.subject { + } else { + Queue.mainQueue().justDispatch { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring)) + } } } else { let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) @@ -815,7 +797,13 @@ final class ShareWithPeersScreenComponent: Component { } private func updateModalOverlayTransition(transition: Transition) { - guard let _ = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { + guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { + return + } + + if case .members = component.stateContext.subject { + return + } else if case .channels = component.stateContext.subject { return } @@ -830,7 +818,7 @@ final class ShareWithPeersScreenComponent: Component { topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) let transitionFactor: CGFloat = 1.0 - topOffsetFraction - if let controller = environment.controller() { + if let controller = environment.controller() as? ShareWithPeersScreen { Queue.mainQueue().justDispatch { var transition = transition if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate { @@ -848,7 +836,7 @@ final class ShareWithPeersScreenComponent: Component { guard let stateValue = self.effectiveStateValue else { return } - + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset topOffset = max(0.0, topOffset) transition.setTransform(layer: self.backgroundView.layer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) @@ -951,7 +939,13 @@ final class ShareWithPeersScreenComponent: Component { } else if section.id == 2 { sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader } else if section.id == 1 { - sectionTitle = environment.strings.Story_Privacy_ContactsHeader + if case .members = component.stateContext.subject { + sectionTitle = environment.strings.BoostGift_Subscribers_SectionTitle + } else if case .channels = component.stateContext.subject { + sectionTitle = environment.strings.BoostGift_Channels_SectionTitle + } else { + sectionTitle = environment.strings.Story_Privacy_ContactsHeader + } } else { sectionTitle = "" } @@ -1088,7 +1082,7 @@ final class ShareWithPeersScreenComponent: Component { self.hapticFeedback.impact(.light) } else { self.postingAvailabilityDisposable.set((component.context.engine.messages.checkStoriesUploadAvailability(target: .peer(peer.id)) - |> deliverOnMainQueue).start(next: { [weak self] status in + |> deliverOnMainQueue).start(next: { [weak self] status in guard let self, let component = self.component else { return } @@ -1106,20 +1100,14 @@ final class ShareWithPeersScreenComponent: Component { return } - let link: String - if let addressName = peer.addressName, !addressName.isEmpty { - link = "t.me/\(peer.addressName ?? "")?boost" - } else { - link = "t.me/c/\(peer.id.id._internalGetInt64Value())?boost" - } - + let link = status.url if let navigationController = self.environment?.controller()?.navigationController as? NavigationController { if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen { previousController.dismiss() } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let controller = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, boosted: false), count: Int32(status.boosts), forceDark: true, cancel: {}, action: { [weak navigationController] in - UIPasteboard.general.string = "https://\(link)" + let controller = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .storiesChannelBoost(peer: peer, 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), forceDark: true, cancel: {}, action: { [weak navigationController] in + UIPasteboard.general.string = link if let previousController = navigationController?.viewControllers.reversed().first(where: { $0 is ShareWithPeersScreen}) as? ShareWithPeersScreen { previousController.dismiss(completion: { [weak navigationController] in @@ -1388,16 +1376,28 @@ final class ShareWithPeersScreenComponent: Component { let subtitle: String? if case let .legacyGroup(group) = peer { subtitle = environment.strings.Conversation_StatusMembers(Int32(group.participantCount)) - } else if case .channel = peer { + } else if case let .channel(channel) = peer { if let count = stateValue.participants[peer.id] { - subtitle = environment.strings.Conversation_StatusMembers(Int32(count)) + if case .broadcast = channel.info { + subtitle = environment.strings.Conversation_StatusSubscribers(Int32(count)) + } else { + subtitle = environment.strings.Conversation_StatusMembers(Int32(count)) + } } else { subtitle = nil } } else { - subtitle = nil + if case .members = component.stateContext.subject { + if let invitedAt = stateValue.invitedAt[peer.id] { + subtitle = environment.strings.BoostGift_Subscribers_Joined(stringForMediumDate(timestamp: invitedAt, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)).string + } else { + subtitle = nil + } + } else { + subtitle = nil + } } - + let isSelected = self.selectedPeers.contains(peer.id) || self.selectedGroups.contains(peer.id) let _ = visibleItem.update( transition: itemTransition, @@ -1415,27 +1415,69 @@ final class ShareWithPeersScreenComponent: Component { selectionState: .editing(isSelected: isSelected, isTinted: false), hasNext: true, action: { [weak self] peer in - guard let self else { + guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { return } - if peer.id.isGroupOrChannel { - self.toggleGroupPeer(peer) - } else { - if let index = self.selectedPeers.firstIndex(of: peer.id) { + let update = { + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + + if self.searchStateContext != nil { + if let navigationTextFieldView = self.navigationTextField.view as? TokenListTextField.View { + navigationTextFieldView.clearText() + } + } + } + + let index = self.selectedPeers.firstIndex(of: peer.id) + let togglePeer = { + if let index { self.selectedPeers.remove(at: index) self.updateSelectedGroupPeers() } else { self.selectedPeers.append(peer.id) } + update() } - - let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) - self.state?.updated(transition: transition) - - if self.searchStateContext != nil { - if let navigationTextFieldView = self.navigationTextField.view as? TokenListTextField.View { - navigationTextFieldView.clearText() + if peer.id.isGroupOrChannel { + if case .channels = component.stateContext.subject, self.selectedPeers.count >= component.context.userLimits.maxGiveawayChannelsCount, index == nil { + self.hapticFeedback.error() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: environment.strings.BoostGift_Channels_MaximumReached("\(component.context.userLimits.maxGiveawayChannelsCount)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) + return + } + if case .channels = component.stateContext.subject { + if case let .channel(channel) = peer, channel.addressName == nil, index == nil { + let alertController = textAlertController( + context: component.context, + forceTheme: environment.theme, + title: environment.strings.BoostGift_Channels_PrivateChannel_Title, + text: environment.strings.BoostGift_Channels_PrivateChannel_Text, + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: environment.strings.BoostGift_Channels_PrivateChannel_Add, action: { + togglePeer() + }) + ] + ) + controller.present(alertController, in: .window(.root)) + } else { + togglePeer() + } + } else { + self.toggleGroupPeer(peer) + update() + } + } else { + if case .members = component.stateContext.subject, self.selectedPeers.count >= 10, index == nil { + self.hapticFeedback.error() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: environment.strings.BoostGift_Subscribers_MaximumReached("\(10)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) + return } + togglePeer() } } )), @@ -1629,7 +1671,21 @@ final class ShareWithPeersScreenComponent: Component { } let fadeTransition = Transition.easeInOut(duration: 0.25) - if let searchStateContext = self.searchStateContext, case let .search(query, _) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty { + + var searchQuery: String? + var searchResultsAreEmpty = false + if let searchStateContext = self.searchStateContext, let value = searchStateContext.stateValue { + if case let .contactsSearch(query, _) = searchStateContext.subject { + searchQuery = query + } else if case let .members(_, query) = searchStateContext.subject { + searchQuery = query + } else if case let .channels(_, query) = searchStateContext.subject { + searchQuery = query + } + searchResultsAreEmpty = value.peers.isEmpty + } + + if let searchQuery, searchResultsAreEmpty { let sideInset: CGFloat = 44.0 let emptyAnimationHeight = 148.0 let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0 @@ -1653,7 +1709,7 @@ final class ShareWithPeersScreenComponent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Contacts_Search_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.Contacts_Search_NoResultsQueryDescription(searchQuery).string, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 ) @@ -1740,10 +1796,17 @@ final class ShareWithPeersScreenComponent: Component { } func animateOut(completion: @escaping () -> Void) { + guard let component = self.component else { + return + } self.isDismissed = true - if let controller = self.environment?.controller() { - controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) + if let controller = self.environment?.controller() as? ShareWithPeersScreen { + if case .members = component.stateContext.subject { + } else if case .channels = component.stateContext.subject { + } else { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) + } } var animateOffset: CGFloat = self.bounds.height - self.backgroundView.frame.minY @@ -1804,6 +1867,10 @@ final class ShareWithPeersScreenComponent: Component { contentTransition = .spring(duration: 0.4) } self.currentHasChannels = hasChannels + } else if case .members = component.stateContext.subject { + self.dismissPanGesture?.isEnabled = false + } else if case .channels = component.stateContext.subject { + self.dismissPanGesture?.isEnabled = false } let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -1949,6 +2016,10 @@ final class ShareWithPeersScreenComponent: Component { let placeholder: String switch component.stateContext.subject { + case .members: + placeholder = environment.strings.BoostGift_Subscribers_Search + case .channels: + placeholder = environment.strings.BoostGift_Channels_Search case .chats: placeholder = environment.strings.Story_Privacy_SearchChats default: @@ -1984,15 +2055,28 @@ final class ShareWithPeersScreenComponent: Component { containerSize: CGSize(width: containerWidth, height: 1000.0) ) - if !self.navigationTextFieldState.text.isEmpty { + let searchQuery = self.navigationTextFieldState.text + if !searchQuery.isEmpty { var onlyContacts = false if component.initialPrivacy.base == .closeFriends || component.initialPrivacy.base == .contacts { onlyContacts = true } - if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) { + + let searchSubject: ShareWithPeersScreen.StateContext.Subject + switch component.stateContext.subject { + case let .channels(exclude, _): + searchSubject = .channels(exclude: exclude, searchQuery: searchQuery) + case let .members(peerId, _): + searchSubject = .members(peerId: peerId, searchQuery: searchQuery) + default: + searchSubject = .contactsSearch(query: searchQuery, onlyContacts: onlyContacts) + } + + + if let searchStateContext = self.searchStateContext, searchStateContext.subject == searchSubject { } else { self.searchStateDisposable?.dispose() - let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts), editing: false) + let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: searchSubject) var applyState = false self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -2015,6 +2099,13 @@ final class ShareWithPeersScreenComponent: Component { } transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + if case .members = component.stateContext.subject { + self.dimView .isHidden = true + } else if case .channels = component.stateContext.subject { + self.dimView .isHidden = true + } else { + self.dimView .isHidden = false + } let categoryItemSize = self.categoryTemplateItem.update( transition: .immediate, @@ -2034,7 +2125,7 @@ final class ShareWithPeersScreenComponent: Component { containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) ) var isContactsSearch = false - if let searchStateContext = self.searchStateContext, case .search(_, true) = searchStateContext.subject { + if let searchStateContext = self.searchStateContext, case .contactsSearch(_, true) = searchStateContext.subject { isContactsSearch = true } let peerItemSize = self.peerTemplateItem.update( @@ -2185,7 +2276,12 @@ final class ShareWithPeersScreenComponent: Component { } } - let containerInset: CGFloat = environment.statusBarHeight + 10.0 + var containerInset: CGFloat = environment.statusBarHeight + if case .members = component.stateContext.subject { + } else if case .channels = component.stateContext.subject { + } else { + containerInset += 10.0 + } var navigationHeight: CGFloat = 56.0 let navigationSideInset: CGFloat = 16.0 @@ -2239,6 +2335,7 @@ final class ShareWithPeersScreenComponent: Component { var actionButtonTitle = environment.strings.Story_Privacy_SaveList let title: String + var subtitle: String? switch component.stateContext.subject { case .peers: title = environment.strings.Story_Privacy_PostStoryAs @@ -2266,12 +2363,40 @@ final class ShareWithPeersScreenComponent: Component { case .everyone: title = environment.strings.Story_Privacy_ExcludedPeople } - case .search: + case .contactsSearch: title = "" + case .members: + title = environment.strings.BoostGift_Subscribers_Title + subtitle = environment.strings.BoostGift_Subscribers_Subtitle("\(10)").string + actionButtonTitle = environment.strings.BoostGift_Subscribers_Save + case .channels: + title = environment.strings.BoostGift_Channels_Title + subtitle = environment.strings.BoostGift_Channels_Subtitle("\(component.context.userLimits.maxGiveawayChannelsCount)").string + actionButtonTitle = environment.strings.BoostGift_Channels_Save + } + + let titleComponent: AnyComponent + if let subtitle { + titleComponent = AnyComponent( + List([ + AnyComponentWithIdentity( + id: "title", + component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)) + ), + AnyComponentWithIdentity( + id: "subtitle", + component: AnyComponent(Text(text: subtitle, font: Font.regular(13.0), color: environment.theme.rootController.navigationBar.secondaryTextColor)) + ) + ], + centerAlignment: true) + ) + } else { + titleComponent = AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)) } + let navigationTitleSize = self.navigationTitle.update( transition: .immediate, - component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)), + component: titleComponent, environment: {}, containerSize: CGSize(width: containerWidth - navigationButtonsWidth, height: navigationHeight) ) @@ -2304,7 +2429,11 @@ final class ShareWithPeersScreenComponent: Component { topInset = 0.0 } else { var inset: CGFloat - if case let .stories(editing) = component.stateContext.subject { + if case .members = component.stateContext.subject { + inset = 1000.0 + } else if case .channels = component.stateContext.subject { + inset = 1000.0 + } else if case let .stories(editing) = component.stateContext.subject { if editing { inset = 351.0 inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight @@ -2418,7 +2547,7 @@ final class ShareWithPeersScreenComponent: Component { })) let _ = (peers - |> deliverOnMainQueue).start(next: { [weak controller, weak component] peers in + |> deliverOnMainQueue).start(next: { [weak controller, weak component] peers in guard let controller, let component else { return } @@ -2441,7 +2570,7 @@ final class ShareWithPeersScreenComponent: Component { } if savePeers { let _ = (updatePeersListStoredState(engine: component.context.engine, base: base, peerIds: self.selectedPeers) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).start(completed: { complete() }) } else { @@ -2602,7 +2731,14 @@ final class ShareWithPeersScreenComponent: Component { transition.setPosition(view: self.backgroundView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) - let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset + 10.0), size: CGSize(width: availableSize.width, height: availableSize.height - 10.0)) + var scrollClippingInset: CGFloat = 0.0 + if case .members = component.stateContext.subject { + } else if case .channels = component.stateContext.subject { + } else { + scrollClippingInset = 10.0 + } + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset + scrollClippingInset), size: CGSize(width: availableSize.width, height: availableSize.height - scrollClippingInset)) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) @@ -2653,534 +2789,10 @@ final class ShareWithPeersScreenComponent: Component { } } -public class ShareWithPeersScreen: ViewControllerComponentContainer { - public final class State { - let sendAsPeers: [EnginePeer] - let peers: [EnginePeer] - let peersMap: [EnginePeer.Id: EnginePeer] - let savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] - let presences: [EnginePeer.Id: EnginePeer.Presence] - let participants: [EnginePeer.Id: Int] - let closeFriendsPeers: [EnginePeer] - let grayListPeers: [EnginePeer] - - fileprivate init( - sendAsPeers: [EnginePeer], - peers: [EnginePeer], - peersMap: [EnginePeer.Id: EnginePeer], - savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]], - presences: [EnginePeer.Id: EnginePeer.Presence], - participants: [EnginePeer.Id: Int], - closeFriendsPeers: [EnginePeer], - grayListPeers: [EnginePeer] - ) { - self.sendAsPeers = sendAsPeers - self.peers = peers - self.peersMap = peersMap - self.savedSelectedPeers = savedSelectedPeers - self.presences = presences - self.participants = participants - self.closeFriendsPeers = closeFriendsPeers - self.grayListPeers = grayListPeers - } - } - - public final class StateContext { - public enum Subject: Equatable { - case peers(peers: [EnginePeer], peerId: EnginePeer.Id?) - case stories(editing: Bool) - case chats(blocked: Bool) - case contacts(base: EngineStoryPrivacy.Base) - case search(query: String, onlyContacts: Bool) - } - - fileprivate var stateValue: State? - - public let subject: Subject - public let editing: Bool - public private(set) var initialPeerIds: Set = Set() - fileprivate let blockedPeersContext: BlockedPeersContext? - - private var stateDisposable: Disposable? - private let stateSubject = Promise() - public var state: Signal { - return self.stateSubject.get() - } - private let readySubject = ValuePromise(false, ignoreRepeated: true) - public var ready: Signal { - return self.readySubject.get() - } - - public init( - context: AccountContext, - subject: Subject = .chats(blocked: false), - editing: Bool, - initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:], - initialPeerIds: Set = Set(), - closeFriends: Signal<[EnginePeer], NoError> = .single([]), - adminedChannels: Signal<[EnginePeer], NoError> = .single([]), - blockedPeersContext: BlockedPeersContext? = nil - ) { - self.subject = subject - self.editing = editing - self.initialPeerIds = initialPeerIds - self.blockedPeersContext = blockedPeersContext - - let grayListPeers: Signal<[EnginePeer], NoError> - if let blockedPeersContext { - grayListPeers = blockedPeersContext.state - |> map { state -> [EnginePeer] in - return state.peers.compactMap { $0.peer.flatMap(EnginePeer.init) } - } - } else { - grayListPeers = .single([]) - } - - switch subject { - case let .peers(peers, _): - self.stateDisposable = (.single(peers) - |> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional]), NoError> in - return context.engine.data.subscribe( - EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) - ) - |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) in - return (peers, participantCountMap) - } - } - |> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in - guard let self else { - return - } - var participants: [EnginePeer.Id: Int] = [:] - for (key, value) in participantCounts { - if let value { - participants[key] = value - } - } - - let state = State( - sendAsPeers: peers, - peers: [], - peersMap: [:], - savedSelectedPeers: [:], - presences: [:], - participants: participants, - closeFriendsPeers: [], - grayListPeers: [] - ) - self.stateValue = state - self.stateSubject.set(.single(state)) - - self.readySubject.set(true) - }) - case .stories: - let savedEveryoneExceptionPeers = peersListStoredState(engine: context.engine, base: .everyone) - let savedContactsExceptionPeers = peersListStoredState(engine: context.engine, base: .contacts) - let savedSelectedPeers = peersListStoredState(engine: context.engine, base: .nobody) - - let savedPeers = combineLatest( - savedEveryoneExceptionPeers, - savedContactsExceptionPeers, - savedSelectedPeers - ) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in - var everyone = everyone - if let initialPeerIds = initialSelectedPeers[.everyone] { - everyone = initialPeerIds - } - var everyonePeerSignals: [Signal] = [] - if everyone.count < 3 { - for peerId in everyone { - everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) - } - } - - var contacts = contacts - if let initialPeerIds = initialSelectedPeers[.contacts] { - contacts = initialPeerIds - } - var contactsPeerSignals: [Signal] = [] - if contacts.count < 3 { - for peerId in contacts { - contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) - } - } - - var selected = selected - if let initialPeerIds = initialSelectedPeers[.nobody] { - selected = initialPeerIds - } - var selectedPeerSignals: [Signal] = [] - if selected.count < 3 { - for peerId in selected { - selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) - } - } - return combineLatest( - combineLatest(everyonePeerSignals), - combineLatest(contactsPeerSignals), - combineLatest(selectedPeerSignals) - ) |> map { everyonePeers, contactsPeers, selectedPeers -> ([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]) in - var peersMap: [EnginePeer.Id: EnginePeer] = [:] - for peer in everyonePeers { - if let peer { - peersMap[peer.id] = peer - } - } - for peer in contactsPeers { - if let peer { - peersMap[peer.id] = peer - } - } - for peer in selectedPeers { - if let peer { - peersMap[peer.id] = peer - } - } - return ( - peersMap, - everyone, - contacts, - selected - ) - } - } - - let adminedChannelsWithParticipants = adminedChannels - |> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional]), NoError> in - return context.engine.data.subscribe( - EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) - ) - |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) in - return (peers, participantCountMap) - } - } - - self.stateDisposable = combineLatest( - queue: Queue.mainQueue(), - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), - adminedChannelsWithParticipants, - savedPeers, - closeFriends, - grayListPeers - ) - .start(next: { [weak self] accountPeer, adminedChannelsWithParticipants, savedPeers, closeFriends, grayListPeers in - guard let self else { - return - } - - let (adminedChannels, participantCounts) = adminedChannelsWithParticipants - var participants: [EnginePeer.Id: Int] = [:] - for (key, value) in participantCounts { - if let value { - participants[key] = value - } - } - - var sendAsPeers: [EnginePeer] = [] - if let accountPeer { - sendAsPeers.append(accountPeer) - } - for channel in adminedChannels { - if case let .channel(channel) = channel, channel.hasPermission(.postStories) { - if !sendAsPeers.contains(where: { $0.id == channel.id }) { - sendAsPeers.append(contentsOf: adminedChannels) - } - } - } - - let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers - var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:] - savedSelectedPeers[.everyone] = everyonePeers - savedSelectedPeers[.contacts] = contactsPeers - savedSelectedPeers[.nobody] = selectedPeers - let state = State( - sendAsPeers: sendAsPeers, - peers: [], - peersMap: peersMap, - savedSelectedPeers: savedSelectedPeers, - presences: [:], - participants: participants, - closeFriendsPeers: closeFriends, - grayListPeers: grayListPeers - ) - - self.stateValue = state - self.stateSubject.set(.single(state)) - - self.readySubject.set(true) - }) - case let .chats(isGrayList): - self.stateDisposable = (combineLatest( - context.engine.messages.chatList(group: .root, count: 200) |> take(1), - context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)), - context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))), - grayListPeers - ) - |> mapToSignal { chatList, contacts, initialPeers, grayListPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [EnginePeer]), NoError> in - return context.engine.data.subscribe( - EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) - ) - |> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [EnginePeer]) in - return (chatList, contacts, initialPeers, participantCountMap, grayListPeers) - } - } - |> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, initialPeers, participantCounts, grayListPeers in - guard let self else { - return - } - - var participants: [EnginePeer.Id: Int] = [:] - for (key, value) in participantCounts { - if let value { - participants[key] = value - } - } - - var grayListPeersIds = Set() - for peer in grayListPeers { - grayListPeersIds.insert(peer.id) - } - - var existingIds = Set() - var selectedPeers: [EnginePeer] = [] - - if isGrayList { - self.initialPeerIds = Set(grayListPeers.map { $0.id }) - } - - for item in chatList.items.reversed() { - if let peer = item.renderedPeer.peer { - if self.initialPeerIds.contains(peer.id) || isGrayList && grayListPeersIds.contains(peer.id) { - selectedPeers.append(peer) - existingIds.insert(peer.id) - } - } - } - - for peerId in self.initialPeerIds { - if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer { - selectedPeers.append(peer) - existingIds.insert(peerId) - } - } - - if isGrayList { - for peer in grayListPeers { - if !existingIds.contains(peer.id) { - selectedPeers.append(peer) - existingIds.insert(peer.id) - } - } - } - - var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] - for item in chatList.items { - presences[item.renderedPeer.peerId] = item.presence - } - - var peers: [EnginePeer] = [] - peers = chatList.items.filter { peer in - if let peer = peer.renderedPeer.peer { - if self.initialPeerIds.contains(peer.id) { - return false - } - if peer.id == context.account.peerId { - return false - } - if peer.isService || peer.isDeleted { - return false - } - if case let .user(user) = peer { - if user.botInfo != nil { - return false - } - } - if case let .channel(channel) = peer { - if channel.isForum { - return false - } - if case .broadcast = channel.info { - return false - } - } - return true - } else { - return false - } - }.reversed().compactMap { $0.renderedPeer.peer } - for peer in peers { - existingIds.insert(peer.id) - } - peers.insert(contentsOf: selectedPeers, at: 0) - - let state = State( - sendAsPeers: [], - peers: peers, - peersMap: [:], - savedSelectedPeers: [:], - presences: presences, - participants: participants, - closeFriendsPeers: [], - grayListPeers: grayListPeers - ) - self.stateValue = state - self.stateSubject.set(.single(state)) - - self.readySubject.set(true) - }) - case let .contacts(base): - self.stateDisposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) - ) - |> deliverOnMainQueue).start(next: { [weak self] contactList in - guard let self else { - return - } - - var selectedPeers: [EnginePeer] = [] - if case .closeFriends = base { - for peer in contactList.peers { - if case let .user(user) = peer, user.flags.contains(.isCloseFriend) { - selectedPeers.append(peer) - } - } - self.initialPeerIds = Set(selectedPeers.map { $0.id }) - } else { - for peer in contactList.peers { - if case let .user(user) = peer, initialPeerIds.contains(user.id), !user.isDeleted { - selectedPeers.append(peer) - } - } - self.initialPeerIds = initialPeerIds - } - selectedPeers = selectedPeers.sorted(by: { lhs, rhs in - let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) - if result == .orderedSame { - return lhs.id < rhs.id - } else { - return result == .orderedAscending - } - }) - - var peers: [EnginePeer] = [] - peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) && $0.id != context.account.peerId && !$0.isDeleted }.sorted(by: { lhs, rhs in - let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) - if result == .orderedSame { - return lhs.id < rhs.id - } else { - return result == .orderedAscending - } - }) - peers.insert(contentsOf: selectedPeers, at: 0) - - let state = State( - sendAsPeers: [], - peers: peers, - peersMap: [:], - savedSelectedPeers: [:], - presences: contactList.presences, - participants: [:], - closeFriendsPeers: [], - grayListPeers: [] - ) - - self.stateValue = state - self.stateSubject.set(.single(state)) - - self.readySubject.set(true) - }) - case let .search(query, onlyContacts): - let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> - if onlyContacts { - signal = combineLatest( - context.engine.contacts.searchLocalPeers(query: query), - context.engine.contacts.searchContacts(query: query) - ) - |> map { peers, contacts in - let contactIds = Set(contacts.0.map { $0.id }) - return (peers.filter { contactIds.contains($0.peerId) }, [:], [:]) - } - } else { - signal = context.engine.contacts.searchLocalPeers(query: query) - |> mapToSignal { peers in - return context.engine.data.subscribe( - EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Presence.init)), - EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) - ) - |> map { presenceMap, participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in - return (peers, presenceMap, participantCountMap) - } - } - } - self.stateDisposable = (signal - |> deliverOnMainQueue).start(next: { [weak self] peers, presenceMap, participantCounts in - guard let self else { - return - } - - var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] - for (key, value) in presenceMap { - if let value { - presences[key] = value - } - } - - var participants: [EnginePeer.Id: Int] = [:] - for (key, value) in participantCounts { - if let value { - participants[key] = value - } - } - - let state = State( - sendAsPeers: [], - peers: peers.compactMap { $0.peer }.filter { peer in - if case let .user(user) = peer { - if user.id == context.account.peerId { - return false - } else if user.botInfo != nil { - return false - } else if peer.isService { - return false - } else if user.isDeleted { - return false - } else { - return true - } - } else if case let .channel(channel) = peer { - if channel.isForum { - return false - } - if case .broadcast = channel.info { - return false - } - return true - } else { - return true - } - }, - peersMap: [:], - savedSelectedPeers: [:], - presences: presences, - participants: participants, - closeFriendsPeers: [], - grayListPeers: [] - ) - self.stateValue = state - self.stateSubject.set(.single(state)) - - self.readySubject.set(true) - }) - } - } - - deinit { - self.stateDisposable?.dispose() - } - } - +public class ShareWithPeersScreen: ViewControllerComponentContainer { private let context: AccountContext + private var isCustomModal = true private var isDismissed: Bool = false public var dismissed: () -> Void = {} @@ -3195,8 +2807,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { mentions: [String] = [], stateContext: StateContext, completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, - editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, - editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in }, + editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in }, peerCompletion: @escaping (EnginePeer.Id) -> Void = { _ in } ) { self.context = context @@ -3340,6 +2952,12 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } } + var theme: ViewControllerComponentContainer.Theme = .dark + if case .members = stateContext.subject { + theme = .default + } else if case .channels = stateContext.subject { + theme = .default + } super.init(context: context, component: ShareWithPeersScreenComponent( context: context, stateContext: stateContext, @@ -3355,10 +2973,18 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { editCategory: editCategory, editBlockedPeers: editBlockedPeers, peerCompletion: peerCompletion - ), navigationBarAppearance: .none, theme: .dark) + ), navigationBarAppearance: .none, theme: theme) self.statusBar.statusBarStyle = .Ignore - self.navigationPresentation = .flatModal + if case .members = stateContext.subject { + self.navigationPresentation = .modal + self.isCustomModal = false + } else if case .channels = stateContext.subject { + self.navigationPresentation = .modal + self.isCustomModal = false + } else { + self.navigationPresentation = .flatModal + } self.blocksBackgroundWhenInOverlay = true self.automaticallyControlPresentationContextLayout = false self.lockOrientation = true @@ -3369,19 +2995,29 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } deinit { + if !self.isDismissed { + self.isDismissed = true + self.dismissed() + } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + + var updatedLayout = layout + updatedLayout.intrinsicInsets.bottom += 66.0 + self.presentationContext.containerLayoutUpdated(updatedLayout, transition: transition) } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.view.disablesInteractiveModalDismiss = true - - if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View { - componentView.animateIn() + if self.isCustomModal { + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View { + componentView.animateIn() + } } } @@ -3410,57 +3046,19 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.isDismissed = true self.view.endEditing(true) - - if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View { - componentView.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) + + if self.isCustomModal { + if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } } else { - self.dismiss(animated: false) + self.dismiss(animated: true) } } } } - -final class PeersListStoredState: Codable { - private enum CodingKeys: String, CodingKey { - case peerIds - } - - public let peerIds: [EnginePeer.Id] - - public init(peerIds: [EnginePeer.Id]) { - self.peerIds = peerIds - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.peerIds = try container.decode([Int64].self, forKey: .peerIds).map { EnginePeer.Id($0) } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds) - } -} - -private func peersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base) -> Signal<[EnginePeer.Id], NoError> { - let key = EngineDataBuffer(length: 4) - key.setInt32(0, value: base.rawValue) - - return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key)) - |> map { entry -> [EnginePeer.Id] in - return entry?.get(PeersListStoredState.self)?.peerIds ?? [] - } -} - -private func updatePeersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal { - let key = EngineDataBuffer(length: 4) - key.setInt32(0, value: base.rawValue) - - let state = PeersListStoredState(peerIds: peerIds) - return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key, item: state) -} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift new file mode 100644 index 00000000000..0a2bde11db5 --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -0,0 +1,723 @@ +import Foundation +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramUIPreferences +import TemporaryCachedPeerDataManager +import Postbox + +public extension ShareWithPeersScreen { + final class State { + let sendAsPeers: [EnginePeer] + let peers: [EnginePeer] + let peersMap: [EnginePeer.Id: EnginePeer] + let savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] + let presences: [EnginePeer.Id: EnginePeer.Presence] + let invitedAt: [EnginePeer.Id: Int32] + let participants: [EnginePeer.Id: Int] + let closeFriendsPeers: [EnginePeer] + let grayListPeers: [EnginePeer] + + fileprivate init( + sendAsPeers: [EnginePeer] = [], + peers: [EnginePeer], + peersMap: [EnginePeer.Id: EnginePeer] = [:], + savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:], + presences: [EnginePeer.Id: EnginePeer.Presence] = [:], + invitedAt: [EnginePeer.Id: Int32] = [:], + participants: [EnginePeer.Id: Int] = [:], + closeFriendsPeers: [EnginePeer] = [], + grayListPeers: [EnginePeer] = [] + ) { + self.sendAsPeers = sendAsPeers + self.peers = peers + self.peersMap = peersMap + self.savedSelectedPeers = savedSelectedPeers + self.presences = presences + self.invitedAt = invitedAt + self.participants = participants + self.closeFriendsPeers = closeFriendsPeers + self.grayListPeers = grayListPeers + } + } + + final class StateContext { + public enum Subject: Equatable { + case peers(peers: [EnginePeer], peerId: EnginePeer.Id?) + case stories(editing: Bool) + case chats(blocked: Bool) + case contacts(base: EngineStoryPrivacy.Base) + case contactsSearch(query: String, onlyContacts: Bool) + case members(peerId: EnginePeer.Id, searchQuery: String?) + case channels(exclude: Set, searchQuery: String?) + } + + var stateValue: State? + + public let subject: Subject + public let editing: Bool + public private(set) var initialPeerIds: Set = Set() + let blockedPeersContext: BlockedPeersContext? + + private var stateDisposable: Disposable? + private let stateSubject = Promise() + public var state: Signal { + return self.stateSubject.get() + } + private var listControl: PeerChannelMemberCategoryControl? + + private let readySubject = ValuePromise(false, ignoreRepeated: true) + public var ready: Signal { + return self.readySubject.get() + } + + public init( + context: AccountContext, + subject: Subject = .chats(blocked: false), + editing: Bool = false, + initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:], + initialPeerIds: Set = Set(), + closeFriends: Signal<[EnginePeer], NoError> = .single([]), + adminedChannels: Signal<[EnginePeer], NoError> = .single([]), + blockedPeersContext: BlockedPeersContext? = nil + ) { + self.subject = subject + self.editing = editing + self.initialPeerIds = initialPeerIds + self.blockedPeersContext = blockedPeersContext + + let grayListPeers: Signal<[EnginePeer], NoError> + if let blockedPeersContext { + grayListPeers = blockedPeersContext.state + |> map { state -> [EnginePeer] in + return state.peers.compactMap { $0.peer.flatMap(EnginePeer.init) } + } + } else { + grayListPeers = .single([]) + } + + switch subject { + case let .peers(peers, _): + self.stateDisposable = (.single(peers) + |> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional]), NoError> in + return context.engine.data.subscribe( + EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) in + return (peers, participantCountMap) + } + } + |> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in + guard let self else { + return + } + var participants: [EnginePeer.Id: Int] = [:] + for (key, value) in participantCounts { + if let value { + participants[key] = value + } + } + + let state = State( + sendAsPeers: peers, + peers: [], + participants: participants + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case .stories: + let savedEveryoneExceptionPeers = peersListStoredState(engine: context.engine, base: .everyone) + let savedContactsExceptionPeers = peersListStoredState(engine: context.engine, base: .contacts) + let savedSelectedPeers = peersListStoredState(engine: context.engine, base: .nobody) + + let savedPeers = combineLatest( + savedEveryoneExceptionPeers, + savedContactsExceptionPeers, + savedSelectedPeers + ) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in + var everyone = everyone + if let initialPeerIds = initialSelectedPeers[.everyone] { + everyone = initialPeerIds + } + var everyonePeerSignals: [Signal] = [] + if everyone.count < 3 { + for peerId in everyone { + everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + } + } + + var contacts = contacts + if let initialPeerIds = initialSelectedPeers[.contacts] { + contacts = initialPeerIds + } + var contactsPeerSignals: [Signal] = [] + if contacts.count < 3 { + for peerId in contacts { + contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + } + } + + var selected = selected + if let initialPeerIds = initialSelectedPeers[.nobody] { + selected = initialPeerIds + } + var selectedPeerSignals: [Signal] = [] + if selected.count < 3 { + for peerId in selected { + selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + } + } + return combineLatest( + combineLatest(everyonePeerSignals), + combineLatest(contactsPeerSignals), + combineLatest(selectedPeerSignals) + ) |> map { everyonePeers, contactsPeers, selectedPeers -> ([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]) in + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for peer in everyonePeers { + if let peer { + peersMap[peer.id] = peer + } + } + for peer in contactsPeers { + if let peer { + peersMap[peer.id] = peer + } + } + for peer in selectedPeers { + if let peer { + peersMap[peer.id] = peer + } + } + return ( + peersMap, + everyone, + contacts, + selected + ) + } + } + + let adminedChannelsWithParticipants = adminedChannels + |> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional]), NoError> in + return context.engine.data.subscribe( + EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) in + return (peers, participantCountMap) + } + } + + self.stateDisposable = combineLatest( + queue: Queue.mainQueue(), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + adminedChannelsWithParticipants, + savedPeers, + closeFriends, + grayListPeers + ) + .start(next: { [weak self] accountPeer, adminedChannelsWithParticipants, savedPeers, closeFriends, grayListPeers in + guard let self else { + return + } + + let (adminedChannels, participantCounts) = adminedChannelsWithParticipants + var participants: [EnginePeer.Id: Int] = [:] + for (key, value) in participantCounts { + if let value { + participants[key] = value + } + } + + var sendAsPeers: [EnginePeer] = [] + if let accountPeer { + sendAsPeers.append(accountPeer) + } + for channel in adminedChannels { + if case let .channel(channel) = channel, channel.hasPermission(.postStories) { + if !sendAsPeers.contains(where: { $0.id == channel.id }) { + sendAsPeers.append(contentsOf: adminedChannels) + } + } + } + + let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers + var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:] + savedSelectedPeers[.everyone] = everyonePeers + savedSelectedPeers[.contacts] = contactsPeers + savedSelectedPeers[.nobody] = selectedPeers + let state = State( + sendAsPeers: sendAsPeers, + peers: [], + peersMap: peersMap, + savedSelectedPeers: savedSelectedPeers, + participants: participants, + closeFriendsPeers: closeFriends, + grayListPeers: grayListPeers + ) + + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case let .chats(isGrayList): + self.stateDisposable = (combineLatest( + context.engine.messages.chatList(group: .root, count: 200) |> take(1), + context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)), + context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))), + grayListPeers + ) + |> mapToSignal { chatList, contacts, initialPeers, grayListPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [EnginePeer]), NoError> in + return context.engine.data.subscribe( + EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [EnginePeer]) in + return (chatList, contacts, initialPeers, participantCountMap, grayListPeers) + } + } + |> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, initialPeers, participantCounts, grayListPeers in + guard let self else { + return + } + + var participants: [EnginePeer.Id: Int] = [:] + for (key, value) in participantCounts { + if let value { + participants[key] = value + } + } + + var grayListPeersIds = Set() + for peer in grayListPeers { + grayListPeersIds.insert(peer.id) + } + + var existingIds = Set() + var selectedPeers: [EnginePeer] = [] + + if isGrayList { + self.initialPeerIds = Set(grayListPeers.map { $0.id }) + } + + for item in chatList.items.reversed() { + if let peer = item.renderedPeer.peer { + if self.initialPeerIds.contains(peer.id) || isGrayList && grayListPeersIds.contains(peer.id) { + selectedPeers.append(peer) + existingIds.insert(peer.id) + } + } + } + + for peerId in self.initialPeerIds { + if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer { + selectedPeers.append(peer) + existingIds.insert(peerId) + } + } + + if isGrayList { + for peer in grayListPeers { + if !existingIds.contains(peer.id) { + selectedPeers.append(peer) + existingIds.insert(peer.id) + } + } + } + + var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] + for item in chatList.items { + presences[item.renderedPeer.peerId] = item.presence + } + + var peers: [EnginePeer] = [] + peers = chatList.items.filter { peer in + if let peer = peer.renderedPeer.peer { + if self.initialPeerIds.contains(peer.id) { + return false + } + if peer.id == context.account.peerId { + return false + } + if peer.isService || peer.isDeleted { + return false + } + if case let .user(user) = peer { + if user.botInfo != nil { + return false + } + } + if case let .channel(channel) = peer { + if channel.isForum { + return false + } + if case .broadcast = channel.info { + return false + } + } + return true + } else { + return false + } + }.reversed().compactMap { $0.renderedPeer.peer } + for peer in peers { + existingIds.insert(peer.id) + } + peers.insert(contentsOf: selectedPeers, at: 0) + + let state = State( + peers: peers, + presences: presences, + participants: participants, + grayListPeers: grayListPeers + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case let .contacts(base): + self.stateDisposable = (context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) + ) + |> deliverOnMainQueue).start(next: { [weak self] contactList in + guard let self else { + return + } + + var selectedPeers: [EnginePeer] = [] + if case .closeFriends = base { + for peer in contactList.peers { + if case let .user(user) = peer, user.flags.contains(.isCloseFriend) { + selectedPeers.append(peer) + } + } + self.initialPeerIds = Set(selectedPeers.map { $0.id }) + } else { + for peer in contactList.peers { + if case let .user(user) = peer, initialPeerIds.contains(user.id), !user.isDeleted { + selectedPeers.append(peer) + } + } + self.initialPeerIds = initialPeerIds + } + selectedPeers = selectedPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) + if result == .orderedSame { + return lhs.id < rhs.id + } else { + return result == .orderedAscending + } + }) + + var peers: [EnginePeer] = [] + peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) && $0.id != context.account.peerId && !$0.isDeleted }.sorted(by: { lhs, rhs in + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) + if result == .orderedSame { + return lhs.id < rhs.id + } else { + return result == .orderedAscending + } + }) + peers.insert(contentsOf: selectedPeers, at: 0) + + let state = State( + peers: peers, + presences: contactList.presences + ) + + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case let .contactsSearch(query, onlyContacts): + let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> + if onlyContacts { + signal = combineLatest( + context.engine.contacts.searchLocalPeers(query: query), + context.engine.contacts.searchContacts(query: query) + ) + |> map { peers, contacts in + let contactIds = Set(contacts.0.map { $0.id }) + return (peers.filter { contactIds.contains($0.peerId) }, [:], [:]) + } + } else { + signal = context.engine.contacts.searchLocalPeers(query: query) + |> mapToSignal { peers in + return context.engine.data.subscribe( + EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Presence.init)), + EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { presenceMap, participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in + return (peers, presenceMap, participantCountMap) + } + } + } + self.stateDisposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] peers, presenceMap, participantCounts in + guard let self else { + return + } + + var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] + for (key, value) in presenceMap { + if let value { + presences[key] = value + } + } + + var participants: [EnginePeer.Id: Int] = [:] + for (key, value) in participantCounts { + if let value { + participants[key] = value + } + } + + let state = State( + peers: peers.compactMap { $0.peer }.filter { peer in + if case let .user(user) = peer { + if user.id == context.account.peerId { + return false + } else if user.botInfo != nil { + return false + } else if peer.isService { + return false + } else if user.isDeleted { + return false + } else { + return true + } + } else if case let .channel(channel) = peer { + if channel.isForum { + return false + } + if case .broadcast = channel.info { + return false + } + return true + } else { + return true + } + }, + presences: presences, + participants: participants + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case let .members(peerId, searchQuery): + let membersState = Promise() + let contactsState = Promise() + + let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) + disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: searchQuery, updated: { state in + membersState.set(.single(state)) + }) + + let contactsDisposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: searchQuery, updated: { state in + contactsState.set(.single(state)) + }) + + let dataDisposable = combineLatest( + queue: Queue.mainQueue(), + contactsState.get(), + membersState.get() + ).startStrict(next: { [weak self] contactsState, memberState in + guard let self else { + return + } + var peers: [EnginePeer] = [] + var invitedAt: [EnginePeer.Id: Int32] = [:] + + var existingPeersIds = Set() + for participant in contactsState.list { + if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil { + continue + } + + if case let .member(_, date, _, _, _) = participant.participant { + invitedAt[participant.peer.id] = date + } else { + continue + } + + peers.append(EnginePeer(participant.peer)) + existingPeersIds.insert(participant.peer.id) + } + + for participant in memberState.list { + if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil { + continue + } + if let user = participant.peer as? TelegramUser, user.botInfo != nil { + continue + } + + if case let .member(_, date, _, _, _) = participant.participant { + invitedAt[participant.peer.id] = date + } else { + continue + } + + peers.append(EnginePeer(participant.peer)) + } + + let state = State( + peers: peers, + invitedAt: invitedAt + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + + let combinedDisposable = DisposableSet() + combinedDisposable.add(contactsDisposableAndLoadMoreControl.0) + combinedDisposable.add(disposableAndLoadMoreControl.0) + combinedDisposable.add(dataDisposable) + + self.stateDisposable = combinedDisposable + + self.listControl = disposableAndLoadMoreControl.1 + case let .channels(excludePeerIds, searchQuery): + self.stateDisposable = (combineLatest( + context.engine.messages.chatList(group: .root, count: 500) |> take(1), + context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))) + ) + |> mapToSignal { chatList, initialPeers -> Signal<(EngineChatList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> in + return context.engine.data.subscribe( + EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { participantCountMap -> (EngineChatList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in + return (chatList, initialPeers, participantCountMap) + } + } + |> deliverOnMainQueue).start(next: { [weak self] chatList, initialPeers, participantCounts in + guard let self else { + return + } + + var participants: [EnginePeer.Id: Int] = [:] + for (key, value) in participantCounts { + if let value { + participants[key] = value + } + } + + var existingIds = Set() + var selectedPeers: [EnginePeer] = [] + + for item in chatList.items.reversed() { + if let peer = item.renderedPeer.peer { + if self.initialPeerIds.contains(peer.id) { + selectedPeers.append(peer) + existingIds.insert(peer.id) + } + } + } + + for peerId in self.initialPeerIds { + if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer { + selectedPeers.append(peer) + existingIds.insert(peerId) + } + } + + let queryTokens = stringIndexTokens(searchQuery ?? "", transliteration: .combined) + func peerMatchesTokens(peer: EnginePeer, tokens: [ValueBoxKey]) -> Bool { + if matchStringIndexTokens(peer.indexName._asIndexName().indexTokens, with: queryTokens) { + return true + } + return false + } + + var peers: [EnginePeer] = [] + peers = chatList.items.filter { peer in + if let peer = peer.renderedPeer.peer { + if excludePeerIds.contains(peer.id) { + return false + } + if let _ = searchQuery, !peerMatchesTokens(peer: peer, tokens: queryTokens) { + return false + } + if self.initialPeerIds.contains(peer.id) { + return false + } + if case let .channel(channel) = peer, case .broadcast = channel.info { + return true + } + return false + } else { + return false + } + }.reversed().compactMap { $0.renderedPeer.peer } + for peer in peers { + existingIds.insert(peer.id) + } + peers.insert(contentsOf: selectedPeers, at: 0) + + let state = State( + peers: peers, + participants: participants + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + } + } + + deinit { + self.stateDisposable?.dispose() + } + } +} + +final class PeersListStoredState: Codable { + private enum CodingKeys: String, CodingKey { + case peerIds + } + + public let peerIds: [EnginePeer.Id] + + public init(peerIds: [EnginePeer.Id]) { + self.peerIds = peerIds + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.peerIds = try container.decode([Int64].self, forKey: .peerIds).map { EnginePeer.Id($0) } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds) + } +} + +private func peersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base) -> Signal<[EnginePeer.Id], NoError> { + let key = EngineDataBuffer(length: 4) + key.setInt32(0, value: base.rawValue) + + return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key)) + |> map { entry -> [EnginePeer.Id] in + return entry?.get(PeersListStoredState.self)?.peerIds ?? [] + } +} + +func updatePeersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal { + let key = EngineDataBuffer(length: 4) + key.setInt32(0, value: base.rawValue) + + let state = PeersListStoredState(peerIds: peerIds) + return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key, item: state) +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 228691d219d..aa411a98e33 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -2277,7 +2277,7 @@ final class StorageUsageScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } private func reloadStats(firstTime: Bool, completion: @escaping () -> Void) { @@ -2571,7 +2571,7 @@ final class StorageUsageScreenComponent: Component { navigationController: navigationController, context: component.context, chatLocation: chatLocation, - subject: .message(id: .id(message.id), highlight: true, timecode: nil), + subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always )) }) @@ -2674,7 +2674,7 @@ final class StorageUsageScreenComponent: Component { navigationController: navigationController, context: component.context, chatLocation: chatLocation, - subject: .message(id: .id(message.id), highlight: true, timecode: nil), + subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always )) }) diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index f85371ac240..c2d5ebb1181 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -43,6 +43,11 @@ public final class PeerListItemComponent: Component { case editing(isSelected: Bool, isTinted: Bool) } + public enum SelectionPosition: Equatable { + case left + case right + } + public enum SubtitleAccessory: Equatable { case none case checks @@ -101,6 +106,8 @@ public final class PeerListItemComponent: Component { let rightAccessory: RightAccessory let reaction: Reaction? let selectionState: SelectionState + let selectionPosition: SelectionPosition + let isEnabled: Bool let hasNext: Bool let action: (EnginePeer) -> Void let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? @@ -121,6 +128,8 @@ public final class PeerListItemComponent: Component { rightAccessory: RightAccessory = .none, reaction: Reaction? = nil, selectionState: SelectionState, + selectionPosition: SelectionPosition = .left, + isEnabled: Bool = true, hasNext: Bool, action: @escaping (EnginePeer) -> Void, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, @@ -140,6 +149,8 @@ public final class PeerListItemComponent: Component { self.rightAccessory = rightAccessory self.reaction = reaction self.selectionState = selectionState + self.selectionPosition = selectionPosition + self.isEnabled = isEnabled self.hasNext = hasNext self.action = action self.contextAction = contextAction @@ -189,6 +200,12 @@ public final class PeerListItemComponent: Component { if lhs.selectionState != rhs.selectionState { return false } + if lhs.selectionPosition != rhs.selectionPosition { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } if lhs.hasNext != rhs.hasNext { return false } @@ -411,6 +428,8 @@ public final class PeerListItemComponent: Component { self.component = component self.state = state + self.containerButton.alpha = component.isEnabled ? 1.0 : 0.3 + self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil && component.openStories != nil let labelData: (String, Bool) @@ -457,9 +476,17 @@ public final class PeerListItemComponent: Component { var avatarLeftInset: CGFloat = component.sideInset + 10.0 if case let .editing(isSelected, isTinted) = component.selectionState { - leftInset += 44.0 - avatarLeftInset += 44.0 let checkSize: CGFloat = 22.0 + let checkOriginX: CGFloat + switch component.selectionPosition { + case .left: + leftInset += 44.0 + avatarLeftInset += 44.0 + checkOriginX = floor((54.0 - checkSize) * 0.5) + case .right: + rightInset += 44.0 + checkOriginX = availableSize.width - 11.0 - checkSize + } let checkLayer: CheckLayer if let current = self.checkLayer { @@ -484,7 +511,7 @@ public final class PeerListItemComponent: Component { checkLayer.setSelected(isSelected, animated: false) checkLayer.setNeedsDisplay() } - transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: checkOriginX, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) } else { if let checkLayer = self.checkLayer { self.checkLayer = nil @@ -568,6 +595,11 @@ public final class PeerListItemComponent: Component { } else { titleAvailableWidth -= 20.0 } + + if statusIcon != nil { + titleAvailableWidth -= 14.0 + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 415c3dca0ce..b7c19852161 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -91,6 +91,7 @@ swift_library( "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/OptionButtonComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/Components/BalancedTextComponent", "//submodules/AnimatedCountLabelNode", "//submodules/StickerResources", ], diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift index 02423630de7..1fad133d46c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift @@ -27,10 +27,12 @@ final class MediaNavigationStripComponent: Component { let index: Int let count: Int + let isSeeking: Bool - init(index: Int, count: Int) { + init(index: Int, count: Int, isSeeking: Bool) { self.index = index self.count = count + self.isSeeking = isSeeking } static func ==(lhs: MediaNavigationStripComponent, rhs: MediaNavigationStripComponent) -> Bool { @@ -40,6 +42,9 @@ final class MediaNavigationStripComponent: Component { if lhs.count != rhs.count { return false } + if lhs.isSeeking != rhs.isSeeking { + return false + } return true } @@ -155,12 +160,18 @@ final class MediaNavigationStripComponent: Component { return } + guard !self.isTransitioning else { + return + } + let itemFrame = itemLayer.bounds transition.setFrame(layer: itemLayer.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value * itemFrame.width, height: itemFrame.height))) itemLayer.updateIsBuffering(size: itemFrame.size, isBuffering: isBuffering) } + private var isTransitioning = false func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component self.component = component let environment = environment[EnvironmentType.self].value @@ -169,6 +180,8 @@ final class MediaNavigationStripComponent: Component { let itemHeight: CGFloat = 2.0 let minItemWidth: CGFloat = 2.0 + var didSetCompletion = false + var validIndices: [Int] = [] if component.count != 0 { let idealItemWidth: CGFloat = (availableSize.width - CGFloat(component.count - 1) * spacing) / CGFloat(component.count) @@ -205,8 +218,11 @@ final class MediaNavigationStripComponent: Component { if i >= component.count { continue } - let itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight)) - if itemFrame.maxY < 0.0 || itemFrame.minY >= availableSize.width { + var itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight)) + if component.isSeeking { + itemFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: 6.0)) + } + if itemFrame.maxX < 0.0 || itemFrame.minX >= availableSize.width { continue } @@ -219,11 +235,21 @@ final class MediaNavigationStripComponent: Component { itemLayer = ItemLayer() self.layer.addSublayer(itemLayer) self.visibleItems[i] = itemLayer - itemLayer.cornerRadius = itemHeight * 0.5 } + transition.setFrame(layer: itemLayer, frame: itemFrame) - + transition.setCornerRadius(layer: itemLayer, cornerRadius: itemFrame.height * 0.5) + transition.setCornerRadius(layer: itemLayer.foregroundLayer, cornerRadius: itemFrame.height * 0.5, completion: transition.animation.isImmediate || didSetCompletion ? nil : { [weak self] _ in + if let self { + self.isTransitioning = false + } + }) + if !transition.animation.isImmediate && component.isSeeking != previousComponent?.isSeeking { + self.isTransitioning = true + didSetCompletion = true + } + itemLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor itemLayer.foregroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 1.0).cgColor @@ -239,6 +265,9 @@ final class MediaNavigationStripComponent: Component { } transition.setFrame(layer: itemLayer.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: itemProgress * itemFrame.width, height: itemFrame.height))) + + transition.setAlpha(layer: itemLayer, alpha: !component.isSeeking || i == component.index ? 1.0 : 0.0) + itemLayer.updateIsBuffering(size: itemFrame.size, isBuffering: itemIsBuffering) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 1132b0f4d44..7f2e080d873 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -92,6 +92,8 @@ private final class MuteMonitor { private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { var shouldBegin: ((UITouch) -> Bool)? var updateIsTracking: ((CGPoint?) -> Void)? + var updatePanMove: ((CGPoint, CGPoint) -> Void)? + var updatePanEnded: (() -> Void)? override var state: UIGestureRecognizer.State { didSet { @@ -110,6 +112,8 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { private var isTracking: Bool = false private var isValidated: Bool = false + private var initialLocation: CGPoint? + override func reset() { super.reset() @@ -134,10 +138,33 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer { if !self.isTracking { self.isTracking = true - self.updateIsTracking?(touches.first?.location(in: self.view)) + self.initialLocation = touches.first?.location(in: self.view) + self.updateIsTracking?(initialLocation) + } + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + if self.isValidated { + super.touchesMoved(touches, with: event) + + if let location = touches.first?.location(in: self.view), let initialLocation = self.initialLocation { + self.updatePanMove?(initialLocation, CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)) } } } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.updatePanEnded?() + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.updatePanEnded?() + } } private final class StoryPinchGesture: UIPinchGestureRecognizer { @@ -392,8 +419,13 @@ private final class StoryContainerScreenComponent: Component { var longPressRecognizer: StoryLongPressRecognizer? private var pendingNavigationToItemId: (peerId: EnginePeer.Id, id: Int32)? + + private let interactionGuide = ComponentView() + private var isDisplayingInteractionGuide: Bool = false + private var displayInteractionGuideDisposable: Disposable? - private var didDisplayReactionTooltip: Bool = false + private var previousSeekTime: Double? + private var initialSeekTimestamp: Double? override init(frame: CGRect) { self.backgroundLayer = SimpleLayer() @@ -418,6 +450,9 @@ private final class StoryContainerScreenComponent: Component { guard let self, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return [] } + if self.isDisplayingInteractionGuide { + return [] + } if let environment = self.environment, case .regular = environment.metrics.widthClass { } else { if !itemSetComponentView.isPointInsideContentArea(point: self.convert(point, to: itemSetComponentView)) { @@ -466,6 +501,60 @@ private final class StoryContainerScreenComponent: Component { } } } + longPressRecognizer.updatePanMove = { [weak self] initialLocation, translation in + guard let self else { + return + } + guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + return + } + guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View else { + return + } + + var apply = true + let currentTime = CACurrentMediaTime() + if let previousTime = self.previousSeekTime, currentTime - previousTime < 0.1 { + apply = false + } + if apply { + self.previousSeekTime = currentTime + } + + let initialSeekTimestamp: Double + if let current = self.initialSeekTimestamp { + initialSeekTimestamp = current + } else { + initialSeekTimestamp = visibleItemView.effectiveTimestamp + self.initialSeekTimestamp = initialSeekTimestamp + } + + let duration = visibleItemView.effectiveDuration + let timestamp: Double + if translation.x > 0.0 { + let fraction = translation.x / (self.bounds.width / 2.0) + timestamp = initialSeekTimestamp + duration * fraction + } else { + let fraction = translation.x / (self.bounds.width / 2.0) + timestamp = initialSeekTimestamp + duration * fraction + } + visibleItemView.seekTo(max(0.0, min(duration, timestamp)), apply: apply) + } + longPressRecognizer.updatePanEnded = { [weak self] in + guard let self else { + return + } + self.initialSeekTimestamp = nil + self.previousSeekTime = nil + + guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + return + } + guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View else { + return + } + visibleItemView.seekEnded() + } longPressRecognizer.shouldBegin = { [weak self] touch in guard let self else { return false @@ -490,6 +579,9 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return false } + if self.isDisplayingInteractionGuide { + return false + } if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { let itemLocation = self.convert(pinchLocation, to: itemSetComponentView) @@ -634,6 +726,7 @@ private final class StoryContainerScreenComponent: Component { self.headphonesDisposable?.dispose() self.stealthModeDisposable?.dispose() self.stealthModeTimer?.invalidate() + self.displayInteractionGuideDisposable?.dispose() } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { @@ -919,17 +1012,15 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return } - if !value { + if !value && !self.isDisplayingInteractionGuide { if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { currentItemView.maybeDisplayReactionTooltip() } } - - self.didDisplayReactionTooltip = true }) }) } - + func animateOut(completion: @escaping () -> Void) { self.isAnimatingOut = true @@ -1112,6 +1203,22 @@ private final class StoryContainerScreenComponent: Component { } }) + let accountManager = component.context.sharedContext.accountManager + self.displayInteractionGuideDisposable = (ApplicationSpecificNotice.displayStoryInteractionGuide(accountManager: accountManager) + |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + if !value { + self.isDisplayingInteractionGuide = true + if update { + self.state?.updated(transition: .immediate) + } + + let _ = ApplicationSpecificNotice.setDisplayStoryInteractionGuide(accountManager: accountManager).startStandalone() + } + }) + update = true } @@ -1290,6 +1397,9 @@ private final class StoryContainerScreenComponent: Component { if self.pendingNavigationToItemId != nil { isProgressPaused = true } + if self.isDisplayingInteractionGuide { + isProgressPaused = true + } var contentDerivedBottomInset: CGFloat = environment.safeInsets.bottom @@ -1698,6 +1808,38 @@ private final class StoryContainerScreenComponent: Component { controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition) } + if self.isDisplayingInteractionGuide { + let _ = self.interactionGuide.update( + transition: .immediate, + component: AnyComponent( + StoryInteractionGuideComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + action: { [weak self] in + self?.isDisplayingInteractionGuide = false + self?.state?.updated() + } + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.interactionGuide.view as? StoryInteractionGuideComponent.View { + if view.superview == nil { + self.addSubview(view) + + view.animateIn() + } + view.layer.zPosition = 1000.0 + view.frame = CGRect(origin: .zero, size: availableSize) + } + } else if let view = self.interactionGuide.view as? StoryInteractionGuideComponent.View, view.superview != nil { + view.animateOut(completion: { + view.removeFromSuperview() + }) + } + return availableSize } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index eb95273096d..6f7cc5ec629 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -540,7 +540,8 @@ final class StoryContentCaptionComponent: Component { fixedFont: Font.monospace(16.0), blockQuoteFont: Font.monospace(16.0), message: nil, - entityFiles: component.entityFiles + entityFiles: component.entityFiles, + adjustQuoteFontSize: true ) let truncationToken = NSMutableAttributedString() @@ -728,7 +729,7 @@ final class StoryContentCaptionComponent: Component { self.textSelectionKnobContainer.addSubview(textSelectionKnobSurface) } - let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor), strings: component.strings, textNode: textNode, updateIsActive: { [weak self] value in + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor, isDark: true), strings: component.strings, textNode: textNode, updateIsActive: { [weak self] value in guard let self else { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift new file mode 100644 index 00000000000..08c9c400d7c --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift @@ -0,0 +1,450 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import MultilineTextComponent +import BalancedTextComponent +import LottieComponent + +final class StoryInteractionGuideComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let action: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.action = action + } + + static func ==(lhs: StoryInteractionGuideComponent, rhs: StoryInteractionGuideComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: UIView { + private var component: StoryInteractionGuideComponent? + private weak var state: EmptyComponentState? + + private let effectView: UIVisualEffectView + private let containerView = UIView() + private let titleLabel = ComponentView() + private let descriptionLabel = ComponentView() + private let guideItems = ComponentView() + private let proceedButton = ComponentView() + + var currentIndex = 0 + + override init(frame: CGRect) { + self.effectView = UIVisualEffectView(effect: nil) + + super.init(frame: frame) + + self.addSubview(self.effectView) + self.addSubview(self.containerView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleTap() { + if let component = self.component { + component.action() + } + } + + var didAnimateOut = false + + func animateIn() { + self.didAnimateOut = false + UIView.animate(withDuration: 0.2) { + self.effectView.effect = UIBlurEffect(style: .dark) + } + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.containerView.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: @escaping () -> Void) { + guard !self.didAnimateOut else { + return + } + self.didAnimateOut = true + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerView.layer.animateScale(from: 1.0, to: 1.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + + func update(component: StoryInteractionGuideComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let strings = component.strings + + let sideInset: CGFloat = min(48.0, floor(availableSize.width * 0.1)) + + let items: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity( + id: "forward", + component: AnyComponent( + GuideItemComponent( + context: component.context, + title: strings.Story_Guide_ForwardTitle, + text: strings.Story_Guide_ForwardDescription, + animationName: "story_forward", + isPlaying: self.currentIndex == 0, + playbackCompleted: { [weak self] in + guard let self else { + return + } + self.currentIndex = 1 + self.state?.updated(transition: .easeInOut(duration: 0.3)) + } + ) + ) + ), + AnyComponentWithIdentity( + id: "pause", + component: AnyComponent( + GuideItemComponent( + context: component.context, + title: strings.Story_Guide_PauseTitle, + text: strings.Story_Guide_PauseDescription, + animationName: "story_pause", + isPlaying: self.currentIndex == 1, + playbackCompleted: { [weak self] in + guard let self else { + return + } + self.currentIndex = 2 + self.state?.updated(transition: .easeInOut(duration: 0.3)) + } + ) + ) + ), + AnyComponentWithIdentity( + id: "back", + component: AnyComponent( + GuideItemComponent( + context: component.context, + title: strings.Story_Guide_BackTitle, + text: strings.Story_Guide_BackDescription, + animationName: "story_back", + isPlaying: self.currentIndex == 2, + playbackCompleted: { [weak self] in + guard let self else { + return + } + self.currentIndex = 3 + self.state?.updated(transition: .easeInOut(duration: 0.3)) + } + ) + ) + ), + AnyComponentWithIdentity( + id: "move", + component: AnyComponent( + GuideItemComponent( + context: component.context, + title: strings.Story_Guide_MoveTitle, + text: strings.Story_Guide_MoveDescription, + animationName: "story_move", + isPlaying: self.currentIndex == 3, + playbackCompleted: { [weak self] in + guard let self else { + return + } + self.currentIndex = 0 + self.state?.updated(transition: .easeInOut(duration: 0.3)) + } + ) + ) + ) + ] + let itemsSize = self.guideItems.update( + transition: transition, + component: AnyComponent(List(items)), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let textSpacing: CGFloat = 7.0 + let itemsSpacing: CGFloat = 36.0 + let buttonSpacing: CGFloat = 50.0 + + let titleSize = self.titleLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Story_Guide_Title, font: Font.semibold(20.0), textColor: .white, paragraphAlignment: .center)))), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let textSize = self.descriptionLabel.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent(text: .plain(NSAttributedString(string: strings.Story_Guide_Description, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6), paragraphAlignment: .center)), maximumNumberOfLines: 0, lineSpacing: 0.2)), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let buttonSize = self.proceedButton.update( + transition: .immediate, + component: AnyComponent(Button( + content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Story_Guide_Proceed, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)))), + action: { [weak self] in + self?.handleTap() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let totalHeight = titleSize.height + textSpacing + textSize.height + itemsSpacing + itemsSize.height + buttonSpacing + buttonSize.height + let originY = floorToScreenPixels((availableSize.height - totalHeight) / 2.0) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: originY), size: titleSize) + if let view = self.titleLabel.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = titleFrame + } + + let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0), y: titleFrame.maxY + 7.0), size: textSize) + if let view = self.descriptionLabel.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = textFrame + } + + let itemsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - itemsSize.width) / 2.0), y: textFrame.maxY + 40.0), size: itemsSize) + if let view = self.guideItems.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = itemsFrame + } + + let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: itemsFrame.maxY + 57.0), size: buttonSize) + if let view = self.proceedButton.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = buttonFrame + } + + let bounds = CGRect(origin: .zero, size: availableSize) + self.effectView.frame = bounds + self.containerView.frame = bounds + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class GuideItemComponent: Component { + let context: AccountContext + let title: String + let text: String + let animationName: String + let isPlaying: Bool + let playbackCompleted: () -> Void + + init( + context: AccountContext, + title: String, + text: String, + animationName: String, + isPlaying: Bool, + playbackCompleted: @escaping () -> Void + ) { + self.context = context + self.title = title + self.text = text + self.animationName = animationName + self.isPlaying = isPlaying + self.playbackCompleted = playbackCompleted + } + + static func ==(lhs: GuideItemComponent, rhs: GuideItemComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.animationName != rhs.animationName { + return false + } + if lhs.isPlaying != rhs.isPlaying { + return false + } + return true + } + + final class View: UIView { + private var component: GuideItemComponent? + private weak var state: EmptyComponentState? + + private let containerView = UIView() + private let selectionView = UIView() + + private let animation = ComponentView() + private let titleLabel = ComponentView() + private let descriptionLabel = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.selectionView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1) + self.selectionView.clipsToBounds = true + self.selectionView.layer.cornerRadius = 16.0 + if #available(iOS 13.0, *) { + self.selectionView.layer.cornerCurve = .continuous + } + self.selectionView.alpha = 0.0 + + self.addSubview(self.containerView) + self.containerView.addSubview(self.selectionView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var isPlaying = false + func update(component: GuideItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let originX = availableSize.width / 2.0 - 120.0 + + let animationSize = self.animation.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: component.animationName), + color: .white, + placeholderColor: nil, + startingPosition: .begin, + size: CGSize(width: 60.0, height: 60.0), + renderingScale: UIScreen.main.scale, + loop: false + ) + ), + environment: {}, + containerSize: availableSize + ) + let animationFrame = CGRect(origin: CGPoint(x: originX - 11.0, y: 15.0), size: animationSize) + if let view = self.animation.view as? LottieComponent.View { + if view.superview == nil { + view.externalShouldPlay = false + self.containerView.addSubview(view) + } + view.frame = animationFrame + + if component.isPlaying && !self.isPlaying { + self.isPlaying = true + Queue.mainQueue().justDispatch { + let completionBlock = { [weak self] in + guard let self else { + return + } + self.isPlaying = false + Queue.mainQueue().after(0.1) { + self.component?.playbackCompleted() + } + } + + view.playOnce(force: true, completion: { [weak view] in + view?.playOnce(force: true, completion: { + completionBlock() + }) + }) + } + } + } + + let availableTextWidth = availableSize.width - originX - 60.0 - 18.0 + let titleSize = self.titleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .natural)), + maximumNumberOfLines: 2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableTextWidth, height: availableSize.height) + ) + let titleFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: 25.0), size: titleSize) + if let view = self.titleLabel.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = titleFrame + } + + let textSize = self.descriptionLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6), paragraphAlignment: .natural)), + maximumNumberOfLines: 3 + ) + ), + environment: {}, + containerSize: CGSize(width: availableTextWidth, height: availableSize.height) + ) + let textFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: titleFrame.maxY + 2.0), size: textSize) + if let view = self.descriptionLabel.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = textFrame + } + + let size = CGSize(width: availableSize.width, height: 53.0 + titleSize.height + textSize.height) + + self.selectionView.frame = CGRect(origin: .zero, size: size).insetBy(dx: 10.0, dy: 8.0) + transition.setAlpha(view: self.selectionView, alpha: component.isPlaying ? 1.0 : 0.0) + + self.containerView.bounds = CGRect(origin: .zero, size: size) + self.containerView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + transition.setScale(view: self.containerView, scale: component.isPlaying ? 1.1 : 1.0) + + 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) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 0aeddf69595..33a5a51fc04 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -277,7 +277,9 @@ final class StoryItemContentComponent: Component { } self.videoPlaybackStatus = status - self.updateVideoPlaybackProgress() + if !self.isSeeking { + self.updateVideoPlaybackProgress() + } }) } } @@ -360,7 +362,9 @@ final class StoryItemContentComponent: Component { } if case .file = self.currentMessageMedia { - self.updateVideoPlaybackProgress() + if !self.isSeeking { + self.updateVideoPlaybackProgress() + } } else { if !self.markedAsSeen { self.markedAsSeen = true @@ -397,7 +401,26 @@ final class StoryItemContentComponent: Component { } } - private func updateVideoPlaybackProgress() { + var effectiveTimestamp: Double { + guard let videoPlaybackStatus = self.videoPlaybackStatus else { + return 0.0 + } + return videoPlaybackStatus.timestamp + } + + var effectiveDuration: Double { + let effectiveDuration: Double + if let videoPlaybackStatus, videoPlaybackStatus.duration > 0.0 { + effectiveDuration = videoPlaybackStatus.duration + } else if case let .file(file) = self.currentMessageMedia, let duration = file.duration { + effectiveDuration = Double(max(1, duration)) + } else { + effectiveDuration = 1.0 + } + return effectiveDuration + } + + private func updateVideoPlaybackProgress(_ scrubbingTimestamp: Double? = nil) { guard let videoPlaybackStatus = self.videoPlaybackStatus else { return } @@ -478,6 +501,13 @@ final class StoryItemContentComponent: Component { } } + if let scrubbingTimestamp { + currentProgress = CGFloat(scrubbingTimestamp / effectiveDuration) + if currentProgress.isNaN || !currentProgress.isFinite { + currentProgress = 0.0 + } + } + let clippedProgress = max(0.0, min(1.0, currentProgress)) self.environment?.presentationProgressUpdated(clippedProgress, isBuffering, false) } @@ -510,6 +540,22 @@ final class StoryItemContentComponent: Component { ) } + private var isSeeking = false + func seekTo(_ timestamp: Double, apply: Bool) { + guard let videoNode = self.videoNode else { + return + } + if apply { + videoNode.seek(timestamp) + } + self.isSeeking = true + self.updateVideoPlaybackProgress(timestamp) + } + + func seekEnded() { + self.isSeeking = false + } + func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let previousItem = self.component?.item diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift index 336353b6e9f..07a8ef0dab0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift @@ -74,25 +74,6 @@ final class StoryItemLoadingEffectView: UIView { UIGraphicsPopContext() } - - /* - let numColors = 7 - var locations: [CGFloat] = [] - var colors: [CGColor] = [] - for i in 0 ..< numColors { - let position: CGFloat = CGFloat(i) / CGFloat(numColors - 1) - locations.append(position) - - let distanceFromCenterFraction: CGFloat = max(0.0, min(1.0, abs(position - 0.5) / 0.5)) - let colorAlpha = sin((1.0 - distanceFromCenterFraction) * CGFloat.pi * 0.5) - - colors.append(foregroundColor.withMultipliedAlpha(colorAlpha).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())*/ }) } self.backgroundView.image = generateGradient(effectAlpha) @@ -109,11 +90,6 @@ final class StoryItemLoadingEffectView: UIView { } private func updateAnimations(size: CGSize) { - /*if "".isEmpty { - self.backgroundView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) - return - }*/ - if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 0cb022cddff..ae22b144ecc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -403,6 +403,7 @@ public final class StoryItemSetContainerComponent: Component { let itemsContainerView: UIView let controlsContainerView: UIView let controlsClippingView: UIView + let controlsNavigationClippingView: UIView let topContentGradientView: UIImageView let bottomContentGradientLayer: SimpleGradientLayer let contentDimView: UIView @@ -411,6 +412,7 @@ public final class StoryItemSetContainerComponent: Component { let closeButtonIconView: UIImageView let navigationStrip = ComponentView() + let seekLabel = ComponentView() var centerInfoItem: InfoItem? var leftInfoItem: InfoItem? @@ -508,6 +510,12 @@ public final class StoryItemSetContainerComponent: Component { self.controlsClippingView.layer.cornerCurve = .continuous } + self.controlsNavigationClippingView = SparseContainerView() + self.controlsNavigationClippingView.clipsToBounds = true + if #available(iOS 13.0, *) { + self.controlsNavigationClippingView.layer.cornerCurve = .continuous + } + self.topContentGradientView = UIImageView() if let image = StoryItemSetContainerComponent.shadowImage { self.topContentGradientView.image = image.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(image.size.height - 1.0)) @@ -539,11 +547,12 @@ public final class StoryItemSetContainerComponent: Component { self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer) self.componentContainerView.addSubview(self.itemsContainerView) + self.componentContainerView.addSubview(self.controlsNavigationClippingView) self.componentContainerView.addSubview(self.controlsClippingView) self.componentContainerView.addSubview(self.controlsContainerView) self.controlsClippingView.addSubview(self.contentDimView) - self.controlsClippingView.addSubview(self.topContentGradientView) + self.controlsNavigationClippingView.addSubview(self.topContentGradientView) self.layer.addSublayer(self.bottomContentGradientLayer) self.componentContainerView.addSubview(self.viewListsContainer) @@ -2054,6 +2063,9 @@ public final class StoryItemSetContainerComponent: Component { duration: 0.3 ) + self.controlsNavigationClippingView.layer.animatePosition(from: sourceLocalFrame.center, to: self.controlsNavigationClippingView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.controlsNavigationClippingView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.controlsNavigationClippingView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.storyItem.id]?.view.view { let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width let innerFromFrame = CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: CGSize(width: innerSourceLocalFrame.width, height: visibleItemView.bounds.height * innerScale)) @@ -2264,6 +2276,7 @@ public final class StoryItemSetContainerComponent: Component { unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.controlsNavigationClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) for transitionViewImpl in transitionViewsImpl { transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) @@ -2318,6 +2331,9 @@ public final class StoryItemSetContainerComponent: Component { removeOnCompletion: false ) + self.controlsNavigationClippingView.layer.animatePosition(from: self.controlsNavigationClippingView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.controlsNavigationClippingView.layer.animateBounds(from: self.controlsNavigationClippingView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.overlayContainerView.clipsToBounds = true let overlayToFrame = sourceLocalFrame let overlayToBounds = CGRect(origin: CGPoint(x: overlayToFrame.minX, y: overlayToFrame.minY), size: overlayToFrame.size) @@ -2382,6 +2398,7 @@ public final class StoryItemSetContainerComponent: Component { unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.controlsNavigationClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) for transitionViewImpl in transitionViewsImpl { transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) @@ -2523,6 +2540,8 @@ public final class StoryItemSetContainerComponent: Component { #endif*/ } + let previousComponent = self.component + var isFirstItem = false var itemChanged = false var resetInputContents: MessageInputPanelComponent.SendMessageInput? @@ -2689,8 +2708,6 @@ public final class StoryItemSetContainerComponent: Component { let inputPlaceholder: MessageInputPanelComponent.Placeholder if let stealthModeTimeout = component.stealthModeTimeout { - //TODO:localize - let minutes = Int(stealthModeTimeout / 60) let seconds = Int(stealthModeTimeout % 60) @@ -3305,7 +3322,7 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: component.strings.Story_ToastShowStoriesTo(peer.compactDisplayTitle).string, timeout: nil), + content: .info(title: nil, text: component.strings.Story_ToastShowStoriesTo(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, @@ -3326,7 +3343,7 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: component.strings.Story_ToastHideStoriesFrom(peer.compactDisplayTitle).string, timeout: nil), + content: .info(title: nil, text: component.strings.Story_ToastHideStoriesFrom(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, @@ -3603,6 +3620,9 @@ public final class StoryItemSetContainerComponent: Component { transition.setPosition(view: self.controlsClippingView, position: contentFrame.center) transition.setBounds(view: self.controlsClippingView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size)) + transition.setPosition(view: self.controlsNavigationClippingView, position: contentFrame.center) + transition.setBounds(view: self.controlsNavigationClippingView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size)) + var transform = CATransform3DMakeScale(contentVisualScale, contentVisualScale, 1.0) if let pinchState = component.pinchState { let pinchOffset = CGPoint( @@ -3619,6 +3639,7 @@ public final class StoryItemSetContainerComponent: Component { } transition.setTransform(view: self.controlsContainerView, transform: transform) transition.setTransform(view: self.controlsClippingView, transform: transform) + transition.setTransform(view: self.controlsNavigationClippingView, transform: transform) transition.setCornerRadius(layer: self.controlsClippingView.layer, cornerRadius: 12.0 * (1.0 / contentVisualScale)) @@ -3870,6 +3891,7 @@ public final class StoryItemSetContainerComponent: Component { let controlsContainerAlpha = (component.hideUI || self.isEditingStory || self.viewListDisplayState != .hidden) ? 0.0 : 1.0 transition.setAlpha(view: self.controlsContainerView, alpha: controlsContainerAlpha) transition.setAlpha(view: self.controlsClippingView, alpha: controlsContainerAlpha) + transition.setAlpha(view: self.controlsNavigationClippingView, alpha: self.isEditingStory || self.viewListDisplayState != .hidden ? 0.0 : 1.0) let focusedItem: StoryContentItem? = component.slice.item @@ -4068,7 +4090,7 @@ public final class StoryItemSetContainerComponent: Component { } switch action { case let .url(url, concealed): - openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in + let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in guard let self, let component = self.component, let controller = component.controller() else { return } @@ -4096,7 +4118,7 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.presentTextEntityActions(view: self, action: action, openUrl: { [weak self] url, concealed in - openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in + let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in guard let self, let component = self.component, let controller = component.controller() else { return } @@ -4140,6 +4162,8 @@ public final class StoryItemSetContainerComponent: Component { } case .translate: self.sendMessageContext.performTranslateTextAction(view: self, text: text.string) + case .quote: + break } }, controller: { [weak self] in @@ -4235,9 +4259,7 @@ public final class StoryItemSetContainerComponent: Component { animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: true, - isEmojiSelection: false, + subject: .reaction, hasTrending: false, topReactionItems: mappedReactionItems, areUnicodeEmojiEnabled: false, @@ -4395,6 +4417,7 @@ public final class StoryItemSetContainerComponent: Component { attributes: messageAttributes, inlineStickers: inlineStickers, mediaReference: nil, + threadId: nil, replyToMessageId: nil, replyToStoryId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id), localGroupingKey: nil, @@ -4414,7 +4437,7 @@ public final class StoryItemSetContainerComponent: Component { presentationData: presentationData, content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in if let messageId = messageIds.first, let self { - self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) }) + self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: nil, timecode: nil) }) } }), elevatedLayout: false, @@ -4582,7 +4605,7 @@ public final class StoryItemSetContainerComponent: Component { //transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0) transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: 0.0) - var topGradientAlpha: CGFloat = (component.hideUI || self.viewListDisplayState != .hidden || self.isEditingStory) ? 0.0 : 1.0 + var topGradientAlpha: CGFloat = (self.viewListDisplayState != .hidden || self.isEditingStory) ? 0.0 : 1.0 var normalDimAlpha: CGFloat = 0.0 var forceDimAnimation = false if let captionItem = self.captionItem { @@ -4642,10 +4665,9 @@ public final class StoryItemSetContainerComponent: Component { let startTime9 = CFAbsoluteTimeGetCurrent() + let navigationStripSideInset: CGFloat = 8.0 + let navigationStripTopInset: CGFloat = 8.0 if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id], let index = focusedItem.position { - let navigationStripSideInset: CGFloat = 8.0 - let navigationStripTopInset: CGFloat = 8.0 - var index = max(0, min(index, component.slice.totalCount - 1)) var count = component.slice.totalCount if let dayCounters = focusedItem.dayCounters { @@ -4653,11 +4675,18 @@ public final class StoryItemSetContainerComponent: Component { count = dayCounters.totalCount } - let _ = self.navigationStrip.update( - transition: transition, + let isSeeking = component.isProgressPaused && component.hideUI && isVideo + + var navigationStripTransition = transition + if let previousComponent, (previousComponent.isProgressPaused && component.hideUI) != isSeeking { + navigationStripTransition = .easeInOut(duration: 0.3) + } + let navigationStripSize = self.navigationStrip.update( + transition: navigationStripTransition, component: AnyComponent(MediaNavigationStripComponent( index: index, - count: count + count: count, + isSeeking: isSeeking )), environment: { MediaNavigationStripComponent.EnvironmentType( @@ -4665,15 +4694,35 @@ public final class StoryItemSetContainerComponent: Component { currentIsBuffering: visibleItem.isBuffering ) }, - containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0) + containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 6.0) ) if let navigationStripView = self.navigationStrip.view { if navigationStripView.superview == nil { navigationStripView.isUserInteractionEnabled = false - self.controlsClippingView.addSubview(navigationStripView) + self.controlsNavigationClippingView.addSubview(navigationStripView) } - transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0))) - transition.setAlpha(view: navigationStripView, alpha: self.isEditingStory ? 0.0 : 1.0) + transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: navigationStripSize.height))) + + let hideUI = component.hideUI && !isVideo + transition.setAlpha(view: navigationStripView, alpha: self.isEditingStory || hideUI ? 0.0 : 1.0) + } + + let seekLabelSize = self.seekLabel.update( + transition: .immediate, + component: AnyComponent(Text(text: component.strings.Story_SlideToSeek, font: Font.semibold(14.0), color: .white)), + environment: {}, + containerSize: availableSize + ) + if let seekLabelView = self.seekLabel.view { + if seekLabelView.superview == nil { + seekLabelView.alpha = 0.0 + seekLabelView.isUserInteractionEnabled = false + self.controlsNavigationClippingView.addSubview(seekLabelView) + } + seekLabelView.bounds = CGRect(origin: .zero, size: seekLabelSize) + navigationStripTransition.setPosition(view: seekLabelView, position: CGPoint(x: availableSize.width / 2.0, y: navigationStripTopInset + 22.0 + 6.0 - (!isSeeking ? 12.0 : 0.0))) + navigationStripTransition.setAlpha(view: seekLabelView, alpha: isSeeking ? 1.0 : 0.0) + navigationStripTransition.setScale(view: seekLabelView, scale: isSeeking ? 1.0 : 0.02) } } @@ -4737,7 +4786,7 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let controller = UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: text, timeout: nil), + content: .info(title: nil, text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, @@ -5101,7 +5150,11 @@ public final class StoryItemSetContainerComponent: Component { case let .image(image, dimensions): updateProgressImpl?(0.0) - if let imageData = compressImageToJPEG(image, quality: 0.7) { + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) { updateDisposable.set((context.engine.messages.editStory(peerId: peerId, id: id, media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil) |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let self else { @@ -5142,7 +5195,11 @@ public final class StoryItemSetContainerComponent: Component { resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } - let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) } let firstFrameFile = firstFrameImageData.flatMap { data -> TempBoxFile? in let file = TempBox.shared.tempFile(fileName: "image.jpg") if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { @@ -5540,6 +5597,12 @@ public final class StoryItemSetContainerComponent: Component { } } + for mediaArea in component.slice.item.storyItem.mediaAreas { + if case let .reaction(_, reaction, _) = mediaArea, case let .custom(fileId) = reaction { + emojiFileIds.append(fileId) + } + } + if !emojiFileIds.isEmpty || hasLinkedStickers, let peerReference = PeerReference(component.slice.peer._asPeer()) { let context = component.context @@ -5714,7 +5777,7 @@ public final class StoryItemSetContainerComponent: Component { if component.slice.item.storyItem.isPinned { self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: component.strings.Story_ToastRemovedFromProfileText, timeout: nil), + content: .info(title: nil, text: component.strings.Story_ToastRemovedFromProfileText, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, @@ -5723,7 +5786,7 @@ public final class StoryItemSetContainerComponent: Component { } else { self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .info(title: component.strings.Story_ToastSavedToProfileTitle, text: component.strings.Story_ToastSavedToProfileText, timeout: nil), + content: .info(title: component.strings.Story_ToastSavedToProfileTitle, text: component.strings.Story_ToastSavedToProfileText, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, @@ -5865,7 +5928,7 @@ public final class StoryItemSetContainerComponent: Component { if component.slice.item.storyItem.isPinned { self.scheduledStoryUnpinnedUndoOverlay = UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: presentationData.strings.Story_ToastRemovedFromChannelText, timeout: nil), + content: .info(title: nil, text: presentationData.strings.Story_ToastRemovedFromChannelText, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, @@ -5874,7 +5937,7 @@ public final class StoryItemSetContainerComponent: Component { } else { self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .info(title: presentationData.strings.Story_ToastSavedToChannelTitle, text: presentationData.strings.Story_ToastSavedToChannelText, timeout: nil), + content: .info(title: presentationData.strings.Story_ToastSavedToChannelTitle, text: presentationData.strings.Story_ToastSavedToChannelText, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, blurred: true, @@ -6375,7 +6438,7 @@ public final class StoryItemSetContainerComponent: Component { } func maybeDisplayUnmuteVideoTooltip() { - guard let component = self.component, component.visibilityFraction == 1.0 else { + guard let component = self.component, component.visibilityFraction == 1.0 && !component.isProgressPaused else { return } guard let soundButtonView = self.soundButton.view else { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 4e741492ec4..ffb08ad0458 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -269,7 +269,9 @@ final class StoryItemSetContainerSendMessage { hasActiveGroupCall: false, importState: nil, threadData: nil, - isGeneralThreadClosed: nil + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil ) let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) @@ -371,7 +373,7 @@ final class StoryItemSetContainerSendMessage { animateInAsReplacement: false, action: { [weak view, weak self] action in if case .undo = action, let messageId { - view?.navigateToPeer(peer: peer, chat: true, subject: isScheduled ? .scheduledMessages : .message(id: .id(messageId), highlight: false, timecode: nil)) + view?.navigateToPeer(peer: peer, chat: true, subject: isScheduled ? .scheduledMessages : .message(id: .id(messageId), highlight: nil, timecode: nil)) } self?.tooltipScreen = nil view?.updateIsProgressPaused() @@ -574,7 +576,7 @@ final class StoryItemSetContainerSendMessage { let waveformBuffer = recordedAudioPreview.waveform.makeBitstream() - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedAudioPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedAudioPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedAudioPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedAudioPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedAudioPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedAudioPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() @@ -790,7 +792,7 @@ final class StoryItemSetContainerSendMessage { fileAttributes.append(.ImageSize(size: PixelDimensions(size))) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) self.sendMessages(view: view, peer: peer, messages: [message], silentPosting: false) } @@ -894,7 +896,7 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) HapticFeedback().tap() }) @@ -1571,7 +1573,7 @@ final class StoryItemSetContainerSendMessage { guard let view, let component = view.component else { return } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: [message.withUpdatedReplyToMessageId(nil)]) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in if let self, let view { @@ -1613,7 +1615,7 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) self.sendMessages(view: view, peer: peer, messages: [message]) }) completion(controller, controller.mediaPickerContext) @@ -1649,7 +1651,7 @@ final class StoryItemSetContainerSendMessage { if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - textEnqueueMessage = .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + textEnqueueMessage = .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } if peers.count > 1 { var enqueueMessages: [EnqueueMessage] = [] @@ -1678,7 +1680,7 @@ final class StoryItemSetContainerSendMessage { } if let media = media { - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) enqueueMessages.append(message) } } @@ -1736,7 +1738,7 @@ final class StoryItemSetContainerSendMessage { if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) } else { @@ -1755,7 +1757,7 @@ final class StoryItemSetContainerSendMessage { if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) } @@ -1810,7 +1812,7 @@ final class StoryItemSetContainerSendMessage { let _ = self /*if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() }*/ @@ -1918,7 +1920,7 @@ final class StoryItemSetContainerSendMessage { present(controller, mediaPickerContext) } - private func presentOldMediaPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, fileMode: Bool, editingMedia: Bool, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) { + private func presentOldMediaPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, fileMode: Bool, editingMedia: Bool, push: @escaping (ViewController) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) { guard let component = view.component else { return } @@ -2047,14 +2049,14 @@ final class StoryItemSetContainerSendMessage { } } view.endEditing(true) - present(legacyController, LegacyAssetPickerContext(controller: controller)) + push(legacyController) } }) }) } private func presentFileGallery(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, editingMessage: Bool = false) { - self.presentOldMediaPicker(view: view, peer: peer, replyMessageId: replyMessageId, replyToStoryId: replyToStoryId, fileMode: true, editingMedia: editingMessage, present: { [weak view] c, _ in + self.presentOldMediaPicker(view: view, peer: peer, replyMessageId: replyMessageId, replyToStoryId: replyToStoryId, fileMode: true, editingMedia: editingMessage, push: { [weak view] c in view?.component?.controller()?.push(c) }, completion: { [weak self, weak view] signals, silentPosting, scheduleTime in guard let self, let view else { @@ -2153,7 +2155,7 @@ final class StoryItemSetContainerSendMessage { } let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } if let _ = groupingKey, messages.count % 10 == 0 { @@ -2243,7 +2245,7 @@ final class StoryItemSetContainerSendMessage { guard let self, let view, let component = view.component else { return } - if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peer.id, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyMessageId, replyToStoryId: storyId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { + if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peer.id, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: storyId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { } if let attachmentController = self.attachmentController { @@ -2494,9 +2496,9 @@ final class StoryItemSetContainerSendMessage { if let focusedStoryId { switch message { - case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, _, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, _, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): if replyToMessageId == nil { - message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: focusedStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: focusedStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) } case .forward: break @@ -2599,7 +2601,7 @@ final class StoryItemSetContainerSendMessage { mappedMessages.append(message) } - strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) + strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) completion() } @@ -2721,7 +2723,7 @@ final class StoryItemSetContainerSendMessage { return } if let navigationController = controller.navigationController as? NavigationController { - component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil))) + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) } completion?() }) @@ -2771,7 +2773,12 @@ final class StoryItemSetContainerSendMessage { self.resolvePeerByNameDisposable.set(nil) } disposable.set((resolveSignal - |> take(1) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in return .single(peer?._asPeer()) } @@ -2804,6 +2811,12 @@ final class StoryItemSetContainerSendMessage { var resolveSignal: Signal if let peerName = peerName { resolveSignal = component.context.engine.peers.resolvePeerByName(name: peerName) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in if let peer = peer { return .single(peer._asPeer()) @@ -3291,7 +3304,7 @@ final class StoryItemSetContainerSendMessage { frame = view.controlsContainerView.convert(frame, to: nil) let node = controller.displayNode - let menuController = ContextMenuController(actions: actions, blurred: true) + let menuController = makeContextMenuController(actions: actions, blurred: true) menuController.centerHorizontally = true menuController.dismissed = { [weak self, weak view] in if let self, let view { diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 0c5cf7e0359..f178635b822 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -537,7 +537,7 @@ public final class StoryFooterPanelComponent: Component { var regularSegments: [AnimatedCountLabelView.Segment] = [] if viewCount != 0 { - regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white))) + regularSegments.append(.number(viewCount, NSAttributedString(string: countString(Int64(viewCount)), font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white))) } let viewPart: String @@ -555,7 +555,7 @@ public final class StoryFooterPanelComponent: Component { viewPart = string } - let viewStatsTextLayout = self.viewStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)) + 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) @@ -602,6 +602,7 @@ public final class StoryFooterPanelComponent: Component { 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 diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index fc37c577981..c6fa45b0f24 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -18,6 +18,14 @@ public final class TabSelectorComponent: Component { } } + public struct CustomLayout: Equatable { + public var spacing: CGFloat + + public init(spacing: CGFloat) { + self.spacing = spacing + } + } + public struct Item: Equatable { public var id: AnyHashable public var title: String @@ -32,17 +40,20 @@ public final class TabSelectorComponent: Component { } public let colors: Colors + public let customLayout: CustomLayout? public let items: [Item] public let selectedId: AnyHashable? public let setSelectedId: (AnyHashable) -> Void public init( colors: Colors, + customLayout: CustomLayout? = nil, items: [Item], selectedId: AnyHashable?, setSelectedId: @escaping (AnyHashable) -> Void ) { self.colors = colors + self.customLayout = customLayout self.items = items self.selectedId = selectedId self.setSelectedId = setSelectedId @@ -52,6 +63,9 @@ public final class TabSelectorComponent: Component { if lhs.colors != rhs.colors { return false } + if lhs.customLayout != rhs.customLayout { + return false + } if lhs.items != rhs.items { return false } @@ -96,7 +110,14 @@ public final class TabSelectorComponent: Component { let baseHeight: CGFloat = 28.0 let innerInset: CGFloat = 12.0 - let spacing: CGFloat = 2.0 + let spacing: CGFloat = component.customLayout?.spacing ?? 2.0 + + let itemFont: UIFont + if component.customLayout != nil { + itemFont = Font.medium(14.0) + } else { + itemFont = Font.semibold(14.0) + } if self.selectionView.image == nil { self.selectionView.image = generateStretchableFilledCircleImage(diameter: baseHeight, color: component.colors.selection) @@ -123,7 +144,7 @@ public final class TabSelectorComponent: Component { let itemSize = itemView.title.update( transition: .immediate, component: AnyComponent(PlainButtonComponent( - content: AnyComponent(Text(text: item.title, font: Font.semibold(14.0), color: component.colors.foreground)), + content: AnyComponent(Text(text: item.title, font: itemFont, color: component.colors.foreground)), effectAlignment: .center, minSize: nil, action: { [weak self] in diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 23ea23802fc..f7824966c89 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -663,7 +663,7 @@ public final class TextFieldComponent: Component { } var spoilerRects: [CGRect] = [] - var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] + var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = [] let textView = self.textView if let attributedText = textView.attributedText { @@ -707,9 +707,13 @@ public final class TextFieldComponent: Component { if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { if let start = textView.position(from: beginning, offset: range.location), let end = textView.position(from: start, offset: range.length), let textRange = textView.textRange(from: start, to: end) { + var emojiFontSize = component.fontSize + if let font = attributes[.font] as? UIFont { + emojiFontSize = font.pointSize + } let textRects = textView.selectionRects(for: textRange) for textRect in textRects { - customEmojiRects.append((textRect.rect, value)) + customEmojiRects.append((textRect.rect, value, emojiFontSize)) break } } @@ -870,18 +874,24 @@ public final class TextFieldComponent: Component { } } + let wasEditing = component.externalState.isEditing + let isEditing = self.textView.isFirstResponder + if self.textView.textContainerInset != component.insets { self.textView.textContainerInset = component.insets } + + var availableSize = availableSize + if !isEditing && component.isOneLineWhenUnfocused { + availableSize.width += 32.0 + } + self.textContainer.size = CGSize(width: availableSize.width - self.textView.textContainerInset.left - self.textView.textContainerInset.right, height: 10000000.0) self.layoutManager.ensureLayout(for: self.textContainer) let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer) let size = CGSize(width: availableSize.width, height: min(availableSize.height, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom)) - let wasEditing = component.externalState.isEditing - let isEditing = self.textView.isFirstResponder - var refreshScrolling = self.textView.bounds.size != size if component.isOneLineWhenUnfocused && !isEditing && isEditing != wasEditing { refreshScrolling = true @@ -953,6 +963,7 @@ public final class TextFieldComponent: Component { if let view = self.ellipsisView.view { if view.superview == nil { view.alpha = 0.0 + view.isUserInteractionEnabled = false self.textView.addSubview(view) } let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 8.0, y: position.y), size: ellipsisSize) diff --git a/submodules/TelegramUI/Components/TextLoadingEffect/BUILD b/submodules/TelegramUI/Components/TextLoadingEffect/BUILD new file mode 100644 index 00000000000..1dd36290ec9 --- /dev/null +++ b/submodules/TelegramUI/Components/TextLoadingEffect/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TextLoadingEffect", + module_name = "TextLoadingEffect", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AppBundle", + "//submodules/Components/HierarchyTrackingLayer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift new file mode 100644 index 00000000000..2eb9b029559 --- /dev/null +++ b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift @@ -0,0 +1,149 @@ +import Foundation +import UIKit +import Display +import AppBundle +import HierarchyTrackingLayer + +private let shadowImage: UIImage? = { + UIImage(named: "Stories/PanelGradient") +}() + +public final class TextLoadingEffectView: UIView { + let hierarchyTrackingLayer: HierarchyTrackingLayer + + private let maskContentsView: UIView + private let maskHighlightNode: LinkHighlightingNode + + private let maskBorderContentsView: UIView + private let maskBorderHighlightNode: LinkHighlightingNode + + private let backgroundView: UIImageView + private let borderBackgroundView: UIImageView + + private let duration: Double + private let gradientWidth: CGFloat + + private var size: CGSize? + + override public init(frame: CGRect) { + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + self.maskContentsView = UIView() + self.maskHighlightNode = LinkHighlightingNode(color: .black) + self.maskHighlightNode.useModernPathCalculation = true + + self.maskBorderContentsView = UIView() + self.maskBorderHighlightNode = LinkHighlightingNode(color: .black) + self.maskBorderHighlightNode.borderOnly = true + self.maskBorderHighlightNode.useModernPathCalculation = true + self.maskBorderContentsView.addSubview(self.maskBorderHighlightNode.view) + + self.backgroundView = UIImageView() + self.borderBackgroundView = UIImageView() + + self.gradientWidth = 120.0 + self.duration = 1.0 + + super.init(frame: frame) + + self.isUserInteractionEnabled = false + + self.maskContentsView.mask = self.maskHighlightNode.view + self.maskContentsView.addSubview(self.backgroundView) + self.addSubview(self.maskContentsView) + + self.maskBorderContentsView.mask = self.maskBorderHighlightNode.view + self.maskBorderContentsView.addSubview(self.borderBackgroundView) + self.addSubview(self.maskBorderContentsView) + + let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in + return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) + + if let shadowImage { + UIGraphicsPushContext(context) + + for i in 0 ..< 2 { + let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) + + context.saveGState() + context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) + context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) + let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) + + context.clip(to: adjustedRect, mask: shadowImage.cgImage!) + context.setFillColor(foregroundColor.cgColor) + context.fill(adjustedRect) + + context.restoreGState() + } + + UIGraphicsPopContext() + } + })?.withRenderingMode(.alwaysTemplate) + } + + self.backgroundView.image = generateGradient(0.5) + self.borderBackgroundView.image = generateGradient(1.0) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let self, let size = self.size else { + return + } + self.updateAnimations(size: size) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateAnimations(size: CGSize) { + if self.backgroundView.layer.animation(forKey: "shimmer") != nil { + return + } + + let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.0) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + self.backgroundView.layer.add(animation, forKey: "shimmer") + self.borderBackgroundView.layer.add(animation, forKey: "shimmer") + } + + public func update(color: UIColor, textNode: TextNode, range: NSRange) { + var rectsSet: [CGRect] = [] + if let cachedLayout = textNode.cachedLayout { + if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { + rectsSet = rects + } + } + + let maskFrame = CGRect(origin: CGPoint(), size: textNode.bounds.size).insetBy(dx: -4.0, dy: -4.0) + + self.maskContentsView.backgroundColor = color.withAlphaComponent(0.1) + self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12) + + self.backgroundView.tintColor = color + self.borderBackgroundView.tintColor = color + + self.maskContentsView.frame = maskFrame + self.maskBorderContentsView.frame = maskFrame + + self.maskHighlightNode.updateRects(rectsSet) + self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + self.maskBorderHighlightNode.updateRects(rectsSet) + self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + if self.size != maskFrame.size { + self.size = maskFrame.size + + self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + + self.updateAnimations(size: maskFrame.size) + } + } +} diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 032c38f515e..f384ad05c07 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -132,6 +132,7 @@ public final class TextNodeWithEntities { let string = NSMutableAttributedString(attributedString: sourceString) var fullRange = NSRange(location: 0, length: string.length) + var originalTextId = 0 while true { var found = false string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in @@ -141,7 +142,8 @@ public final class TextNodeWithEntities { let replacementRange = NSRange(location: 0, length: updatedSubstring.length) updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) - updatedSubstring.addAttribute(originalTextAttributeKey, value: string.attributedSubstring(from: range).string, range: replacementRange) + updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange) + originalTextId += 1 let itemSize = (font.pointSize * 24.0 / 17.0) diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift index 99190021a3a..dbfda1f3727 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift @@ -11,6 +11,7 @@ struct EditableTokenListToken { enum Subject { case peer(EnginePeer) case category(UIImage?) + case emoji(String) } let id: AnyHashable @@ -86,6 +87,7 @@ private final class TokenNode: ASDisplayNode { let token: EditableTokenListToken let avatarNode: AvatarNode let categoryAvatarNode: ASImageNode + let emojiTextNode: ImmediateTextNode let removeIconNode: ASImageNode let titleNode: ASTextNode let backgroundNode: ASImageNode @@ -119,6 +121,7 @@ private final class TokenNode: ASDisplayNode { self.categoryAvatarNode = ASImageNode() self.categoryAvatarNode.displaysAsynchronously = false self.categoryAvatarNode.displayWithoutProcessing = true + self.emojiTextNode = ImmediateTextNode() self.removeIconNode = ASImageNode() self.removeIconNode.alpha = 0.0 @@ -132,6 +135,8 @@ private final class TokenNode: ASDisplayNode { cornerRadius = 24.0 case .category: cornerRadius = 14.0 + case .emoji: + cornerRadius = 24.0 } self.backgroundNode = ASImageNode() @@ -160,6 +165,9 @@ private final class TokenNode: ASDisplayNode { case let .category(image): self.addSubnode(self.categoryAvatarNode) self.categoryAvatarNode.image = image + case let .emoji(emoji): + self.addSubnode(self.emojiTextNode) + self.emojiTextNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(17.0), textColor: .white) } self.updateIsSelected(isSelected, animated: false) @@ -167,7 +175,12 @@ private final class TokenNode: ASDisplayNode { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let titleSize = self.titleNode.measure(CGSize(width: constrainedSize.width - 8.0, height: constrainedSize.height)) - return CGSize(width: 22.0 + titleSize.width + 16.0, height: 28.0) + var width = 22.0 + titleSize.width + 16.0 + if self.emojiTextNode.supernode != nil { + let _ = self.emojiTextNode.updateLayout(constrainedSize) + width += 3.0 + } + return CGSize(width: width, height: 28.0) } override func layout() { @@ -181,7 +194,13 @@ private final class TokenNode: ASDisplayNode { self.categoryAvatarNode.frame = self.avatarNode.frame self.removeIconNode.frame = self.avatarNode.frame - self.titleNode.frame = CGRect(origin: CGPoint(x: 29.0, y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize) + var textLeftOffset: CGFloat = 29.0 + if let emojiTextSize = self.emojiTextNode.cachedLayout?.size { + self.emojiTextNode.frame = CGRect(origin: CGPoint(x: 7.0, y: 4.0), size: emojiTextSize) + textLeftOffset += 3.0 + } + + self.titleNode.frame = CGRect(origin: CGPoint(x: textLeftOffset, y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize) } func updateIsSelected(_ isSelected: Bool, animated: Bool) { @@ -192,6 +211,7 @@ private final class TokenNode: ASDisplayNode { self.avatarNode.alpha = isSelected ? 0.0 : 1.0 self.categoryAvatarNode.alpha = isSelected ? 0.0 : 1.0 + self.emojiTextNode.alpha = isSelected ? 0.0 : 1.0 self.removeIconNode.alpha = isSelected ? 1.0 : 0.0 if animated { @@ -205,6 +225,9 @@ private final class TokenNode: ASDisplayNode { self.categoryAvatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.categoryAvatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + self.emojiTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.emojiTextNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + self.removeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.removeIconNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } else { @@ -217,6 +240,9 @@ private final class TokenNode: ASDisplayNode { self.categoryAvatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.categoryAvatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + self.emojiTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.emojiTextNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + self.removeIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.removeIconNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) } diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift index a906a601053..a5c33584308 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift @@ -21,6 +21,7 @@ public final class TokenListTextField: Component { public enum Content: Equatable { case peer(EnginePeer) case category(UIImage?) + case emoji(String) public static func ==(lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -36,6 +37,12 @@ public final class TokenListTextField: Component { } else { return false } + case let .emoji(lhsEmoji): + if case let .emoji(rhsEmoji) = rhs, lhsEmoji == rhsEmoji { + return true + } else { + return false + } } } } @@ -217,6 +224,8 @@ public final class TokenListTextField: Component { mappedSubject = .peer(peer) case let .category(image): mappedSubject = .category(image) + case let .emoji(emoji): + mappedSubject = .emoji(emoji) } return EditableTokenListToken( diff --git a/submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/BUILD b/submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/BUILD new file mode 100644 index 00000000000..ded71fb7724 --- /dev/null +++ b/submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "RoundedRectWithTailPath", + module_name = "RoundedRectWithTailPath", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/Sources/RoundedRectWithTailPath.swift b/submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/Sources/RoundedRectWithTailPath.swift new file mode 100644 index 00000000000..98d4bf895f6 --- /dev/null +++ b/submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath/Sources/RoundedRectWithTailPath.swift @@ -0,0 +1,130 @@ +import Foundation +import UIKit + +public func generateRoundedRectWithTailPath(rectSize: CGSize, cornerRadius: CGFloat? = nil, tailSize: CGSize = CGSize(width: 20.0, height: 9.0), tailRadius: CGFloat = 4.0, tailPosition: CGFloat? = 0.5, transformTail: Bool = true) -> UIBezierPath { + let cornerRadius: CGFloat = cornerRadius ?? rectSize.height / 2.0 + let tailWidth: CGFloat = tailSize.width + let tailHeight: CGFloat = tailSize.height + + let rect = CGRect(origin: CGPoint(x: 0.0, y: tailHeight), size: rectSize) + + guard let tailPosition else { + return UIBezierPath(cgPath: CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)) + } + + let cutoff: CGFloat = 0.27 + + let path = UIBezierPath() + path.move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius)) + + var leftArcEndAngle: CGFloat = .pi / 2.0 + var leftConnectionArcRadius = tailRadius + var tailLeftHalfWidth: CGFloat = tailWidth / 2.0 + var tailLeftArcStartAngle: CGFloat = -.pi / 4.0 + var tailLeftHalfRadius = tailRadius + + var rightArcStartAngle: CGFloat = -.pi / 2.0 + var rightConnectionArcRadius = tailRadius + var tailRightHalfWidth: CGFloat = tailWidth / 2.0 + var tailRightArcStartAngle: CGFloat = .pi / 4.0 + var tailRightHalfRadius = tailRadius + + if transformTail { + if tailPosition < 0.5 { + let fraction = max(0.0, tailPosition - 0.15) / 0.35 + leftArcEndAngle *= fraction + + let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15 + leftConnectionArcRadius *= connectionFraction + + if tailPosition < cutoff { + let fraction = tailPosition / cutoff + tailLeftHalfWidth *= fraction + tailLeftArcStartAngle *= fraction + tailLeftHalfRadius *= fraction + } + } else if tailPosition > 0.5 { + let tailPosition = 1.0 - tailPosition + let fraction = max(0.0, tailPosition - 0.15) / 0.35 + rightArcStartAngle *= fraction + + let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15 + rightConnectionArcRadius *= connectionFraction + + if tailPosition < cutoff { + let fraction = tailPosition / cutoff + tailRightHalfWidth *= fraction + tailRightArcStartAngle *= fraction + tailRightHalfRadius *= fraction + } + } + } + + path.addArc( + withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius), + radius: cornerRadius, + startAngle: .pi, + endAngle: .pi + max(0.0001, leftArcEndAngle), + clockwise: true + ) + + let leftArrowStart = max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfWidth - leftConnectionArcRadius) + path.addArc( + withCenter: CGPoint(x: leftArrowStart, y: rect.minY - leftConnectionArcRadius), + radius: leftConnectionArcRadius, + startAngle: .pi / 2.0, + endAngle: .pi / 4.0, + clockwise: false + ) + + path.addLine(to: CGPoint(x: max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfRadius), y: rect.minY - tailHeight)) + + path.addArc( + withCenter: CGPoint(x: rect.minX + rectSize.width * tailPosition, y: rect.minY - tailHeight + tailRadius / 2.0), + radius: tailRadius, + startAngle: -.pi / 2.0 + tailLeftArcStartAngle, + endAngle: -.pi / 2.0 + tailRightArcStartAngle, + clockwise: true + ) + + path.addLine(to: CGPoint(x: min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfRadius), y: rect.minY - tailHeight)) + + let rightArrowStart = min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfWidth + rightConnectionArcRadius) + path.addArc( + withCenter: CGPoint(x: rightArrowStart, y: rect.minY - rightConnectionArcRadius), + radius: rightConnectionArcRadius, + startAngle: .pi - .pi / 4.0, + endAngle: .pi / 2.0, + clockwise: false + ) + + path.addArc( + withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + cornerRadius), + radius: cornerRadius, + startAngle: min(-0.0001, rightArcStartAngle), + endAngle: 0.0, + clockwise: true + ) + + path.addLine(to: CGPoint(x: rect.minX + rectSize.width, y: rect.minY + rectSize.height - cornerRadius)) + + path.addArc( + withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + rectSize.height - cornerRadius), + radius: cornerRadius, + startAngle: 0.0, + endAngle: .pi / 2.0, + clockwise: true + ) + + path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height)) + + path.addArc( + withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height - cornerRadius), + radius: cornerRadius, + startAngle: .pi / 2.0, + endAngle: .pi, + clockwise: true + ) + + return path +} diff --git a/submodules/TelegramUI/Components/WallpaperPreviewMedia/BUILD b/submodules/TelegramUI/Components/WallpaperPreviewMedia/BUILD new file mode 100644 index 00000000000..2bbdb275afd --- /dev/null +++ b/submodules/TelegramUI/Components/WallpaperPreviewMedia/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "WallpaperPreviewMedia", + module_name = "WallpaperPreviewMedia", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/WallpaperPreviewMedia.swift b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift similarity index 70% rename from submodules/TelegramUI/Sources/WallpaperPreviewMedia.swift rename to submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift index 0a232c2355d..7903e414e89 100644 --- a/submodules/TelegramUI/Sources/WallpaperPreviewMedia.swift +++ b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift @@ -3,7 +3,7 @@ import UIKit import Postbox import TelegramCore -enum WallpaperPreviewMediaContent: Equatable { +public enum WallpaperPreviewMediaContent: Equatable { case file(file: TelegramMediaFile, colors: [UInt32], rotation: Int32?, intensity: Int32?, Bool, Bool) case image(representations: [TelegramMediaImageRepresentation]) case color(UIColor) @@ -11,23 +11,23 @@ enum WallpaperPreviewMediaContent: Equatable { case themeSettings(TelegramThemeSettings) } -final class WallpaperPreviewMedia: Media { - var id: MediaId? { +public final class WallpaperPreviewMedia: Media { + public var id: MediaId? { return nil } - let peerIds: [PeerId] = [] + public let peerIds: [PeerId] = [] - let content: WallpaperPreviewMediaContent + public let content: WallpaperPreviewMediaContent - init(content: WallpaperPreviewMediaContent) { + public init(content: WallpaperPreviewMediaContent) { self.content = content } - init(decoder: PostboxDecoder) { + public init(decoder: PostboxDecoder) { self.content = .color(.clear) } - func encode(_ encoder: PostboxEncoder) { + public func encode(_ encoder: PostboxEncoder) { } public func isEqual(to other: Media) -> Bool { @@ -47,7 +47,13 @@ final class WallpaperPreviewMedia: Media { } } -extension WallpaperPreviewMedia { +private extension UIColor { + convenience init(rgb: UInt32) { + self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) + } +} + +public extension WallpaperPreviewMedia { convenience init?(wallpaper: TelegramWallpaper) { switch wallpaper { case let .color(color): diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a6d720b95904f40db565ee3d84b2cc0498aec01a GIT binary patch literal 1966 zcmZvdQE$^Q5XayBDeh&`hP1?v<2aF~NvvfE0b+E#O+19u+ltx*k_-(#J!i*0y8}N| z?$5sa-<>b^$<6xuTx6Ot0UU;J-xNfq?P3$KV8FvGCn4Hs$*2+vu#ft)#3D?jaq9tvk$<+0@ET#1ff@f8w zI4{831gNsQ;Hu`JGR2z`eHUK0APb(bj)}!Y*9i?as6q}CM#>$F)C-ou1ReUYQe{FN zqX>yn8qpUemS{e*kUM5V#s^krT9vX-FJQaKqR>@Bk+}wOJMC)O zK*AJuQrTeBU_Nmt&d?{)z3ma#YiOU+ij;`jNNj(i`Vxm#ip*vblu}wWS_|wp^4Mez z4it~lgwiMpVvrO%<#P;!2oUSh&{;~|k|L@qI(MWk3MirJOiozf9q&c>c66(;r_vd|&a zi?&DaU$LHjX7+0J_hz_c?R~mjzCsp??%t9x-pTpvz8~fZ?!Mw3jU)f^`-g%2rhPCT zd^Fo#do?`6UHkxjA{^v@X2H|OOvBUInF;JGu<`CzLqD4y73~uc;7vOs1LaZjP*_9{ zNdA7e#ZbX!uz{en8lKQr{1g9#B=J-od9n#kBp4H4wR5{49*#|)M|00OXyy*p{XnIZ zRDjqI?4o6%CnPaWJ0KuY3e4vPlJ!ee2`t$0uBwiP^Vsh8W@PsC-mS45r_*pSXK=dP lo-IYmH^VT4P1qGUzuo;Y7`=O0+i`a32wj%!%Vy>m;3+# literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json new file mode 100644 index 00000000000..6f73ecc7a71 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AddPlus.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json new file mode 100644 index 00000000000..1caab208d52 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hidecaption_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3d86f95ada3b469222217193d04c35626601af2a GIT binary patch literal 5259 zcmZvgTW=i45k}wVSImn5JAh~VegO;vu@nb^9Ym)6X83_#ttAH3Zo;JvJHI}ss(X5d zr1&6Ehh2TCuKKEaUcG(u%@=mb(`08eUw--bG|yjtIe+!lbUMG2|4wet58s{6?;k(R z9(W_8u8$9=+mF-X&FS~Mkr=?Stm6rXpd$4R!8 zLP<`jF<8WT$I=o@@D`6Ot8rSq3(1E0IN8)_;m-=)d@rysty5R)XOM0Xhs$b;vL1vz z4^!sLi?5+_I@8v*va627ZMr>>_&|=kx&YI-xNIUP_ltEkS1VhIOG?pN@$d%IV!ZP? zZ;<_YZ>*`H;Bs_$V~27LGM}Eej|+=U&Tw^%>$3%pt?O>94uq*+YS8@+ct3GAPnh5! z4wsFOvgr-FCCP=A4TjmeCEHSB9ncaBZmGFQR5WAH{)s7T@wFfuY-Xz*&cs{_Cn0z$ z^AH|>N!H^f#<|EXu^GP<9~cU3}TI(HQ=+rtz#Mq2wOb61IAiX zVw`7pZ6ku%dR$o3ZYapDDKs^%roj6SqwY_PchJ6oG55Z1m);7A?+2 zYGO^zG-vBLBB*ADifOjlaeET|+(2!nsoJEb2!Q3!j7yn4>o}ha(jzD)Q6q7PjR6V6 zwUxN*gByFZXn-;49w^udwQKCUiQ?4DcLPyn!7O{l4 zKI;$9y% zAkYy*vP4f7P~bXR{%?sj@9!pQ1=opN(RyeYd27DBbv%8GwWqHa_QA#%iRaj8g}6XFVq4Rgb&DA7aOtT`hn^d?tvaBXN z3@PM(^C!t0$xC4_O93Ex4P{LTjh9FPDeH(PCCE>+%Qa@wm{DAyXbr;=ezl|ikY!H0 z8M2=-jYy{V93D(cS5Ni9h8~RBhipKQd)>oO7h$^7AwiIK8c0(rA^prl&|u9oLR9sH zsP~3A3h6!;rWz_tZ$gL)tq{~qWUK43l6I#s#F{^H^E4G&>5`qvAt~ zc$Qd@CKG`cj}ie~0g^*!5}aqQ!dw<1qA@`{EbUw-yK*E@Mymp`R+`;ir;-=SkwLzw zFRf9@a=nRygs-Ge@ywJ$q%>mJ+7$E{CQ>fNiY_H2>BEGe->n}Zd>f(2=slx@RHMPP zMQQronMmk}k*ACnfPmfEG7d(2vtvXY7lH&dCQUBhp{rU)v0GxEBL4JDsv$_c*tL_> z=#T_#e0aw!)zEm!DTfa!34KG^&}>i8Fi@vXTcB zWqLwJQ6dUnB}y`_f)aFz9iSmx3rA>A8QK%Nfl6(n2wQ#0fXM1f^|T%xV=nm--7MK; zRgq^*i`7$BG@ABoRp!BHYgJ3`%8Er@^-wKcW`}K zkJ->^=0%FPRR`KTZ9u|qoN&O+WuG#}pHzCU&F z&(piR+pjNw%s;KFrNW2fd0aX2_W5t?OgE?N<+q39={n0NuA4sH>&yA^bY?~QzO}-) zw-2y6cC`BnCTM&eJA8Zhj;+pef}43==k?_`Z0(EvUm?4hcsA^2k^^4Hg4@Ag-#*^n zU*118{o~>E>qMoT+F9k_nXaE+6Wo8K9LUdn9*9QrDGJ%}k?P5)km=Q)p-T7g9MN{H z=<(t9?*8;J@wxEuk2kpEv+L#K>GS!syLX>AryOoBm&aKStPVc>aQD9xyVuW#w-1kc PoM?QWUcLI;zrOiD`KcO1 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json new file mode 100644 index 00000000000..8b6179bcafa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "showcaption_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..174e22bccde4770c8d82cee573af091079b6cb6f GIT binary patch literal 5347 zcmZvgOK;`28HV@z71|7t6o^bpyof*$Aem{3q76EAx(l*!&Ya9R@c6X0Q>6LzdESo{ zC41_X{Fs;T`jY0Ww{O1v!cTb^{OHEZPrnS~_~n=5S6>b1`*-?vn2-GG@8|pXPannw zc&$;7PY>stkHgiQ^M7wo=j*?GGrs=*_}l4m_|M=cXFsFcKECK*{cp>(ETJSnCi`%1 zJbfJcuunf8Pxr$-%_%y!jNVV{;#|(-N5SCAS`P58!SEdVsjNOO^ER^hkx+!yuc0CD z_?lu2F4j>#%wZl+xU_`ibJDn6@*G#j`8kFq8j9;WNBk|Wc*o!Tl){>vrn)JXxOy`s ztSgflw*>E<5k@!z ziLmmEz7dk%O4l`#1M^yqZ}Tjwm|W|=uC=k1?8klX-4Lm(HN>#*5J{^Fs}Zb-Q_d9R zBC^>Uv20oAB33;j1ujI25K_G+PcyZUAw4!Eo}jK`ErN-aFw3z`i*lYkrN-KaT;z!c z%Gl5|1}WLEGOuMdPm2#83l6!5(e*TNt&kMPN@%WHM$}d%^fAU!foyUb8e7D+O6`ow zf5AfS21JEQ?K{+-p+hQ>jMUD;YEU)y*n-_BU~ARxIH)yrj}NFfeh(u;(gdQ%8q{Q@ zr{pL%J#)ZZWvQoEsESvw0ubdIFuvQd>sd2=fwjr}*nC@%{%{ozeg8m3q zj;(C>k*3VGmXGiQ?GpW5yvbEaOH2#WE=b2!u}e!{P(lzg*-)Uh3}G8$PMlNBh>qGX z&c4to2D3wTg;tcJqB1Lk)O$%yO{qT?v8m_?;;5-P`+{^&$+=kvkEJrMnwrwQC=5bt zqMfbjOQi1&mJ;olC^^`fh29_pn6w{b9a#ihTspLdj@nu4waFxj6Qk{0x1ftsPCD!k zrjX&etj|marHp3Uxat_FxMi(j+5$iiI+9VnO%^C27HeU*aDos`9hmI8gm-=+eU0gdx%omU$d z0w0Frf+lNfOu8s&l+UejOUk6hAm=0j)3|~(R^(Y&DEnMjn+wV)g%_lptO8~{T+}WI zX>>MqCj6X}LMv*g)rdo$Rbs$WkVQea1XK-a(xJ=t=nOfhj^K6g!p)yRmL@QPUUAd~>oD$-&sW-=~jnGi9p7fH&Nh#d(=YEC)( z0{x`Ww#;)ID{N$?Q>z0)2~fO;8Zx#ZDZSD}VnhQ(tQtiEnqq{SRjrG0?Ag3p*&>b@ zX;iI#2t8I;+ImEczv^DnItjuBzGzn&H?d}6zNHrwjE?0~8fM|l!7TY*i+K-D3kDt1EVx6mevz#1MPIhS|}Squ%C z#0pucg*GV`w2T-u$reKHO(&bM8&Y;{R678GwFm_3LD4G(#8x^^dO-=*JH~0?>fvb0 z$p2%y(ddGSI&$}rQ+W+X3M0sEB=^u+SA(6|IyxlD$C0uQ|CVXwwxa#D9jDvgoiST7 zQ7!6}d{RqjB!yO2xB2?-+4*+jw0(|@b(%#x<$v&I`@UlL{IGXt=FetPr+3d{XUo3~ zU59%-Y|H0?GjCtUEh<>g!-fc|#x0V~nvRY^>+l$}4hdrm*}laA{hxsCCii&lj`dV^ zbc|Mq7A&hCa@7k(S6o$;^4F8nt7I!i33jYwT6rG5J>=t1A$@LlZQr1yZU^ff;1>Aq zZ7;&TI67kLW?pB~o>uCebw{lWEU}yG2QxklKmTdCAO1P$J*qu6Z7)-LgsLxAA-mKc zta!xOAF{5#zQ4abJ&r&8$io&tSO5Cozt7{<+ne|22EIGLyS@45^4s_WX;3|J_v;^X z*wFs9t#o~Uy!`rbIzNtjI=gPmy}sN(o$uLE?`9i(d-DJrc@Q#mcwH0fuWna=yM2eL zqdd4Cw|!n;er2xz#QsOfVWsqJB{}%mH{2b6ee-m4cX|JO>TeI{p9eSQp}KD&KzX^f zgWC^kXkX)fN8E3pqmVhyULZYmy+DOWkmo2mj)e_9J>1;hogW6CA+P@WhH!lLc=>q# ueEjV8-RCtZSJ#)z(-A~@Udi}N4d0DJ7>iGK~i&D>@s~0bp{o#iGySN$O{J1}SIDb?g;5Cgp zogenw`(^WL|M%@~fBoG{b@g`s-|n>hwODQ1Pnq%XwLHn+ye>YP;uSrFnylL0FEZ@$ z!)bR|q7FXUT$D}Pdt+=?_XMVh{U-2ku|}8wN2@Q+U5*7GrUpdNmvvrQr+sqPCBZUb zrPLT2=1QtQsoi2df-;pyy5O?G>d2{<jVMxS7)2HsL%bHVIR~uWW)q2~p_FY5 zBU{?Bu@X%b43U9V@K?$-TTmAic;~`UHMjyO(TsAj9)peSFGr%*AbXvjRM5$p=n!$= zr$Rn8PAn{sFZAGS%fHa57SG16#(o2 z8Hq9fxnZ0?)1L_GBr*Fg0$I6iFTUe&1KUPFKA+@7jYOi`s?o-ue&6S-h>J6wS1C+O~OSvTqRhLYA;x*s7O zOy-#9RtCtab(x{(CI8hLgr?a;RyJ3&8I-S{B$WXq<|PG0mvxtuDs$tJ8HyNsgErS> z&5$i}BfH#t8^Rxu@8n+i{cicVcpXE@v3Vg54j&zk0S^jl!a7KR=LUj>Sw;e@J$nN@^o+P|(o1RdKi+kqK{Y{>=@4;U!9s;rBvxSGNSDWIAYE_DI|3bhv4Xx4B+%%QLa zqBYQS$WOqib2dIur&OJMv?gFKJvh@a4~^qyh%F#2Tq}#ly%>Lr zAP*bKN;ti+0H~Gy1Zj$}0eAX#pXvmSMgA?wv4eBNE6D2rE5^nS~uDBHFB!x40TTme!t_X%~3lN0Sywc*#D$Vd2 zL=&KIun4kk@zF?R0uaWIgl8}}vZ0boaHt7EgIr|1YVNT%NwTd|DIQ^*4sLKBVL^g1 z!);X8vLI509p+JGFbY}rD%^E2p!K8*1EgyNfvpM*X1vW1C{-ZJ&8PxO11d?5xYA-& zuLA8%l-o8=ssPGyw9~j=1!C7lewXZ3pi>N#Q?fLU2zQ{ADd$Xj748nYrV>RjgbytDNx?l&;;$#~XANVk^ba4p6;0HMtV9g;C`vD=7 zL-cvl=@c7=6Mt~ahp@3;t<4UPgJxrVba&^2VhcE_gI^x*!N#8 zhvk=rK9BToALBy_eG}miD8c(I-%oHuU%#ttzBwF@=aYK(8@{vPxB2y-Klf_$X8U2^ zfuHs_x7(M;&+6U!P9g6H4u=7s)9ce%>3V-UetOvLPl~?eT=(U!j)(L9fF0>G&H&$R zAAl*mwKQn@*h7$f1ls&?dxKCFWpJ&=K3B(2m}?)!e*!YCgqO}~B}(ul8?K38ZO_}g zoLe=KOBi#gX}RqD!)dngGI!3 zp91-mm@?6Z#3e~o#A~0RD2k`Dfy7JEg^Nrj0cjSMGfo(%ihx}U#Tw(8+);+r851mI z*Q=%11o0AKy{vk5rFe)FyY~9)6&1A+(N&owGDS`xzSROL0-{RsR1u!b%{O3;iVAso#?X6MyY7S1%x3R=ZrUvKWt=VHZO+D0fZK z47EiuvQ5?75PpMPCHJD=@0L%CTZ7Lja4*Ec;ey36;6Oo%SO>`!>#`>`WD`Ag3%}-o z?yCVs?Tf>{eoa0E6lu5E)x;1WO@P7T86AAhIGdnNKv=_56J100IB3Yq$-@1fJh%E3 z6KN4aAriDkKpKY>L)L3YEeD55jpbmYzLv<-wojVlp-yKUfc#)ODw0Yu^u1{0q^EnFdx6RkN3f1XyVsSABx!mkVo zd&qLo$mGFRgKb`*lHH2J)S*d?%5f-YL8HJNFjm?vk_2^JPRn@`3=;K?Bdp&0ny|+E zRpU|Jm?41Zio!1!n)n4+t3aoF+n`nprvV@k$8n(0R8>+-3%Axj(Kw=xf)tH#0f{>a zMYKd$g~m`-96Zr!tjg4lK4DuF42_JUh|Y0Y8I=PBPOw@nFkesr;$Xr}Yv=%0=fBD! ziKRnbQ_xul=k_(BL#BO`NzFZ)N~FjhDC|jhEMb!T(X*>ml8wZmprLXpeiMfxL;T*1 zIcOdJ9sEy5ud7p`dPk^Hi2G~fXocsdQ-(=W97=>)94W>2QlBqkAc5sLEXoE9|mIqv=l^#4xoMzy#N<2ZPR%o}x zHI)?QI#|KD`qH+jtIbf|m8Q0FI8Ksw*o>2N!D#M!5l>b#=->~!U(!+GGw!4~3xdb* z7f{57oNlvdAu@G`4)5`zK;}Xrun4a412ViwOn}-{!%vZ#6|Qoj;8v^(a|Q+|JPIEi zOc+!n6m$$b2m^f3Jo^Y2*Z1+YbXBUwthm)@0%Vk)8RSqWYRBtqgQq6QvcaHlr(b$GVur#Gtba?y)SXwSM?&IWE5} z^iiaK@o1k%=u3!vHu27;{?!Ec^6@og^Ud*ix<9M;zv1f&{x-k<^XFb|-flncEAZ3) z`eys`^hLcNZxa2jz~Tsy(|J6#m9F;Z)91V0{;cTx%~f6Q)#-S@KVnDvc+G_1v=SwF?i-H8Uv2NVhttQ) zsbB8)9~QWEkzDCtg<~nL33-nr_aTHf@BqR&`v}4}k~Zr8ZhLds z-!1qSw)y@w=y-ZQ-R_^Mr#IKngp|$I>2$9sVJ3L(E%9^+<2mk~ux@chsLV+pM61OdLS?0u1)huARK_iu2q;=5R58?iKzB@- zj4LUv2h=!=BU38mBe2=Z9wT=WN`5CADdxmBu@Fpjh~SPrEZD!2GGdf5e~_@^m3$R3 z5K31O!zDL}V6qM&<_b$;$`zeNG^dITq-aSLog;S_Z4}dDl~9r;VpEdFg5_9@vyr40 z=`JW(N-)Jmq+%}K1F;>$2&YhqVSrOnkai;ELxpnlDXJXOMD<1Xh$K-G+bw-Uj4qwD zCxnvyz(kP=WhB?3SB(>1RX;N5ToM_@ldibirNI44gsH@|2g)>UFV0% z{l#~_``;zEv+K*N=elr0XsP7z^@ou1{9ImKh_1h-pQ6n9;$7F@9llEgxS~_zVc)HH zVsY91-n8B7^-H6B`;Yoo>dq7rH96 zubit-U?HOM*$+(C@)Ka zu=OKS=trc`Tw1oqO%UH$Wl%CzB*}A@N;6w?5krb6?Xnf$=PrX&N=kcl5qBISR^Dky zi9oz_RAo)wK<5;cv}o=OwZfgkt!P3SYN*e~=pREb2BCPxj5w4QrsPe}7H^(1geG|Dk`6rH5XrbeNLN~$L|E1b&DB&&YLxH8cb9!<69GBXL?9)n;Q zz8aJk+K?fANTxRA(&vavKSS=;_-&!4PC12d%W>*Sq+AXX>Dx2Wi;sdPO&lX>=FoVV zW|6kO=228J#)w+H==x^cMQx7{ddD6XQr< + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/Contents.json new file mode 100644 index 00000000000..33886b2d9c0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "IconQuoteRemove.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/IconQuoteRemove.svg b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/IconQuoteRemove.svg new file mode 100644 index 00000000000..4a556256b0a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteRemove.imageset/IconQuoteRemove.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/Contents.json new file mode 100644 index 00000000000..e1ea2cf8d0f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "IconQuoteSelected.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/IconQuoteSelected.svg b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/IconQuoteSelected.svg new file mode 100644 index 00000000000..6ac6704909a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QuoteSelected.imageset/IconQuoteSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json new file mode 100644 index 00000000000..ab416f72c81 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hideforward_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b05af4f6613f27493d04c6f1dfe89e37b0725c0a GIT binary patch literal 2746 zcmZuzO^+Kz5WV-W=u4yoDebE2Zz+mIb_qg&SR8H<2Qxd_HS*eEJ1FGW^J-??GrNlq zR{MGC`_-F%adZ9tHM?vKTe62g{xsIUd1K$cHU0ipe;QfhtDpM)-TB^nfNOo~bUyZ* z2eZ2F|Jio^`ulhG>cjF^cQSt)b~Jn}T@J6?sr~1j_n{`X>?9Kvw%vnik3Bz}y1hxT zA(CP_I+0pqwGRt0xyUsJZ?p3`1{^rMn1Vi6TuN~;ZBS>b!{dpU&+(X`#%S&qSc}`4 z1?pW5T4Lvf`da9IRyP-o@@8#ve2yv@)e?7f*wDVHGHg%qe#u-w2nbYCmUPGX=U zc7_%nXV5#LSbVgzH-6s02B8P!QmGs<#fGx9DK&o!Y!P^YwhR{@TOt2baA)ofS{JfU z5ml9}Cy>EAE+Q;ifl%gTy~~ngO&|qY5JI*Ae+hZPXo$rpvYkN>q9jtDc=F1|=pvj3 z0uo~hKES0=&E$dP5DMkNj#J>2F!@}`hiG>O{!7Ztz!FicjEE9zCI@E(2CE@aP_&s4h!T9P)*tADuSnwFNp?D zNiwQoS5qP(Amn4oTC=!N7<+d8V)UXWicD zAS8$ml@LHf1;) zV~NH}Itb|$8OerN3y4W$fB2OFDilCNj8q#Ap6{SJT#6(V53m%*912+D zNmn1xQ>|;zpb;v@kiz*v-F(UrU+}+_Ecz!{h4>0XeUdI%@9W3d<-g1EpR))e#8mlOnoiIE8S zn{)AK<8xXKY=aF)LBo+d+<;+Vn|e>a=t3#SA;o~8LhP0H8S9DzAZUZWG#0!E7<2NJ zF2qODaDoWf(~0>*G6C~})=Xj;wFTI}kWs-!4M8QOPZCgClS9lRQvrw`s~%#HU<$!t z2wDahL!>;=0n!Ra0)jMfCt5+3+P=$bZxIu)c_3yBRpT04B^Q+No#&n0cTsZV@ zFu8q< zLHoxW$$YwaTKA{J=VRBOtlo&$le??K{@m{oQLjT6@Md!aX0b{Gw74`W)Gkx2AGfzi z)oKN6dx>*(_zYWqmj4OJ!V{!t;YkZTwSaTsSDW)@ces0O`sLVvG8n8GYt?THSp?ky zPah^bob0|qc(Xl*1ed6%kZ^eld64aiVZh7y0-cYW?XEu>o!$@EXvfRb;h}$JUv6(- mjZImt4~MhWf`JhwVFja5diKYnSHdhtTde*1RYs@eN@AHTGV4j7)< zFYW58`>Y5P&qwHVv%b5XEa-m^WBr<8;RWn%=Bw@PeABKL-}c6jjz&`#cOScc(>LqQ z&4``1+x6XM(QajnGV|4|^;h*l8EyKfzPJL_x8Gq3E4SUIo!>OGllHIcMSFhwN*!NJ ze=WAnpN&1R_`Fy=94kYMo9W7sytBTN+Ada2nQD`cSsry$K_VT=IF5A@dCkd$0IhXo z%E2pQZDY*NsRg$4&KpjOqV_rTZ#o#C9VtpWaLCRorxTH3R+KflXnerZpqcc2=gt!ypB^)I}W6<&>l4EcsE>*FTELGS7RrUo!SW2)8xNHjd zf)(loDG4&=u_IpS)@0iam7JWoUW^`Yb4X>!*uwF$u==)aJ~yA5RrBkQp2@H~xFE!y zLQ>N+S&nXbkuzyQi?dHb*{oyYlno9gNF>fC2Sr9_k3)mwV>AXs%nGA9Cd%BeamJHV zu;VD?(4Wh!y$M7~S=K4WXpOvCgM*3Hq>aNFjmkRABg#?1Ajg2?jWCkN&Mv{E#S*N* zHlXa1as6V4S-2<{1;QAA>B$!GSD!7z8i246h#YdI7n`?P$Ff z>yBJb&?3Q*H`;rG63Jpfk+DWw#~yWzwFd)C0z@ffD>|?a*~I7t%oKwSn3qy! zgQdu)WQiEoyh%1djv~wgdFwIF_n>u_ZN$8gTy_wZG+C58flwaV_K-7C_%50ZEklfO z5@JZ{>y^y1M^6umlLrA<@g%-R9Z?pM0loM(AajhE_6EhZXT|JTAb2lMh+#G<%Tmvt zsgD~RvLQoy)+p4_K-^9-C&z%8eIld*tUB9cLBjuPymGNAtzrZVnXbIAS51%DTgrn zUiBamcH)9|pabVU(ydP?1S#1JwE}zxZIPV%^JXoaG6)hPPWZl;R@g*b>9Md{s z9ReilaA3VlNFTV%fpAPB7(*5U0^%Yf1W3`aWF0dPf*}*h;ll(Dc}VE29VZJia`baT zA|P7OjUuRwi6;AbNeyuxlW}k{XxMwC5~62}jc9)k6cwF^I@GkQ%dv&Po3u%dv_V6^ z1o~bk^sp=NciJd$d2FK)$x{DOvdjD7%VQfgcDuFxsVxW&VrerbJxFb+>gZz|g`BIc zMoF~)bsP0S`I7Q8lrMagO7#i_mFhKK|N8sgd#mJzcwVnzufKMu7u~#T`|;Pd|L@}M zeEUcLHrycP=iYsY{!LO*ofoI$gzZ8L+kCu=mj4@5}}4pH)4woV01=-0j=!$tG;N070sG{<( z;t*U=z{f{6kVoOd-aq6RUwL^Z5E`I;$f2<~fY939o0ZV*--G1u0Q(Sb1@}pKn{hDY zDI`34v;uXT`Sr5hG$QX$ClL4Pc74-6Q%|oipA8bv&ev<`EAsX6nq6G~-HH|vciYW3 RU9XNu|Jgiw^7`GIe*g|>xzYdt literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ForwardSettingsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ForwardSettingsIcon.imageset/Contents.json new file mode 100644 index 00000000000..71efa15f5f8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ForwardSettingsIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "forwardtools_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ForwardSettingsIcon.imageset/forwardtools_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/ForwardSettingsIcon.imageset/forwardtools_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..26877de63a729e258fdc6346d2cc16d8c1c19881 GIT binary patch literal 5030 zcma)=TW=%B5ryC9SMgg$t zNChw-7Tlwr>Z(&!r;79D{kw0!baR;|XRJB>>)(?xUwvi1{(3qbKgfS4zr}ZdJsdwh z|7Zf>jhuRVemvYhOuKi7|K9BnH-GxZy#0Rrzx~tn)8uB`KgO(>)LoO!H zp|lpG**{Er?E1^o{x~^1=iq#@#s!|THf8g$Mg5#&NAD-cO||8ijZ+jX=H+=mH|KI{ zW`n8C{nn^rb-@yco4uCf#bFkOY&qt&(+yVkNh9jyq%-BMT*_d6p^E3dNz8BE`p=| zFU<~yH64RYZX1U^RT6#kxo+&fNd4+UHC57TZCG+~Y4atI)Lsg{RtZdL+?1}e_!_1U z6RJME0k`d{J@PQ6+4~l=bk}GlKa(k97A^8sD)PEH=q-{E8#TsVq2ww7hS|9qeTAMD zT8-7Dxztjfe6fobof}o5ZKJ9e!VKAS3EjY+&&AnZqv(f&CA_ma*${Cc(@d~NX=U4T zHet3Dbg__MYR)AiBM~|>HXgr-aH&e+yH7iYf+&i#~U7a9U$&LDLh zLlvL!*xO{eZqBK4PTCsvvf_*Ac@AhqnPEEQU>FHdo>COj1C^BoDk0OEz1S*k-k-c= zWZ~&jyhPB@GwTCSbHAeiL2^ObK(dVpkRUB5FE=2oxYBPZ-nmpdmV#q4;ztxUkyAv) zOr&RA4U%UFFHU4yu+k7!!Ai+ZPKnGm5F{>;GGsf1h)Ix8D~IK|%C{g`#1hkttb6)E zEQJigV(BvK{qzbw_X1Ytf+r_)Mnhc@8oj5!rA{1TO`(Fw+(AhYIdsW&H=v^B?j!X| zY>X$^#|`lZQy%29PJg=RoMu|q8`peyad)V6I%uzorA{$)hx~ll$unrK!}WZ zCt#4K6cAQD(KuMGk(u4kdFojpSt6DTil^xMqX|i&Od;4YnBazUHrU#|V*t5sE!Gma zj8mx;CuPdHSSVe&B9c*BOrT|FAcf4GWsaYrMZWfDz3141ZDs(3CJL!<8^uMm9F76- zQyS-TkakBd>Fg|DkIX(1LMoPgx0%kTa11k0Aqa9v+jiOxdEZa3ingb08q?lk&oY%M z#{o$IQc9k-HtVz7nS^HdlhNYqUD@EJ>w; zn2(l0xg=V0Pw9gBQX=29NCZQyT^LU<)g)-5#Gp-!PB5~L0tvQM=W`^~>%lCw(bMPXT&hp!AM1(4g_@rsWwQHo#jo)GaXLJXa8RZPt00U=COfyB~RA!;=+N6lfQJy1; z^%PmgfN;86Vq>PeIi>e0LK%0X2!mqGL;1Kq73pi4Y^*&*1Y`@9@jm|INbLZ&&36a$eV>{DZ06{@=P zAc~V$<_E?e#Gnl*qc}D|AY~e1V%6_k_?iwS<9rVG8-hq}t2KU|EXmbN6_G__IH#B_ zMk9noZjk=UiZa5CYoRE$ipX>slqse^1(WnCsj18h(C`SD)RH!A!!NP>zr^sFwB&IA|pklH#(F*J#xILI)2a;cZeFy5!1QI=* zXLt`t5-fFu)}6s~k~&dcpftb2=h{gMvY;)+sMci3?Iom_Dzcx{H^qkklE6`n8l|!- zt$WG7$WK{IPhET1Vx#HG+T<+J3%BZ-BKxVXO|F=XCq6R4houdoR4BG+38JEw(3=Y; z6cHwdh$*Jgs3k=-KB>fyUv$-#S1RLu$@+3d-lHrpU09l%zH{OC#`#^$?hnV~>G{e0 z@K4^&@Y((2fB!j{-TT{5hXH(d_;7do&FL5O!?_i-C&DY=R!cXBr_;}m`@@rww?8+- z+}qRf`EcY$dG)h`?{6Q$Od8PU1(87d;%N7myAQZ(B!U~W-t+eKGtas!|DQlMmhuZr z65x|wcnC?+ce|bE7oDjLGS3v*AWcIFPfX9d5z{?+w3L&X4p~wb*2_@P0 zZ1G486~<(8WMfAX-u10)haI-4sPz)OC6bdf1L9j*InfySpjU{`x*@G#ok3 zA$u^!Z$yfGe56Jf*H>>|kWRxu%BaI1e-28$e5qc&8m9d%|2xi|W>iR*Se)^y zJc(YAad4V!j-{-toGw0~7Ahlp<6Kl;TN_I7;19{nY!w|B<{P#f2t{(%CbKRCB-{tQ z3sLoukgB;#(ZYo6C&)NNO~xCa zq1(pS4j5V?#)PmN6G>Y`&N$V2kQ@r#x@d*Q0UCjhO77OJMXE$G8!NC)2Vc2R~zCdfGCrO5>= zOOwxCi=omXLWN-SEWs!cPnb7zGs%8}FcWR`2qxKxCqX1XR^t0_hW+sC*G^rq2{~}L z=|Ta0_Z35ztoAX-h%s8gnKzgf7`tdh3ZW!r5urB)qNAjZPsN2E!&oUk^INgT5QXSH zWdlWJZG9qck`)WulTWKlE$BAWaqr7EC!1n;LU;Gl#)tpNo% zJ<;gF8?x8}(OMj%+@oY727{2-F%QR75NCnoP%;xrL7U`!v>lX|n)jwFcSvj&!fvq0 z8zYws>~+q?I!I#05JR%at(Jdg9)f8_Ln#r(ytj2aL<}kv9i^T_>@ZqN-djt8&`DFr z4&3;Jd_V-wM(-jdx6f~Mr&2-O z5v9bbT9Y>D9psEH5d>&tp(eb<5lHpyXb1*RG8PRYc0Bj2brwwHbmIO%oC|_wL=tk-V=4J4IJtjJV(}H=%G1-)fk~zZEUXRw$&-Z5$bOd zO$oVf6Ugu<1Tcpkkt@IT=+%sP@Em>eIEx_19LeIV168MpF#;PND4-b1geH6p>>Djo zkg;cLocGQee0NlzAw+bk$LMkV9#{&62s`2sDR^1^psON}JGKtdl0~+7GAVP*IE}BO zOjUb1pl9)RWwI{;h7^KB$uNYfUM)WRBHGu$L?8^u##wJGMAEEQgDT(*<~GFqgHpIt zBA>tw3OLTuQ1HB%O8xjth~{nQ<@KyFb8LdJ2xb{|hHGZf1xugRz!VhE-+^In5o9P3 zj%9A7o)jd_gV!w8#K(gN6QxisA;z3F0i$4rB!|xQX?JkXv@?zGB<;en+)nr+GT&Mp zB`=p2FEB1Mn{W5~!}+A%{f^5Gew*L^{cBR2>+RjtfS;$^`|az)C-rXqypu0K_l2j$ z)6H}`d_0cRN%8gRrn$R3?9bC4JM!IW0k5}5U@A1IyJ0aTH?Pf)_qPaDaRxVP+2``` z5w`Rs{u7YSQ+(u!6FkX=YvPyN^LBT*du;mYIK3b6;Xc%xTK%JNxIt=w+mG<|%RjXj z2=aIgVH`b$aKHL&m5-nMa-s8eK5p-K({bSH{cr_3o}CU4({uIg{`UDy%I4;9I4e%L d61;hP|KEh@>y>nSJlAwwC~mm8`0kfC{{vdl0^0xp literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextChannelIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextChannelIcon.imageset/Contents.json new file mode 100644 index 00000000000..4fb60ebb2c5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextChannelIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "channel.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextChannelIcon.imageset/channel.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextChannelIcon.imageset/channel.pdf new file mode 100644 index 0000000000000000000000000000000000000000..898047f96e6c133436001be5f8d1c21ad20958c2 GIT binary patch literal 2267 zcmZXWO>Y}F5Qgvk6}%Wo3WVbDYe*mn(AZ5;v_)N|x1a}gy>V2?+E!AEG{3&@a4D`- z@4=uS&XDs>-Q3^3dqswoB5n2b$Dc~;*RS=PH)`BJ$iK>6^3_k{{_*rld%(3w9Z!dG z^Q2aH<3HPBTz~&o-@d>6Z#b&I6&YJUZO@BWebx8+!kWmAW6;Bs>SE6q$6>FWA*K?N z_QoX&G3h7eO|s70u&^B%`^3pF%$sa|Dej7-;!8a%rkJxHRFLyis!~4KTy!uYd1yk9 zd>~37OfkR)XG+2zRXt{F;R2W|r36T2=ZPXn&Y7L)Vaquy#3pBL)f1Mglud-iP9;Od zaUxA}gj}>!Y)IU(U^YdEW7(t_Z3+l+%E^|ZGq!@axsnpI*p*Fi)>9FNdx#*#G3F|j zV*&&zjKed|CLePYyHggF}cq@y6TYGV-_Rv5AqRbFjV3;p*aIxX>&n z5W?mc8wwI_$NA}1NQ30D=yr6iRiRbhqrgrqOE10p+O zY_Bke?=EGARI5Vx;T@BdcAje$gsejq4yf@~(O`&SU`Vy9zaXtQIvLJAl%<4by&c-F zunTip&8+ud5*vz_LTgPO4Z;J&mn!1k&aG1#08E%wal97vr>C|Sfi5uYtFx^7_- zKV}So3pU3u%lv9*VUeg{IAC$M0VbDN(3ss)WI9F91*CcNJoi!&AHPw1^{bM1s68_0 z7fBwU`K6-dQh%sWlFI|O`gXscPDlOWcRXnLR=@rG*Qi(bo5!(%pT~#o=I!)Ff2jGE zVqO-6gw#({f0xcDt;gf^`7n$}El=jU9d|qJPvagp%6oZ+_nQNljtyPBs)G7!yZUka z08_Piu-4~$Zl}-CrDygZK`u^8S0{;s$A01B`0eJj*-ekvs$UM{M}={uW?$v6#-#L7 z1GkS575VhOBWeCCB;)BB!W;QNsX~a&wPMMO;_0y2?#6?{&yUp)cL>LeizcX2##-ElTVb#wFGFYo>X7&p}- literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextGroupIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextGroupIcon.imageset/Contents.json new file mode 100644 index 00000000000..02af66c5a56 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextGroupIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "group.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextGroupIcon.imageset/group.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/PanelTextGroupIcon.imageset/group.pdf new file mode 100644 index 0000000000000000000000000000000000000000..001e5de8244c881c97a50212df5e447f45309a4c GIT binary patch literal 3052 zcmeHJO>f&q5WVlO*o%RrKvw(x0R#r(PZVtsSLz`^&;v~~wo*xSMY?eE>-&bJmXwsh z?Y%tM)Z^@%nK!dLdvbGe^+t)nnBtPxw_h0NZ{PCS?_VD)KKuCL_DfZ^!0^m|tv2`V z1J^K|5770tetMiN=zjO#x;6d61^Bkbrg<#3)u#Nq_x<#ApjtfLww=?fdb=L5dDYZU z+p=oNMYcRUtDpHNE`{u#@kQw!q}1;14^Uxc({8I`&1M(X-^;R^U!U>w#q?L%u)kPd zl&Rn3x8rY8IGdt$yj-(vcKp(m8)k)8G6v6`@Ik8N`I7m3F|tm>@OoFGeW_)i#%gm7~T2HOc|N@%UF&oGv#cuzl@aZ zfkokrj42EmI?5W(wR0(sn36)d&ddvwoOO5lWooP||pAg;5DTg2bVocUsC33nD0$ zOyJNn8W$qiG%BPWM;`}ogF)I+L>Xc-91WaAC2dlvB=zX&@5eqTAdUt`gDt81$IZapELL5JuW)73?%>nH1!R zLLv)m!3J6#HAxwRi3W(YS~>|NDMi}=Etpc0Bu5eo!CH-2lTF@JJFDo?XFzOn-~=L5 z3UGsbB6gJSyT_s^egdu-P6?x2^u#t;(h*{v(L@&wLc@!Ky2(3>I6OKO<`7p2twXdD z;liynAcl@m&37e^Xt^#Y1E&*&3MyUFC@7kvw?bQC10XaY+9VlJVx59PYd|qb;mG5$ ziYsXQ`6sqvzcE&A?uH`{Z*+_^I;Q*cTxl!Gpo2|@UpVH`1^*xO*su9SfBBftrsMco zv+oThv|b)nc@b^S!-+P*M$WG8KPo$wtuMPaI#=Cgl&U_A^=@u|MB`GW$MJgk++Hu* zqOH2`m#X`>cwaPsc5nU7fPU`H@p0Vmzs;^WwcK!F*#CC)YlfS`yVc`E@iO)9HgLp2 zz;J!Znf!?E^t4YKWV`+0_mmp0JYlEMzq&@upxJ%HW{~!x{{H49!oEt@XV)=cgy0xUWJHhyj4+K?5=X>^`@;h@JI^mz#F=;wLA^bL??p! z3@F%NmUjr1QwDRsi}<{L!di6{e*(RFq+pJ&u{57j3bs@Aua}Z>!Ht;)rJb z&Y{2DJBKj^e0azKJ$y4j3iq}{$YK8NTVYrqLzuq@R_1siP3Z$Gi&1v~d4(>ZC} zHtwKyTP#=AmXW+aUqIYfO}(yO^H5QX38SM*Dy1S##lU!*7!*${*P5jMOPLp09ew?b)%B zaHME_w!6BzPSvUMJbC;2&2y{sFj!^O?zi6urCz*HFJBIu?X`Rz+#Fy1u-Sfm_@q4G zrL5XN+;6V#hSlrMUpMQ`<+rcY#k={p^?vws-~iJ;>bW_6>VI@z*X)z`Drlc;4MDB% zhQ8zUVZYuE*68G|i$+<`E@NU+cXQO~7#eyzSdJ<+g`{jp!9p7MJ6)|!v8ox?ZLa4w zJ9;~On&DBG?m#!&!FK+1R??;#TX4kUv@a&8q`k4;a1X1UEjCo35L1m&aX79g7pkno z-DFv3bcvR|KpZOibH8V<;I_7fbF;F#7MqeKY(~dK2#g{;E+$_UPM@r6mv-9Q zXsc&?aXv>cQFpo&S5gGz3c#}ebY(M8@!p%vTBCiak*i^$YFFSZsH$@aCN~exEPZUE z=u!>dEG&eYjE~|&x6oJ4FJYC}T#uWdFu(U0=)h{9J%#fcku zVa8ND3(m6YTn0|3<&dL;{iKUEg&O&fs5zs9Gg*?|Xp+d1GL4N!(c*F_z|!m#;ZDaC zOir5?JF+CW>Qb56eMJ4ig`$ekYK$MbSlWCXM{+NESBhM$hjEs=J&NbBzL~4`%`_QasFx96M9&fXtggwZ+jTk^yr)+8#3^c|E0kwcekbAwRS)O;jgxf|_?=;Ma? zgDD5HOnGsC8TzTDo2tx&VpI?72)taZCOWn>JSqo|1u99L6DU^6Vu27D@lHT5MJXUr zb=<}h)e`90ZJi^Z1tMRt3skcs>Dtzaq>!cr*wUDY4eN|IrFlmKvdvm3*>f1HV$N35 zlyxDKx^hH-QA$jpab|!*`p($LkI*Pz>!aSYY{X_gfPh94sZAT%2DF?Q1K_6=&a#m< zN0#AiQ9f;%M7e4ovw`obseBSg%^?D0k+ylao#cHzoE2?H+Elu|#h#%nla3Q47m!qP zl(jnSo%b9ZKlN}*k11sIvZ+VTB_EWMLj_vAM+yX!ac|C*V9Xk|OgsyzWDxz)&?rZu zg?myL%;g+->PaFPVr~7jd(0+5BPmAMdeR97>nM<5V|I?~WDIKjv@|9KWw$5wWF<8t z%|I|(gjC>}X3Or0^3%pFxzXseR4&=q`ybK4;b*3D>V6!` z`(TYqOi=$D+)WVNmdHQ%KZf#dCu+P$C23h5OPYQoWrV?s@LL+dw8|Y0>&RN#sgW`G zxQd=PI^HdHq@O3PZdw&6J`yC+nmgu2X8F)0nULVTi3RyxLFiAlig*T>J?>ExcpCVI z%c(L|$uNh)hfJRXqZzu^KTM$5B1k)0v@%9V^OZFmgHdSF1{PbjGdWbo_bISs23Rh1oVFjLf2P3uZx1n3YkpJYu%9)0yqaET+i9S4|h>cmMRG{_qAse(aBL1{i}~ ze=$Ic9A6l$zTR$k4}10gS6(3SS^e_&9~-rLd-ZWM0Y7f8Z?0bLzNq)d)ap6r&paJ0 zU2gWf&-d%iUdbzp%gNluZu_v=aw56g!M9iUV2TQC(CjUSK>h8;>ie5(Tvc*|OLaKs zV)vQ7_Qd}^$jlPOJhLPh-1h^I7r(fAxVqhae7yC`{pQ19R366smH$;@GKdOz`tU2Q zJ*jPnn4}*=!O9pZpS~(EG%g{2w=AI`bL+J6;r{C8c5^@Qx@Yy>Yr^q#zq{K!Q%`TM ppN&CTUG8=dN-o@@zIu1_&j#_lFckBD(}3Kr9Y-5f^55vN5tfU^^)A>v^hAPtEa+ z5p!XU-|4QddaLTKKIh%X58r*|ZCw^`oH_pO?~5^Ce{H_`W;r~5l7E-5#dm)=Jbr%p z$wa^#S@rz#bhv$3b{`J^y4xRae*3L?|NZvA{qyqAg#+C9n6KRRH~le0+iNT_nq*_` zJteb$Saiqrm*@TC;+-wghwO}x>~by_^RPvO&1s9@*YPtT_tn!L}A!$$*3aEc>rl4g($UqpPfSHm05l4GZ0dM!u5iwx;Cj@ZgQ5 zuT3=Dy2G0b3#k_uv-qGEbmcY(Z+x}MmpDi`ML}rII^w9?Ft%%l4Ndf``a7fKtCbeS z4ZLt`sl5qqs_Lx@oTt@Pb0GW0Ht!le@;_J$&Zgk16uYw&k*8!vHb#mrfn~MWgl0pxqZS1~E{mF%9nxxgaxaQ*0=4&3Qy&OYp5}4AsDP3jpElf8Ps&3wZ z+ji9+d04Uyp{F9rWy>BTr=;^Vy)J(Ru z)#l}kTeaxis0wWxRlN{q$ez#W2KGX!-btgP27oc(oh#YJgad_Uf;CF3xL%5h)=|*a zL4Iwe)Pjs8-1KPc5RVdSs1k8VS^5x~F1^RPr+*l`Vje0rpMf4o8uclg+Ab`T^kk}_mF#)L_b*cyvvU*lU8EMbXhM%Dv;AeLf*V6k)= z^?rGSo_hfsb0LruThLHng~kx5Z>bZHSW~DVGIvlCL=Ii@!wsnDW%x+F5*y;P_Kca8(TAyLYYFaXE4DH>s)kgc*g+p!&+)Jau};}tzOEM zb*WOiazrAdw3tBaoq-fGch)(6hF1C7pY@()E4G;d5Sl5Zaoeaqq2+K4fS=MhmxFXT za!Kc4`Fdo+aA*LJExTvRuLrZ##z>#1bE!U^e_T%_F4X)CDU-vWD}IIdAD74F&)mxC`}I<; z7i&~vLi!tLAXc5}Llzl>jt=|(mvV+DQwi>?rEz+zV`Nnqq2P?6E!=m6Kd2X!JZq z_7JNm&A0mXAeR9y#AEPHjE9_*;Z(*lHzFn*(=_DH+^Ll3h)1MD8Jw6%2O<&>HeE+b zP7KOVJOOMFjYu)#?xkXQe=}2#i&EB4P$OiOUkmIr-)<&#ae5S#yVw#VGIGxV8xhRYfZ>7 z9t>^}Wl%9nZ)H#|F$z`&^*lxPayeqcasyOME=+U*cM!sz408XGC%&-u za>x_mK><2<2Ibjlt$;!B5GRF%ms1ld$mMM1QLV`ZcT}Aos>ps(-xQnvOX6q!YLv>V zbnPYkB0crwt-AJb#@4o`zw4>LY5fb&6xlEB)ZdE9XyPN+p4s1u=y}pvbJg0`sE%6V z!oKEGnNUQSnEIvICYq@f8$_u`aDO!vSKg@1*CnTSEAkp;edof3x#=qxo;A*|Vs^iN zd_2B9n;-tls~JAKzx?|j2ebQl`}r_|KOR2a-F|!g#r$w?A?=&^##zKO z2H99DuPjM`&wAiF@b|YbxA(`-uebj4bog;W7?+XT@y8VIT-gB656{2r5046=!mpu} zD%UXinA571si+I6)T@n^E?!TCH?*0G( literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/Contents.json index 8a22af52454..43739ae6bae 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ConversationInstantPageButtonIconIncoming@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "ConversationInstantPageButtonIconIncoming@3x.png", - "scale" : "3x" + "filename" : "InstantIcon.svg", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/ConversationInstantPageButtonIconIncoming@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/ConversationInstantPageButtonIconIncoming@2x.png deleted file mode 100644 index 88d8b6e0a35af2e8370b7adc96b0193707c0d1f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367 zcmV-#0g(QQP) z0Y^1a5Qe`mN;CJaDp!OQCg9HRAGrKma zq$U@guzSvSh68x!nfG=6#SUcoDv;g)=W9{mDCg5ias?#Te{GWWKf{>nCXkdrFtR+% zpW0Bg_4nykgtY?SEgsrnwAE!#Wh2_^vXk`8?0*I9pM)gmKG4~zSn`=ynqVZo^q!ui zYjuMxC$UyH$$mKpq@U4N*E~!1J(6t2%usLV0KISul(9(B2LM%=tfQHgk@Elm N002ovPDHLkV1kAppDq9Z diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/ConversationInstantPageButtonIconIncoming@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/AttachedContentInstantIcon.imageset/ConversationInstantPageButtonIconIncoming@3x.png deleted file mode 100644 index 4797137de8b1c68a71be66506f44ade313a3b384..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 447 zcmV;w0YLtVP)wsqUKy!O9Jeu zu<3u)a9O3OKc~|x4|7H$$3C@sAdn6Ay#7kRSv?cQTc=j(^o6C?sJ|wz8&Px9@5g** zhlCZ1nwt*bB1_x}!de0NtRpS}AHDvYyg$XGD2jDJp76z0=in%dp4!U+d1dfGF}t`9 zV7ul6WYN7F$X0TIWs?Z7OVvTM(97 + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json new file mode 100644 index 00000000000..922ad2ecb08 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "quotemini.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf new file mode 100644 index 0000000000000000000000000000000000000000..15b20f4560ec57a72291ab8c2f9107f307654e2c GIT binary patch literal 2878 zcmeHJ%Wm8@6y5VHxG|6vh$K>ch(KTXnErV}d8Vy8Xg9fBl-re|~%PJpTCM_KPoCVEE$SeR%CXJ^$j|HP9h>Aq>;M#R!UD#ibTA?P)2?eiUrFZ@57!mqB+`1yK1D;oAU6Qg!8 z{3?u&t_dZCNO`eiL2fx{igH@k5p+^*`Ao$`QfVhGwvjolQo-#gCC7*YQm`})4qc&iH+H65>)>E4xNr75>gj&0x7SSJ4YmQKB7StjUL@g0HP-~7*Yfz4d z)S8}JF;Qz4)B;&hs~6M;$^x|knI-jseg2J=><`9#c{ki27|{*M=mzYsN|9Jg0v(JR z`LP>R{klO>`QeED#045I_T5zV0ozVkZ(GtO7u+1fX1XB!dwdgLJ>ODJbnAdp!yH;= z*9$-5wj_$DmUa2uUa#A{_1*fV=ziAk^X6+e>bosX_j=>hLib1UHK$q|n(+SGViu$O zzS%xLji)IlFIIjMQFK z$4@00>{(JKxJFxUx zX#XR4LvNs#qsw*BEK3CS@l!p$+uR{gPVuk0XAJo{ub!YQ7ST6AW|jmF*US{{uL&Nf zeV^xTzOC-}b)RehnV}^J*1xXkKQ~&Zngbs`azG!)+otbao9c`pM?duq*aIG zeptP9i~Hg4bw4b>dnfKbO#kZ--Cv!SsysrSUvBx0f7AS?WXK@~Bl?$)*UeuJ{kF?8 z>7aEI#!2mL2ui#F)IJ*@2sAF*=*6bBa??Enb}G4?g^tqXWQ>6Ut)z?iPmu*>b9SLv zXqZh>Wf!BR{a*Rtby+zxO1`3W-Wi>R@zPj0qhL`98;vtgXe*OaDP~wuG8%8JCs4VZ zyaJe{_d3OrtV>u(z8Xf!7Zp=S4Xaw}>_eiwP!6AyH4|_%0gd#i8dh#5+nXTUBv(%Q ztfEB;6cR|zI=Hg16%49@0)`MZ#qVT7Ezu1miUy&)o6gIuob!n`2c?ywUNtO|qaeNW zN?T!M3L)7@t#T-z4jz@loANP63g1c-;VTI3CAyLnHPA~3Hh2gBLB|wm<=keeo{lWdQvCB?Q~!<27R@lyYOB$7ICRES zr!jJ%rWW(s_st$5EQiSo=$kcG#Q;*pgrGAJuL5Qj$The6MqCtaV+d$692}(SxHuV% zNfz;9mQbERZf8uVh#L$JV7*fYtkmEdbR!~vlT3Dic%;Tq80uT`QP?e=fO=pO4fG2w zGf+*RToSYrl7cxIXTKo_=qGCgyD!RvA`=@JRMJPQi8L5IgRw{u@|iyM%@7N}8**Ai zHj=VAlEav-gRmtiB-FwS6J&($xhNX?*0+@~$>^=8%|s8pWs#-PE(a(Ex(Pu{7SK$^ zQHe%`6nR74U!b_zdILql47v=_r?{a(CY2GYBGAxA{b zaK5BOsm#t4szc-SdG0=S+wK>fyL@I+r|A*kOMT9K@#<+uOGY6>fp(qK5wRz znz4|hiMUTl8zN1qhMHnIZW3n~jGS0=wG0ftwl#%ny#3T=#p6 z2{_VW-h23~+464?;l-B|7Ah4%HVURBCN;cI$B! zf@Jg6)D&ViOgvt)q3-6I6f#s$$sFL0s9F8i!V*%WV}zosIWWdv*%Bk{u(04dib?`b zjVm`M#|G#^Hs8+}m%UKn+9EkRDFWT;>xKP%yQho%>DEuTU%vD^hVe15_-4D^9S`E; zZ}>RCv-tI&KL@dRSUn9D_-S}tuiovxh>zp^Ftu#s?QCf|9Cn}g{csTU`LV3#?snVb zu!TqZ_BewNt35E`D>V5+A&9?R7C)>X5vrgJmg4N^Zuc2`^(_7akcp*mVTlqvu){I& zyVY^E**#ru{jwiEbxOqUG*I+c;980+JmC7^7F^!MmLXi0FCpA-kTpHL%BqYMFCmhz zE6BQW<8i-QZ-#wG-TVF?<#=`2y$r9#tM%jSQ&ASn-R>wT;ZoqmhxNY$qA#CxtNpR0 OV{uXIZf?H)`Tc)$7T#_E literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json index 17f972537b1..168869f39de 100644 --- a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "AvatarBoost.pdf", + "filename" : "Reassign.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Reassign.pdf similarity index 52% rename from submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf rename to submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Reassign.pdf index ea8526a585b6df61876fa1a771773fdd6e13c27b..a1e5a5f0b471bac01943e2aa415bc5e39d4e48c8 100644 GIT binary patch delta 1285 zcmZvczit#U5XJ>3pwNgZ}iNq6JMNP5qKuwev zqN1ZfqN8EFx3`Y&?yAwxo^QUH@!n7W{l$;>i|08c%BtwkiUcVsN%o)IaJx9U`?5$6 zhrb8QLkKYbd<}+g?)8c`DX}D<0}15FUW01Vkz`ipd!%z3kdmFZTfMOiB`P(hp(tCjk(gE{Uo|oz z7O()4V2-|X5)xzPXte>DkbUnc)DF<>Vn7wJ^1a!W3@M?wu3w4~JR#wc1FTv?RWx;E9_XKS*v+B=%H2^)PPa}w=ineN&g(7r{Dw#ueY z8}IkgBsr(r^QO_!s@e>bqlt}ZGHz&NXEPql-Stu}jty<~GkV-M-q-4D*|%1G8hgLL z^?j=2R)>FwFBh0}@JE+N^S{gZiQauZI)Cu4zSH%_Z`^wI3@L9TBHcy|!=q;xk2#ZD JEBzf7Tled({pGK_hHBh9KPUTs*Hl~biGsZr!E#TV`}!7d z##!ye_?9Ia_LhgcBJ8TCpTA;x}T8QjjEsXVcG&%OT^IDB>gMFeeKa-|iPVs?NjA0Vv zb_`~tRNNm>mC+dDS5@CPyLJfoD>3|gU-i57J%Uqk!))*_1S%y{f=lV?Sy|5Z&ouMa zafp28Efdlh&*>D(L*g9&EN*VA<+@!B{4Tm~x7^Q@AqPdaSVq1~bIxIL(QUfj&0SSD zbaC1Ixvrb*@7|2bH$Qh`y-0~iV=b7{{=~UCw0Jt7=f_X1lR<`Tm(LUQ?R^iQ!fyTU z(2PINunx+|3H}3=g5e1v7Q=oze45(%IqFp`St;uDQPfR^)kF)=3R`g%y|^K>w|YY2u@K{bZg@#t+#};paYj2MXW54RMOFX-I5s8dN?Z#D%h~v&d|Ut z56T%T9CLzOX{ctwk2hqJAwxA08O%~_hzJa&Q3{SqH;E>ZVxy2C?2PlswG0(m>X|GB z-H_WEqs)CvER!DM+L@4vP0=JFgnb9^cH+B5H}CqEaz@!Hz7FsYm%gl9v4ZZ*$2Tj^IZwJzNaAq*Fc=9g#~J z^=K;6ctr(P(rMwSN;y|_$xuy@k#4wlAbxfjd*)CLxq$bDHA1S`5ppwmyoiRj z%&MT2qJf2Ag2O}`%c|oRvscCpicJtsVpc*pnb;vL#499vjZ2g;0Y-l=50U@x#2iuc9t81N{h{a=H)<-GYnsG3Un-BSd|jw z&#ce z;Nn0_LBBXE15q5x8wiG?>d;jK`ht{@)j=?c&gMM^YJ8^z#bFxU2awe^|$Vv7Vn$ox_Z;aVLI2Myohxr zI@HoqVG^b5X5Srlb+e~&Tu&dL7hQ{y4;Vpcv1D$rdu6fz27w-30z2qN<92f3(pw} zczTcx<3^hx#Qq})fp&aKDEAOjmcwB2p^28V5;`A4@ss8OUY(8)9gqz+xM4n y-phTrZC=rr>*cG_bc^e*>uEI0;D*J``tJrh!eslt+Vz9dC?Ar%c=7h9cmDvudwR_P diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostButtonIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostButtonIcon.imageset/Contents.json new file mode 100644 index 00000000000..4171612590a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostButtonIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Union.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostButtonIcon.imageset/Union.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostButtonIcon.imageset/Union.pdf new file mode 100644 index 0000000000000000000000000000000000000000..073d03fbad39e1121d90a2fbf880e068180249f9 GIT binary patch literal 1249 zcmZvcO-~y!5Qgvm6>~v?N_cILKP7}zLaC~@Dh<7%9=00;sgm6`yHu24-`648q<{}A z`|*6wcn>bmFOIbgLTFOd{rD+}PEP3bROEJ6j>7Eo>QioShdW}xX_4xORj!w!I?sRR zO`g6#qsi6&sp-XUp`}_cQrpe(xO?@EW7LYNS?*LvxpC7st*|os7_FlqnH5J(OE8zB z4R*(@_ zLvX%0XIn0zX*qZWf|4m`KoUYlVU=`I8%Bdfkjgph5_yR0Ql15vaY)X9CFTU5kg{IG z`q(CjC#192q~?`oF2tRD+@0e?> z>f7I6nX1eBHgCY^Je${N-2+`Ct>@^GdbYm=@W zatnv$)okHqy#gb&dX4tpRzc&Nt3J+Wh?GhK)6vhQyN9lR5q=M{X99knB3#97u31st9u%=I%A(+kDq<0v(( z>iHtC1b#x*hjWzUuJwZz7PEX literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json new file mode 100644 index 00000000000..dc0972e2578 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "replacedboost_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d6cbcced6ade83e8531def0af86abf5a5cdba7e3 GIT binary patch literal 6171 zcmeI0-;WzL5Xay5ukcHy7O5MLe_N_5(JMs=5ar5S#Y4z#+e5hok`xvGdcL!9W*u*& zUL|Ty^!nwir_{%;Bc;{$x8FOZo;_2~pLfINhX3tMtuKBUHt!D~ zl!bWBPwfx8VR_drUJifVu7>MxU#P1$^?$2<_h(1F9zRN-zC4w0@|){ZjFh7y-;<9y zI<>m% zqKTTaBHL5)Hd?iYm`FPxSd3(oju=aXL5ZcDFt1Utoubv2CGSEG8uP*8lsp*~`e3md zVLJFtHd|{UT5W<09Fv^M7|feItU_pekDtg*%BnNHPm~g`UY=S*OIaHoCJlxg@5iTn z^q5!KUPpW*JOk&vM^G`&X?%AIu}7#f8)kZIY@mq6UG~{09!V}l55*WV@Y42{LW~+p z2qtNdxl)hl_&_T5K01QYYiK4~2fJdQbwscbWo!T5YJ3OJ)7;n2C>SWn5J7==C)A^D+s zpDoUUD5yaw9;g5^&f{RFoVXZ;-UJf^*MMj(5g+nsBhF~8II=XJ?P^?W1AC9Nd@R7! zCu4N7unbk;IGRq@auq0R=+SJ)^`qEPDP;v2x4JMtuZlW$jEDHe^?aPqPF|oSUXa?6 zD=wWSloGf}lITKe1HhHIosP|eRZaM+pm2>Gw_*+F%4l(TjqPby;a1VkcMN@bQfA~J z%PSzD=?5!oCWmS$sz+pWud|HH0plD?cY@VZ_ z9cC^|#MAi@W!1%#W1>ZIt1mNEia1rRSG1#LfD4afjOLhqFbpbKA)pU4goKDj%)Aj8 zLe8G!(en`8O=nKR!l^n&Wej}TqV+U*+M!CUMpyCk7vm@X%>7=&o3iOLmc1#lue7Qf z$=d7<=qiFEL~}iXi@<77OvZ;xXmmgqC^2Uc22#i|zX4cS3)ul0QLs%kC~;88TjEk< z08gl;h%+eAaOwrn4aEeALyDr%B7yk?2OSf52qGBGWri}8|A;RfJn|k;==!JuNWdq~ zeVa@g?J%A+tYr~6mo-XfNWlV^Mrk}R7xt$rp5i)h`8=GQFeSu*M+a1OAgnQixrd-Q z!Y(KgZ3gbg9&4M?IA|Wu@Q=wRhaLj_LX2EONK63hdMIr&GLYpIxgP=K0H2thw-n?U zLV_0YBj(2@19ZpPgVq5Q38Ed%=%{>|q`s^6So*cH_s*K~BHIj%SK3Vvk`-Hx2 z+3#5jamZT3cr#Dc8E;zUfTGULzsax2nA1VVtHrTnf4)G^o6-Y+j6C+hAE=&!3;!#B z+&jaaWH{<%0fPG@NWMfMBGz0n;}0}}Ie(x)wER&@YsDX;RQw@p9-F+NMp8&B;tY{qEyr&KJ!kG|Dg2%awLOA!@EL59cZTwb#O0lsRJFqp$=qMLmiTX z6?HJvwA2AUZm0u|xuFicJI|1Ku>$AwE~sAsRJX1A>JZNvp$m2JOTsyIfDIe!kld-DL+oN}kI=#QxaE@ebfe3cx5t}a@2!@LUc3)Y z7r=|JH=FHYuipNO3t@Z~zx@5jpcb!}?}sA%INaPWUu-|Ax6>P%9Cmx*+i9ljVZZ&j zTMc`~SI*aEx~uKxFl?|Q-#4Gc*UKF=g&I6c)vYv3a$CLl{`Ll@D)!)7oz}V9e#BUs z*}sQU&4e4+Y9@AYFAGkNUo8*I_4fVw&`-PJU8hy-jr(!xjYzl+ zI+yT1*goZh!#14_y7-f!=QC+hL-&68tL7T4SD hL9xSQfERCW{~2KYc#Xf@9Y%M2cDZ}>=$oHj{R>f-&CdV; literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json new file mode 100644 index 00000000000..68543cd8721 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giveaway_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a4930da9e853b210e99ca2352e6b43fb22a2b510 GIT binary patch literal 5593 zcma)=UvJ&U5yjv6DfnU_DG+LB|1S^(XzZpa+M=$~x1bN|^|h@+mRd~5iX zUuF(YKGX)1EV3C$ELLLc=hcP{8LTujE*6%6=}8YQxx#xqlo*}ck(2K1SQd9IVNAt# zkm4(*&It#Xg27?&HChOqhpS^QA!AGpEw?-mkNU;O0JbBxV6G3bzxPYO-%WK1Ar$X+ zA0M#s?g1Mo)FoPW#9~-cmzq1b=c@YYdG0q}-0le>VY=l?C5%L~YgbY;kcG zL(!?(H#jKBTF4|123x5?oB$-uEsG1$W&EghF?&)Fh(!he?+}+}v6p#Au$3GmdqjS= zTttk78Snwy8d0<|bG$*os#k+jyrm47qbF^(P6Wc8X>keG0O9l!Q&qNyL>}~4iEL|` zD<&wOe8M_B%+Js&E!D-UJBapyYwyr$>KPWB-?6q>4%W0P^CD?bmz0w8LNKdl@uP5q zTvNqZZO=JnfxU7r_vMu2K|DIr`N&U^Ylx36W4-OG@{&Bcp!c?&($dUBkK=jV)|qjO zVU-ji7-}{y7|1l#sqV-LT_pJu{2!~ll0R47_~NUjwgB}&c9)8a4So*KJGdBE%eU}2 zc;YLeI7HDcR$R;C@D}V*2|TjaGva`Z#2MJgFOb@@vfz@0Byz^mddyYiAXZOoMoNoE zp>~zOeAE)=z{q*Pmx1hBp5nZeBv=J`+@W0DhgA0W+Fs0B~0Z4jyQvQ9hETFh%KPrT=gA|D4ZiIcs@BNf+IfzqryfNQL5VdHhY># z9t^b^C$T4JNUg5t=OQ%_mIEB^E3=72!kTgM1J?J%IERuuN<~Vf0lO%Y&cRFkd1f8h}>miV?@&+KvM?Bb1>Kr10pbF^~~3HTB!75&t_H=u}YF+_Q&Z%SSXd zG|b&>R436(XjPZDM)!nhsY9tgYy=_}hPbI4uQVZShWBXfDR}G#S#s{J6ttkvB2y`- zkjnRUR3HkKs;=pbl~O3MrUHr{Op!tk_7N-%2B0rMem4#|@^p2MI@VX=DO}~`bc!eFWr#C8!b?##t{k2>Z2?db^G!(vFct<*GPxq0cl7Xk9L-ARN`5FWEtnDSq^72vs722$lz- zRM7Is^_A{jHi!^3W+#G{)Bs1NMMdSciVhN%%a}?X?K>qVf&xOT<}gn-q|z!j6b2a# ztJpwqr{2G+6(pH3y3lJCh^7bYY^853>=lGm5>odO1=${Yvzd`6UAdjAeW4u&q)kFc zS;ri~%m-9zCp-{|Mpp{N5>LYR{bIv;dR4pSo^0^Q(R*{Q##*w4-H zd&>qt9z$YBm{{>1{cH>j!hNsMDqT4kSwt%=8!C#VRf(AfvSBBV zh1jg2D&ocN3VmuI1*4|xNC(takUN$tYiWiUI^>L~7N<`d15#<{gz+l-p)$1nUGg$* zaL+wJni9_#pgBmT1I=JR)RdgTxiXq2s5dm4U~oNC zlBx{?K!=5DbbYeQEYYK^Q|Z7GaYuA-y| z9CV|W%gZ`~3g1~uXTZb?l(Q9v9TAt@?gOYkv_K$L|BdxrjUi8Ir)G8&2OvC#Q-L-G z0bdA@pk^WnS!e>}gtz4`Xxr}>Aic#gh=hd4^X{&d;t z`uz0pe4v;PU?uoIt64m;_BPvgM-;;(O>Z|)vGynOYi$MgHi&1F(Iw*JjbydllN z+aFC@f12+HM8sc0$OgC8-&nUw-aJ0{>!eU@diClzfBNn}yz^RB literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/NoIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/NoIcon.imageset/Contents.json new file mode 100644 index 00000000000..f923ab5c87a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/NoIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "noicon_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/NoIcon.imageset/noicon_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/NoIcon.imageset/noicon_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a85f38b99b80a4c0fa865344542b6c418a621664 GIT binary patch literal 1820 zcmZvdO>fgc5QgvjE9O$EMe5r7X~)vI2dQ!1SK}uE-L(bX8kd{CioCd z9`DS~yfc}c-K}pg1aG9EWXzKfu72uwpb&2SsjhEbd1T4j z{n}S<^Y#W-_wl!?V?UYT>F_{08<%9!J2Sr{qdGD=c4~B0&4l9SpU6ihQ(hR8YZI=M z*&N*RRBK@j1Qdc)EYJ`ovM5aO2_6-jg4-xKRvnR16vQLo}$|0^#SEn7M-e?C{F3Y(_*YL zxMLI4D8_}t8v0=>S{1>XBW+yQJhzqW!0+766mHej zy{qv?zi?-GSGLFiSJ{A~wd@hC>*W1@i>ZRIU<30#tL7Pf#Yz4hk|>g0MDhi8bYUob zRrckfd0M)@wC<6mkg;(V{3lprlmW!kBT^VFs0k^|(?5iSw8aT3P>AIzZL#VlGAcOs zZMi?Vmig|zTcaH3U2}97aK7JOjG`o)rs=^KJP4fJ@BcW=e)u0N+kPlVm?CFqXKy~; F{slKidO`pI literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ToBeDistributed.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/ToBeDistributed.imageset/Contents.json new file mode 100644 index 00000000000..ddaccf8bdc9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/ToBeDistributed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "avatar_tobedistributed.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ToBeDistributed.imageset/avatar_tobedistributed.pdf b/submodules/TelegramUI/Images.xcassets/Premium/ToBeDistributed.imageset/avatar_tobedistributed.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c768d71168ef9ffedf379d645f39535c3396cff0 GIT binary patch literal 5365 zcmd^D$!;4*5WUY=^u>T3Aj94lz%bxd2ol4#l!E~y2YNJ?Gu_e*JbkipBdkA3u-74j6umU&htN z?owz>JT1`4X8q;1S&;vpzU4L3!ZXO*ELYpxv_u8jmlu)*JlifEhitkBAkMFX#Ax4rf z_cJ3eW@zYqFTHe0H}fpdV{sK?y+c<)-v^nLNuo-NbFB;wU1J`jjdLuo!5YPv9fGEl z*D8w8d#m8HKpfz+Rz5g@@Z9MrQcoX~6LBBp5(Hb3p)({SSYI*Kn`E4K!u2s)uL9^{ zq_Wlt(?^|53S#Jx*I=AVR;WIP=pqerI8g?Bg=UJTtO(pUy^6{sk38*~K&g$9S(nCV zWKs9YSQluqPhO?iKu{GeKp0ZzAudjN9R%d@Vg>(_w03We7ArSj>a8m^?lcl=W5x3}LD2oiov;W*+h~d0~3%thSky1+smdq+uz~ zL)3$_MiME5(R8PqS9TOx7qLh!IQpokB%zzAib z^!V5Zn`d1^7J?nouNkWZ3_WT~qia>Dv6ro2jO=9{te0|QwKmFE>)2QJfDx89Ad+)l zGRDSP1R*JOZ1#X{V5IS#?ILR%ensu0Hq`d-;M|;>#4cuVS&JT7*D^+YJ?7>O#mR#t z9z=5~VaG%cOTB?NT3d^Dy+^PeOx6+O=2}}UV53S9YHT#YLqu}U#|Q+QO~B5Dg7rQ@ zG0AcukBz`o)B(L1td~9-;u4N(M2k1rWN4F>n3$Ur=F^}c)Gi}IgsEv3e#M*?G_D1D>-Okq;}PkNj(I!{fsmkP=aJ-)?Zw|roEzoIDt*p z!9f+GkJW>@p9j7d%O-n|Rgk@mSp*e_A@*PjL=IMoBqNOmFc1O6Eb2AIHpZ$0h@g$c zycQ~sk~Fr0ou7JRjxXyaHk8~m*5sym&>*Z~n%**t*idrM#xXzG1CA~u2fmYl?0c;b zBy~z8@8NK{K>Y1LRtHhF^&xk&2?IlL(AU&waNcy-G$oZW0)PQmLP5v}Y@!lJj%F6y zT_m+;BM+!XTF0)~WW3BJH%USlgDHo6DF{_iv5IG*`%RXszDQ~i&f7k&@49YWotFz0 zMs#WH=u$Y{9O0K8GGJN!sY+;=xYtDYv8>#QyR57jb~rY!qII2&#tia&q`>_SXVWCQq+#FPGmJy?+*&$snM-7pzq?`?o+ENjR6aqR7K7Ypk9>U+$LI>x;v+UpM2Y4ksqn zO%+T33Xl7|6Tr)l6wpts3S>NPk;hPgM+dX;oPc6Ff^h9Sg1ni{qRZjYtbkMF5tQ&; za0ub@RAucp%d6{g(@}VTdV%0R+^%oNN8;ht`J*Ysi_`TQ@ul$Pd@atd{u(J32<~>b S-0X66obix literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Unclaimed.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Unclaimed.imageset/Contents.json new file mode 100644 index 00000000000..183ad0c3607 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Unclaimed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "avatar_unclaimed.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Unclaimed.imageset/avatar_unclaimed.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Unclaimed.imageset/avatar_unclaimed.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c284f77aaf6603c8b13f7e8b0ad23911490cf321 GIT binary patch literal 2196 zcma)8O^@3)5WVwP@UlP>AT)>M@B;_}G`md@Bt^GQZ$S^PR@rr7OO53ONq&7FWs1_; z1zH{!^wV%Y-aK;l>h|XKb5cPFl3MnE{w1V*@j||QDcbHX{}g74uim%a{qaFsfa`2E z91rbgFIG40*KO0T-@KC7AC|wGL3|dZ^thy+HqYy>ej{XIwev}0SL~a;sD_uDq3Hw} zWqb;jBss;WDElRf?@33G?^?MKWA?8n%UBOrs1aUiuQ{28ovU`$-3-;ZjhI2Nf=**dp*lKF`irvNEWsgG)*7MN*N$4g*V;JVlulX^NC&oH#BV zxEX1LRo+o>e#S`IWZAQ16{GVM&`YbyIhIyMlWYpI5ol|$Ovy=7F+`4eGqMSFO#$tR zOg$qF5)D2%a9*u;1&bN;Hbo?l8Z}vFv{0FlyemDTU>H#F0hJ?-QA##(L~dCUXv1zK zNRO82k~Jn6fJw1oM-F8fjeIQ#DhL=)lR1CYWg!&Yt&PRDie(`%Ck4xE*YzKmDydsXeB~zzzo$c z)su@}mI?wIRytm*Tp*VksGKp>$oU1?K~Luz+s*3E1EfrcX_VxtaT+ni%yq)J3S;uD ze(JjZILMEGU=m@ie*gY=D_6Ih`?dhTw0GOhtNxq(IKLL4V_eex%Wm?tZioKMp=k%1 zXWhEEyY9PV+aY2ebQ5^HIRKMfpv5@MP#u%2ciTIpDsu&EImNl|zu+ugN&hT-I4~x(LEY2M1YVoo8y!niG{&f5953653;jQ=Y-%It?W^;LuM$a#* z#r5j<%QgS~rF(pJ^Ikf<|9x?0nnwQej~{=0_s75e^{1cF&SovoKjF16i(CG+k{3VW z_d~T<4@#ECq3o_HBPl~?S6uL4sf2C_Rc_l7rKOCUx|a$(Jn~-YI#%R^vbC>jjQt+3 zoYz&Izg%@+9;#mZiV8i{uHOZ@rjKBi^XS7FW$ZD1#6ll#Fb!>*K3v3;gg&(L_J}TQ zZcW(|sg%yW82UPD@vCXqs}HEM*z96p*W~Z~a)o~vy%C<*%2dryQ>X1!DBq5^uB+x} zFzs_@U-Pr43$U~MRqDm(UR-to@c)(k$+q|M5)dWReP0)Vz|n;sg`!MK%*8~Q5rXJ=H^Otf*qND`>H0 z(4tw4R?)a4R==lI>LbQx{ny?O$q*I(B2uxYX?10@mW-;Cu9i9|pXel-atTG`La6Lv zwCXL8Eh-EDRs&c%UnVP+mdPmcF0Zn;pyfPz@a0~j;URXvpResgU4?p}ip&$LX!CJ( zcbV-C)-sD<*U&6i_nFu?Sq;g9&BsNnU^=GBWX{h?d5E4>nlQF=`K-7MUzYN$eU$4| zQh0G86FlEnrP|%}g&1IXJ9-tc7?%ml&qxa?C>>A|VV#*MgQ!WRU{J|pnMhG0WeFu9 zhzd#C`(<%BED3kR_8W;? z6-zgxv2-#&vOwz{quYGis8jZ9$MZj6Q{&mr-wv<=KysXkF)6yhub9-v4i^13Cidw!{Kne1!|_i@RD>q4sHh| zE`zO;Dg;Q*m1eAc9aw9WtATB7NimMeXC;Mcy9&@!xboBXVXjr>(nsvwSE<_9L5IBomYWNZ9|uegg1ynA zBKyo99&fN*6oN`MgqTCx--7!+e zm?@E0+p(U9nJ%$2!z%P7QRb)_8l$RXJPrJ^M{S_r0K*j0QSiG(=_3AGlWS27ZM7MI zUkd;q7Fr(0jeFpGP-uDBbO*fFD`L+WQVF;f;B}xT<2SKG?4<~{pSDs2;FS^sP`3OY zVA{^G`!-#~08#Kjm!7vC!=$K7Bjt{TZd|36ISJ$jRR@M#XrmBL zlL7<9M*d(@_0w9$L@oj@{Aka?!Wi3HGv2-qygeeiJPI6ln0t71dDKCNyRd2vu`3%5 zCql{Qt*Rxg42%uZib(>wM%m~E+9|NB8%d>PX3TvXnCqs%+)%%0o08R1O?ItF#O$Xr5QQ|C(LQ(J3B z-PeJ-N5qv!A>$5p505L4I%rW>Yh@)tz_ZcDA!VJEHWE<|%16QSs9X?-801d8<7-HT z$flkr_`Gfg-nRj~ehT1?CEyJ+@J<1|oGBz0a4nm!Q*Q2tEoV2thfC0#r-?8-RYQSj zsFr9?g1f<}kOO}v*<)aLi^wI|b#+t8__o#zyRQRwzd8^3_E|a50=Jek_9QEcU0Zfx z9cLeK2_6MTTNkAVf=Q|w#BEfPN5xCU_!(;72GoWrpf;7DHqKBx3DjCu!>@uhTpzW^ z`t*UJkyghthwjB9KgyFnW-Hh5fgzaeHk=4)87TSCkE7#&c0}SL&T6T{2RhQ6MD?m* z_BrXtx6e+2&T#8h^c*0ru?lL+=qm1R4Y`UOQ+7d-b#ko8CH8-?m#?KhTJVp;dZ?5NAEYsl+D(`=pFC7(fcjH zE^VhlLN}y7;I=z~1Y5O&XXH#M3dXp?vo%iv>1_eOOabWIU>pO-@V6pP4+cDu4TEQ` zf*RH47#XZ|Bhj{TdynZQS`mC|&exO-PJH(cD#@~=5SxqZ- zXqzfNEgAyX!IK=0C&&Jk!)S$c)OlA$(v{?CU~uW7K*9gx+_?l9m!UeaEL5CRWV(sM z)sx9bC&Wp@Q@m&5hy2+3|!f+3%=VYeC^FU<6#Uu*rto5EBx2LtPdRZ(tOm>ihNdCviO0uBaYa!4{( z<=Kn|t8);NaOn~0RY9y4`b7UgfDmfK& zXih9f4;}EBr~(1Tkf7}XHQ%8jD{orj9OI*r4-NbASHtRXqBTvdmr`Z<;0QI^)UnDZ z5Ga@M)X+h~T6L#h)k{9U6=Yd!89s(nM^HL8dh_%Pn}raZIuVp@J3cV3hd0I}QM;ucVpu~3z>keyEB6h57 zHE(X!VlLpT3B)%r*8AhR6WBJOH=_s?T@2;Pv8whtFzH-zc6_HU-|!m2tDLezo>5ZE zy#+veBDP1Ym>=u{v%2Bb?h}Lgi{sqW;Pp~g&#bY(+4V`^Sz~iF$@cj%NM+ zVsGK0(M|6^Ufo=dH=v%gC(~>^c`fICps1$Yj%lO*@b+@+?i5=wa5E1bdv93|X8cgj zJBi9!onQVgStn-i4tQ~wyT6m&PGCvNtI!0JyD}8{*e_~pJtrlE#dVq?M?P6G9I;a_ yhpM^}=St5?#F%Qpn&NP@d|G8da!s)e6!8VHd4_-&bMyGc!~XzqMT$I7XaE5IgY!E8 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/story_forward.tgs b/submodules/TelegramUI/Resources/Animations/story_forward.tgs new file mode 100644 index 0000000000000000000000000000000000000000..88e2a4ea5b5e540a75e1b97c5664fdd1991ea01a GIT binary patch literal 3374 zcmV+}4bk!+iwFP!000021MOVfZX3xF{S|}H&Bj#U+t0x!4~t}xO`ZfjFl*UXEK3HZ z&1PZf-*c*ZW;8QwQliaSNXo#bB=+>Rs;kbazRcIv-G|%NTT-k4tlqA?_S&q}>iu^0 zHZ*E=`DOJMzi#l$Cbjx)_12S7tB>vP>(40j=bJY_+-!f_Y~P{ayLa!T#OC();tutm zovYQ==J$&&|NXUpe0lv&+PwQ66}R>L^`Aff_~uW4|JzSLqn_KXJpY7ZUspH$Vs z4@OUk?WLH_zdp-soEWhwM(jFgu)X>9%Nv3Pf7xs=uJ2?DRE@bZHTrSvjI$fx`N)VX zQpX2y`$G8qb`xuowT2{w+#v?T{u(q9&GD|iiWIJ1DCz;mS zY6`yC@~iNrXu;Q8NtV0V|NMTR3f>Z$k;9`$9Nz86&6kTpP}rBPK5XH?F7Jx4uZy#j z2e%(rog3?wCWARUBONfooi>ENoyl*-Y5201XWgTcM@i!OxeV~^P!_en>T@A&D4l|i zF!^=Hgtbybj@p1GtP>cWP11>mbdDC80*q0TGHFLhfk1T3Qr|DDw|CoLFIYI@&wtw7 zeH3^85n_11xw^d&p^7l>R+4`>fkkZt#r03n#J%vxK^oiE)h{x!ul_B+K3{Rp7HZla z{Cu~)`E;?T5AUyz?nA+%VGBQ8UR~{}|Bh$EmvQ~=jN4RGZzp}}=RCJNA+fdI-G181 zcDvb0l5YQFyScu7f3y9(xM)I=ERteGNzK;XX`;GxGDGI#4r5hL>#8e4u`WI!2H_Dpv<|FIfn%UotIt{xa|! z;dKN2WVXgwDZiB@GIExu-NTYgmDC4JKa@!wYM{q;#&biMJ_eX=LI>kfKpSM*I4Wt4 zb~*DjOFFX5K?$;M8ku=fdrAwozYc6Sr@(es!*;u1`$@3fI~`!24&Kk56y5Cv-6KLo z3y0~oi}B;Cdwh366UX_O{6KZVcMoYYO&|{MgCv1s8YfA>cS*6pe#hm&2Hb+~ zuLIrPDbPLC(A_WSehPFaAR)r(b>Pj~cv5hW6Xy;?w_X`-XF>L;o5;uxZxrJ!$WCx< z6uw1Nj~&tKLE$)M43D-!qxKPYD!(M1<_vyq93suKUrw+QT^$w@skF6WCuOB z#4&lY+B9WDT{p4}4-}75i_HS+R=X60JW%AAJ(14YE~x%GP~D#b)ng6S!-DFkKy`0m zzlcE*!v#7is+%xz@Ls3v;5foei}C^WTUj#qCeMQFKIkZ(5%Ef-xdQehRWiW#osuwl&pcfZ`w?J7b+}E@h=G6zxkZ| z&3B!#7f^S6T2oHMahT5GxaWf>oPtM@G2JI=fk3YfQw5i_oS|cbv|$1C*8%A96o8&< zfSwjWKM9~a-5`1c=WdSL_0=1zlY&(=HXRp^WBH+3#bR30xzjIFtvsXhD+$)U|uC&h4)dD z^Kv=PXO88a*MaY?WWMj37gT>8sGd%N>eP&3{fEoz4;S0u>F}yxUXWPjS6F4CQ(c68bI;VZGZsB`{)z_{-Z^ofy*$-|cxSd?Hpr(pKn1 zx3I{-G5)EE&z}RH$i>mK&OuFb^Lig*0V^wq)A}Bc{1{EfC_btyhj;Nkc(^3T;Yeyz zvgg&rCTQo%R$-9X1PSD`By_E1U$zcY@`@s==#B%mQ+y2U{xPA%icr>#Tst>_YZv-K zt+O_K{?K@BOTUS44kN+oB%Q2?3WSP7bL5bk)_f{H$jyIGxv!cl^4wl}On5eG!+WZr zP$A0cDbhBfiG&)nFV~+?Hz~gJ93$t!ld^$9o})OA-iqj`kKwAKb2ZX6Vs&HLAL=_yStcVIS@jMk-yai(s0!MC?yl!HVme4VoiGd1^gQ=`hiIeY; zataFhW532d5a+;kymq@g|0C!Ext1@O!WYaDJmLTMj2zJz$N#dqekUmYWaM@;A$O=F zclvJR4v!#r1wh>s0o1@+VS6o>KQ+XxAyKFxk>Ph#D9ir*sQRDmt}XnTML6)3>Id>0 zkGdARnxv?&_i)&X-nO4_3|^Z@V^@wy<#V8g=}V=CE%YlnaPIQiY2hhjnD(L;8mFxA ze;BUXdBWAooad;!EOOwlZ=#Nx7||^X7$@+U0T4`ZoDh@{bl}Sr+8IVmeyI{*o7so) zCHOQsGG4nRD8-8-S$3;KlC|roEjN&OZ)1mDjoWT1fCcYKitmV90d>zYvg~-dQs=gdPTmMbPR?ftj{zq#uqeLjR<5vv&n14N6C^0ign>MR z3j*Mi$;Xml3b9o@{@PE4dsf8<^_ZauqXdP7uy|bZH9$vv{DzPA^MH}g0?2$@zZ@2@ z#wrVyx*~s&Dl0lIRF1db2=!;}vft5h2<25-UN{@*ZpeJiw-7d_a$xG71={O6vaZXX z#@etVqv$&3B3Xy-g+eS7S82QuBXqlwIXTicX%nGsH0W69_@2F0WI>{%C>OJh3qu5C zB?0t`-~NZ;2eE*PwwVSa3;3uHl0xWIAfsRo#ZJ}3u{ zh`&oP{D-~DO+611EOtQI?pX)i$uOSP?D8W5EMAr48R^*m0((PQhT%z``0XaqQ~5fu zfmxYfBUmL}r}`Qv+cjSWh7XCkJ~%I{NN}hmZ)jp)m<*$;rPlKr-sozCJK(F0#BVNb z4zKN=fapN#j45z>Db@$ayd{(sO7E+qgYWPbd;~eE9Fn2HkZI)g6@YyrCMQm~2ye^< z_*eFCac*pXcDj2U%ASiAE&lrtdtKMF@ZWqYMyw$0oqCBTIVQ0~2`iXm2=&xW;C%xj z|79vhd0_$@r7sDCxjJ+7?)U-#Zy6X<_X8wHN12&r@2cYyM$2BV#5qhPy`1jXMs^06 zNy;$-?WlDWaCYK2^2}ZwBII+4V4TVNz2nPFi}?%kK)y*BFonC*aO9(gA7u?b6d(4qPFLPR)`LlU9)A{}X0O3vpr($IQ E00jZ5fdBvi literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/story_move.tgs b/submodules/TelegramUI/Resources/Animations/story_move.tgs new file mode 100644 index 0000000000000000000000000000000000000000..bad9a18b3610a20b321e75fd9911fa9999a99821 GIT binary patch literal 2563 zcmV+e3jFmSiwFP!000021MOPdZX3H5{gq&zsR8d7e?Z%Z7AV@j1%A+}mDo*U8`cbaSF6Pu zD||dv-Pie##ge~&ZeL$qp2_O#AKi&>GyeGlWmqlc#TWGd*XT37k z;5#Y}A=X!1Rq6V0v3;Nzw=4LG73}T`HnbI7IcNzCb!oHHI2tPZp0|DLy6>6srH?79 znQc?p|ctgFHb%=n@tI1MK_5TL_ICb1m4TD( z-`S3h#6Dmf#`W1}yqy_addnu~dSjh5p_A4QZ2?ih!T;<|KFz%lCR&s$xB)K@9X8&m&KmN z*Yw-#;o>!B#sSedThkwx zSO312t+XB9?&!4UYts@oY^A7Lc9Oo_@*wTnQQ93R-Fp(Dn}`9o8v}Y@s(EZ8d~{YQ z{@AJ38C#by<)l~afv%PA{7`lOL^6}RZA>zdak3LN(+r%a|e>E?AuIQQp zP?Y@8$ya=^eQ+W?P_2a8=KJC(d`!L5_EjMcL7OvC&TiE0J*6#u%9f8M2xP+s_Gs{C z`V`Er41A&t(N}XL?&Cfo-hu5`1KXo<%?)wwZ-*2UzLg7RR7N0ZwnYYix3sfcJ5;ya zM8?C##AB1O^R~~(8^)wQd2%9lq;15z&AkhGJku`g1(z_^s)SlVDg``8<{}_Lw7IpE zz)XSC-OJMG5@T&JlECbCHRLe@3dnp=$Y#+VoPmc>H`L0^qIj_gRb*%3=U6wXgd)Lh zcSm$HB1dzw5Ed6$Nb@pYJFXV-W8VVN%aD zK%^`ug~ui*Qaji!Hjj;B62lXamH~7nHSl38F6&|x!Mw55sDr|vP_6&pL()VjO@X*GrvDFvkEjoY%;-`6W*#^5*E> z*A-GcaD|6+w`)rPTb$huiEFH6|8EXi4``irD#=dw?MVqiQT=ugQVV& zkUGtaA7OWeCl)pV!-+02lJ`(Qwqa{VJtIc}2mPNqyvLOiic4*{yzM=r5 zc1+R=90i`>g;n7lo(sr}_281GkkfEXI+!_o>k6elgLqsiBfYKIWQksaqQh#2I znN%-dd~Xp;DWQ*0(gJ=3>H;XwLaYhur4)iac-IS@yyd+{oQd|spMnRp#aJ6)KVWde=Pc>~5A}mKg^|5^krXh! zgKGp%SFk4}H&{7T^MV)|@FUPATO9zEO@e$RlsZn@J5x_y6|hj7wwEs+vvRV^rU?O7 zfu%_iLE$5`5WoRlIh4jv9sI@p=7z$Bh-Z*wD+&*p@d7WE4aHW3=oUXLJpv72wd4RP zH5h@e?vAEmc%eZ;V#eA;>C`F^LIR)F6ip?9Uk_E6w-1N!#ad2by7!=9|EsjuF92gdFDTQWR#IyLGoGm-1 zk|eR6_t-ibDeCAax_KWGkU`BDsUX=@K_6s2WC~psM8XbaNWcsi4lXg?bMjlv$?wI| zFFR;fuT6<00!(q!Mg83lLx1*O4w>Zgg1+b(c~Ln4Txsr|ku7+kUmth8|EN3watUrC z+oj2^0Xi&s01tRVn7*Myj&*=*VlMZ_EK*30C7%NlfrZRX@^>6YJGswe?5BKW*hA`R zcG1&@L%OQ4=<^=K@fIvIj~Br%JeL681M|5GCxm?@HZPC$Z@G9RQ|wcc{9Ey|_sn*T z%x2$UX0zmhJ*znorg4tYJ8f*ePJy@%ScRA8) literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/story_pause.tgs b/submodules/TelegramUI/Resources/Animations/story_pause.tgs new file mode 100644 index 0000000000000000000000000000000000000000..0feaa6ad0937b073b37d86e6289ed0ff3f18c75b GIT binary patch literal 2168 zcmV-;2#5C{iwFP!000021MOPdZW}ic{gq;$DT4QlKcMYH9}2W@f*;gIiQU9@ASrEw z!2jMeL+*NaMJckQv`%3JvZx_<$hjZR=2N$Ov+XXl=>F_3IxoF6o#Q=?^Ykz8ybHarmt^b z@pik9;8|JxbbY)2PJ_pG!*g%($fxd({;UdtSzHt0Zw!@8)`I=VGWO>3UXTcdH#!+DdS7K$MP_52G%5(~ zt#sB2KF(^oW?{69*@=Z#M*0wF5v{r_a$l|9uZsmp+)tA~h`MHf#wej5qG$6lIxxQhWa~k+{bA9t2<$~>j zWU$?JoIl3ULks@5&mTIuXSg_~Igs9g1J3&H5B~b?h9~&|zw9=5Z`X%15uuhDHd0MA zRH@t9L6s84PTgiQqYfN>`*v^HcC|;Unp@p&U*BzRy8FvY`eAeT;XR8|C47+RassM# zhW@wY0=ljxkta~uG@!D5GF0Ak?WaKH(?cYm1vVQ!ht2t$fz3|@%@81aOE8fK6(J1N z#Xx>Q6^%*JX4Rt#r==TdfDuDgip>%!f+&E9D%s{D9Xu{y$8a75s=aeu<&_JY- zv90!@U|S3Xlkt;hhsnB3@MN!(BTK|__7BK8z%v**XX^{hk}-jloQ#gyIx927YlXU= zV@1#2xPqQ-3P%)l;-$*$foL2z#tQi{sKy?si-G*$AdN}UW;HknerJ*_4W`GTJcLcQ zze+grkM<%$*#41s2bU+8hqMCS{((U2fjr0Hf0+G&$F5`2%K!gK@^C`S^JWmy_Imnpao%`q{Q+&;PggK|B z^EvMk78kQKn)^(sOCP?OJk8{B&*Ws2mutmyy;c}iuNCItwZcxX72jXqzFD(2R-5&0 z4VUo^W2U8rMzt34RJrv!^YQjV&N$b;`)2Qe! zoRJy<6j6=2SBu|)0j8G0q)N%2>vE8ew>A}Tf}nk7Lcd!sC{?E9g^pn1YCWzFFojm3 zp_({GR$+>@qf7!Q!A9;(tE6cUlfobxfx1-H6bz$CoXrwofQ`dUVHz?*4y~YcC97 z1{@h~siG+aZz9WzgYM~bboczfd(SCw?j=`0Hdnh7T0lvaT&)hJYBx>QKdwNWW#`Qi zaIA^VzFV5!%VTT~91qL&B#rEaL_3-17iLnA4i|mA} z`$^o#;COPV`-XQKBwD&+^>y>u^$3@^^68a73AMqDV)S=x(n?>@b=*wtTcbfVAYu^p zb7WIu&7Xd|s*8ySs4U9oK})`|`+tfJj%0U9&Dn(oD^uRr=0w#dN=I5=hJc!qpaaL% z5EU;093!`CL&eB>)SxJkdL7p{DiA7L>Mw<4N^P>VLBe6|yjINit|ge-Gf5Z}FUwWc zqfV+s_O&+E3%kQ17FJVrt#1I6rkdcS#E zfy$O56EMm-Q|(J8e-X;bQD-{9HgEDNS`?eAF-K#yh)uQFW5TLC*oh6)L8V0r(@RstB2PC4N- z;$R@amFT@QKvmQF0Xi|r03I{Cssd(*mw5t>ye1}KLTpTjJi+*sMeIw^ep^BN0xtix zMu2!8m6PK@XWPCC?Jt;mHIjjgjvDuxJUn33Jr?i|d@BaaY9jLnqFaQ9 deliverOnMainQueue).start(next: { value in + |> deliverOnMainQueue).start(next: { value in let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value)) }) } @@ -395,12 +399,20 @@ public final class AccountContextImpl: AccountContext { return (isPremium, userLimits) } } - |> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in - guard let strongSelf = self else { + |> deliverOnMainQueue).startStrict(next: { [weak self] isPremium, userLimits in + guard let self = self else { + return + } + self.isPremium = isPremium + self.userLimits = userLimits + }) + + self.peerNameColorsConfigurationDisposable = (self._appConfiguration.get() + |> deliverOnMainQueue).startStrict(next: { [weak self] appConfiguration in + guard let self = self else { return } - strongSelf.isPremium = isPremium - strongSelf.userLimits = userLimits + self.peerNameColors = PeerNameColors.with(appConfiguration: appConfiguration) }) } @@ -413,6 +425,7 @@ public final class AccountContextImpl: AccountContext { self.experimentalUISettingsDisposable?.dispose() self.animatedEmojiStickersDisposable?.dispose() self.userLimitsConfigurationDisposable?.dispose() + self.peerNameColorsConfigurationDisposable?.dispose() } public func storeSecureIdPassword(password: String) { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index cb54e2c54af..26d43dd98ec 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -58,6 +58,7 @@ import ManagedFile import DeviceProximity import MediaEditor import TelegramUIDeclareEncodables +import ContextMenuScreen #if canImport(AppCenter) import AppCenter @@ -401,16 +402,6 @@ private class UserInterfaceStyleObserverWindow: UIWindow { MobySubscriptionAnalytics.trackInstall(installInfo: installInfo) } - let appLovinAdProvider: AdProvider - if #available(iOS 13.0, *) { - appLovinAdProvider = AppLovinAdProvider( - apiKey: NGENV.applovin_api_key, - adUnitIdentifier: NGENV.applovin_ad_unit_id, - userRepository: RepoUserTgHelper.resolveUserRepository() - ) - } else { - appLovinAdProvider = AdProviderMock() - } NGEntryPoint.onAppLaunch( env: Env( apiBaseUrl: URL(string: NGENV.esim_api_url)!, @@ -423,10 +414,26 @@ private class UserInterfaceStyleObserverWindow: UIWindow { termsUrl: URL(string: NGENV.terms_url)!, webSocketUrl: NGENV.websocket_url ), - appLovinAdProvider: appLovinAdProvider, - firebaseAnalyticsSender: FirebaseAnalyticsSender(), - remoteConfig: RemoteConfigServiceImpl.shared, - lottieViewProvider: { LottieViewImpl() } + appLovinAdProvider: { + if #available(iOS 13.0, *) { + AppLovinAdProvider( + apiKey: NGENV.applovin_api_key, + adUnitIdentifier: NGENV.applovin_ad_unit_id, + userRepository: RepoUserTgHelper.resolveUserRepository() + ) + } else { + AdProviderMock() + } + }, + firebaseAnalyticsSender: { + FirebaseAnalyticsSender() + }, + remoteConfig: { + RemoteConfigServiceImpl.shared + }, + lottieView: { + LottieViewImpl() + } ) let launchStartTime = CFAbsoluteTimeGetCurrent() @@ -703,6 +710,9 @@ private class UserInterfaceStyleObserverWindow: UIWindow { }, openUrl: { url in UIApplication.shared.open(url, options: [:], completionHandler: nil) }) + setContextMenuControllerProvider { arguments in + return ContextMenuControllerImpl(arguments) + } if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self @@ -906,7 +916,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { icons.append(PresentationAppIcon(name: "Premium", imageName: "Premium", isPremium: true)) icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true)) icons.append(PresentationAppIcon(name: "PremiumTurbo", imageName: "PremiumTurbo", isPremium: true)) - + return icons } else { return [] @@ -1742,7 +1752,6 @@ private class UserInterfaceStyleObserverWindow: UIWindow { if !buildConfig.isAppStoreBuild { if value >= 2000 * 1024 * 1024 { if self.contextValue?.context.sharedContext.immediateExperimentalUISettings.crashOnMemoryPressure == true { - preconditionFailure() } } } @@ -2803,7 +2812,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { if let threadId { replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)) } - return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) |> map { messageIds -> MessageId? in if messageIds.isEmpty { return nil diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index ec7277dbc23..a1aa16eac13 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -487,7 +487,14 @@ final class AuthorizedApplicationContext { |> deliverOnMainQueue).start(completed: { controller?.dismiss() if let strongSelf = self, let botName = botName { - strongSelf.termsOfServiceProceedToBotDisposable.set((strongSelf.context.engine.peers.resolvePeerByName(name: botName, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peer in + strongSelf.termsOfServiceProceedToBotDisposable.set((strongSelf.context.engine.peers.resolvePeerByName(name: botName, ageLimit: 10) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, let peer = peer { self?.rootController.pushViewController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peer.id))) } @@ -785,7 +792,7 @@ final class AuthorizedApplicationContext { return } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil))) }) } @@ -904,7 +911,7 @@ final class AuthorizedApplicationContext { chatLocation = .peer(peer) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) } : nil, activateInput: activateInput ? .text : nil)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) } : nil, activateInput: activateInput ? .text : nil)) }) } } diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift index e5621608c8e..c009a047424 100644 --- a/submodules/TelegramUI/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -246,6 +246,12 @@ func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentati }, send: { message in let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true)) + |> mapToSignal { result -> Signal<[Message], NoError> in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> deliverOnMainQueue).startStandalone(next: { messages in if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { send(.message(message: MessageReference(message), media: file)) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift new file mode 100644 index 00000000000..c9168b7ba28 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -0,0 +1,914 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ChatPresentationInterfaceState +import ContextUI +import ChatInterfaceState +import PresentationDataUtils +import ChatMessageTextBubbleContentNode +import TextFormat +import ChatMessageItemView +import ChatMessageBubbleItemNode +import TelegramNotices + +private enum OptionsId: Hashable { + case reply + case forward + case link +} + +private func presentChatInputOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, initialId: OptionsId) { + var getContextController: (() -> ContextController?)? + + var sources: [ContextController.Source] = [] + + let replySelectionState = Promise(ChatControllerSubject.MessageOptionsInfo.SelectionState(canQuote: false, quote: nil)) + + if let source = chatReplyOptions(selfController: selfController, sourceNode: sourceNode, getContextController: { + return getContextController?() + }, selectionState: replySelectionState) { + sources.append(source) + } + + var forwardDismissedForCancel: (() -> Void)? + if let (source, dismissedForCancel) = chatForwardOptions(selfController: selfController, sourceNode: sourceNode, getContextController: { + return getContextController?() + }) { + forwardDismissedForCancel = dismissedForCancel + sources.append(source) + } + + if let source = chatLinkOptions(selfController: selfController, sourceNode: sourceNode, getContextController: { + return getContextController?() + }, replySelectionState: replySelectionState) { + sources.append(source) + } + + if sources.isEmpty { + return + } + + selfController.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + + selfController.canReadHistory.set(false) + + let contextController = ContextController( + presentationData: selfController.presentationData, + configuration: ContextController.Configuration( + sources: sources, + initialId: AnyHashable(initialId) + ) + ) + contextController.dismissed = { [weak selfController] in + selfController?.canReadHistory.set(true) + } + + getContextController = { [weak contextController] in + return contextController + } + + contextController.dismissedForCancel = { + forwardDismissedForCancel?() + } + + selfController.presentInGlobalOverlay(contextController) +} + +private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?) -> (ContextController.Source, () -> Void)? { + guard let peerId = selfController.chatLocation.peerId else { + return nil + } + guard let initialForwardMessageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds, !initialForwardMessageIds.isEmpty else { + return nil + } + let presentationData = selfController.presentationData + + let forwardOptions = selfController.presentationInterfaceStatePromise.get() + |> map { state -> ChatControllerSubject.ForwardOptions in + var hideNames = state.interfaceState.forwardOptionsState?.hideNames ?? false + if peerId.namespace == Namespaces.Peer.SecretChat { + hideNames = true + } + return ChatControllerSubject.ForwardOptions(hideNames: hideNames, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false) + } + |> distinctUntilChanged + + let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + + let messageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + let messagesCount: Signal + if let chatController = chatController as? ChatControllerImpl, messageIds.count > 1 { + messagesCount = .single(messageIds.count) + |> then( + chatController.presentationInterfaceStatePromise.get() + |> map { state -> Int in + return state.interfaceState.selectionState?.selectedIds.count ?? 1 + } + ) + } else { + messagesCount = .single(1) + } + + let accountPeerId = selfController.context.account.peerId + let items = combineLatest(forwardOptions, selfController.context.account.postbox.messagesAtIds(messageIds), messagesCount) + |> deliverOnMainQueue + |> map { [weak selfController] forwardOptions, messages, messagesCount -> [ContextMenuItem] in + guard let selfController else { + return [] + } + var items: [ContextMenuItem] = [] + + var hasCaptions = false + var uniquePeerIds = Set() + + var hasOther = false + var hasNotOwnMessages = false + for message in messages { + if let author = message.effectiveAuthor { + if !uniquePeerIds.contains(author.id) { + uniquePeerIds.insert(author.id) + } + if message.id.peerId == accountPeerId && message.forwardInfo == nil { + } else { + hasNotOwnMessages = true + } + } + + var isDice = false + var isMusic = false + for media in message.media { + if let media = media as? TelegramMediaFile, media.isMusic { + isMusic = true + } else if media is TelegramMediaDice { + isDice = true + } else { + if !message.text.isEmpty { + if media is TelegramMediaImage || media is TelegramMediaFile { + hasCaptions = true + } + } + } + } + if !isDice && !isMusic { + hasOther = true + } + } + + var canHideNames = hasNotOwnMessages && hasOther + if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + canHideNames = false + } + let hideNames = forwardOptions.hideNames + let hideCaptions = forwardOptions.hideCaptions + + if canHideNames { + items.append(.action(ContextMenuActionItem(text: hideNames ? (uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_ShowSendersName : presentationData.strings.Conversation_ForwardOptions_ShowSendersNames) : (uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames), icon: { _ in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !hideNames ? "message_preview_person_on" : "message_preview_person_off" + ), action: { [weak selfController] _, f in + selfController?.interfaceInteraction?.updateForwardOptionsState({ current in + var updated = current + if hideNames { + updated.hideNames = false + updated.hideCaptions = false + updated.unhideNamesOnCaptionChange = false + } else { + updated.hideNames = true + updated.unhideNamesOnCaptionChange = false + } + return updated + }) + }))) + } + + if hasCaptions { + items.append(.action(ContextMenuActionItem(text: hideCaptions ? presentationData.strings.Conversation_ForwardOptions_ShowCaption : presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { _ in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !hideCaptions ? "message_preview_caption_off" : "message_preview_caption_on" + ), action: { [weak selfController] _, f in + selfController?.interfaceInteraction?.updateForwardOptionsState({ current in + var updated = current + if hideCaptions { + updated.hideCaptions = false + if canHideNames { + if updated.unhideNamesOnCaptionChange { + updated.unhideNamesOnCaptionChange = false + updated.hideNames = false + } + } + } else { + updated.hideCaptions = true + if canHideNames { + if !updated.hideNames { + updated.hideNames = true + updated.unhideNamesOnCaptionChange = true + } + } + } + return updated + }) + }))) + } + + if !items.isEmpty { + items.append(.separator) + } + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ChangeRecipient, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in + selfController?.interfaceInteraction?.forwardCurrentForwardMessages() + + f(.default) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptionsCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController] c, f in + f(.default) + + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(nil).withoutSelectionState() }) }) + }))) + + return items + } + + let dismissedForCancel: () -> Void = { [weak selfController, weak chatController] in + guard let selfController else { + return + } + if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds { + var forwardMessageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) } + selfController.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) }) + } + } + + return (ContextController.Source( + id: AnyHashable(OptionsId.forward), + title: selfController.presentationData.strings.Conversation_MessageOptionsTabForward, + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + items: items |> map { ContextController.Items(id: AnyHashable("forward"), content: .list($0)) } + ), dismissedForCancel) +} + +func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) { + presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .forward) +} + +private func generateChatReplyOptionItems(selfController: ChatControllerImpl, chatController: ChatControllerImpl) -> Signal { + guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else { + return .complete() + } + + let applyCurrentQuoteSelection: () -> Void = { [weak selfController, weak chatController] in + guard let selfController, let chatController else { + return + } + var messageItemNode: ChatMessageItemView? + chatController.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == replySubject.messageId { + messageItemNode = itemNode + } + return true + } + var targetContentNode: ChatMessageTextBubbleContentNode? + if let messageItemNode = messageItemNode as? ChatMessageBubbleItemNode { + for contentNode in messageItemNode.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + targetContentNode = contentNode + break + } + } + } + guard let contentNode = targetContentNode else { + return + } + guard let textSelection = contentNode.getCurrentTextSelection() else { + return + } + var quote: EngineMessageReplyQuote? + let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 }))) + if !trimmedText.string.isEmpty { + quote = EngineMessageReplyQuote(text: trimmedText.string, entities: trimmedText.entities, media: nil) + } + + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote)).withoutSelectionState() }) }) + } + + let items = combineLatest(queue: .mainQueue(), + selfController.context.account.postbox.messagesAtIds([replySubject.messageId]), + ApplicationSpecificNotice.getReplyQuoteTextSelectionTips(accountManager: selfController.context.sharedContext.accountManager) + ) + |> deliverOnMainQueue + |> map { [weak selfController, weak chatController] messages, quoteTextSelectionTips -> ContextController.Items in + guard let selfController, let chatController else { + return ContextController.Items(content: .list([])) + } + + var items: [ContextMenuItem] = [] + + if replySubject.quote != nil { + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteSelectedPart, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteSelected"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + applyCurrentQuoteSelection() + + f(.default) + }))) + } else if let message = messages.first, !message.text.isEmpty { + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteSelect, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Quote"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController, weak chatController] c, _ in + guard let selfController, let chatController else { + return + } + var messageItemNode: ChatMessageItemView? + chatController.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == replySubject.messageId { + messageItemNode = itemNode + } + return true + } + if let messageItemNode = messageItemNode as? ChatMessageBubbleItemNode { + for contentNode in messageItemNode.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + contentNode.beginTextSelection(range: nil) + + var subItems: [ContextMenuItem] = [] + + subItems.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c, _ in + c.popItems() + }))) + subItems.append(.separator) + + subItems.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteSelectedPart, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteSelected"), color: theme.contextMenu.primaryColor) + }, action: { [weak selfController, weak contentNode] _, f in + guard let selfController, let contentNode else { + return + } + guard let textSelection = contentNode.getCurrentTextSelection() else { + return + } + + var quote: EngineMessageReplyQuote? + let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 }))) + if !trimmedText.string.isEmpty { + quote = EngineMessageReplyQuote(text: trimmedText.string, entities: trimmedText.entities, media: nil) + } + + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote)).withoutSelectionState() }) }) + + f(.default) + }))) + + c.pushItems(items: .single(ContextController.Items(content: .list(subItems), dismissed: { [weak contentNode] in + guard let contentNode else { + return + } + contentNode.cancelTextSelection() + }))) + + break + } + } + } + }))) + } + + var canReplyInAnotherChat = true + + if let message = messages.first { + if selfController.presentationInterfaceState.copyProtectionEnabled { + canReplyInAnotherChat = false + } + + var isAction = false + for media in message.media { + if media is TelegramMediaAction || media is TelegramMediaExpiredContent { + isAction = true + } else if let story = media as? TelegramMediaStory { + if story.isMention { + isAction = true + } + } + } + + if isAction { + canReplyInAnotherChat = false + } + if message.isCopyProtected() { + canReplyInAnotherChat = false + } + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { + canReplyInAnotherChat = false + } + + } + + if canReplyInAnotherChat { + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsReplyInAnotherChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in + applyCurrentQuoteSelection() + + f(.default) + + guard let selfController else { + return + } + guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else { + return + } + moveReplyMessageToAnotherChat(selfController: selfController, replySubject: replySubject) + }))) + } + + if !items.isEmpty { + items.append(.separator) + + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in + applyCurrentQuoteSelection() + + f(.default) + }))) + } + + if replySubject.quote != nil { + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteRemove, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteRemove"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController] c, f in + f(.default) + + guard let selfController else { + return + } + var replySubject = replySubject + replySubject.quote = nil + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) }) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsReplyCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController] c, f in + f(.default) + + guard let selfController else { + return + } + var replySubject = replySubject + replySubject.quote = nil + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withoutSelectionState() }).updatedSearch(nil) }) + }))) + } + + var tip: ContextController.Tip? + if quoteTextSelectionTips <= 3, let message = messages.first, !message.text.isEmpty { + tip = .quoteSelection + } + + return ContextController.Items(id: AnyHashable("reply"), content: .list(items), tip: tip) + } + + let _ = ApplicationSpecificNotice.incrementReplyQuoteTextSelectionTips(accountManager: selfController.context.sharedContext.accountManager).startStandalone() + + return items +} + +private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, selectionState: Promise) -> ContextController.Source? { + guard let peerId = selfController.chatLocation.peerId else { + return nil + } + guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else { + return nil + } + + var replyQuote: ChatControllerSubject.MessageOptionsInfo.Quote? + if let quote = replySubject.quote { + replyQuote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: replySubject.messageId, text: quote.text) + } + selectionState.set(selfController.context.account.postbox.messagesAtIds([replySubject.messageId]) + |> map { messages -> ChatControllerSubject.MessageOptionsInfo.SelectionState in + var canQuote = false + if let message = messages.first, !message.text.isEmpty { + canQuote = true + } + return ChatControllerSubject.MessageOptionsInfo.SelectionState( + canQuote: canQuote, + quote: replyQuote + ) + } + |> distinctUntilChanged) + + guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else { + return nil + } + chatController.canReadHistory.set(false) + + let items = generateChatReplyOptionItems(selfController: selfController, chatController: chatController) + + chatController.performTextSelectionAction = { [weak selfController] message, canCopy, text, action in + guard let selfController, let contextController = getContextController() else { + return + } + + contextController.dismiss() + + selfController.controllerInteraction?.performTextSelectionAction(message, canCopy, text, action) + } + + return ContextController.Source( + id: AnyHashable(OptionsId.reply), + title: selfController.presentationData.strings.Conversation_MessageOptionsTabReply, + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + items: items + ) +} + +func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) { + presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .reply) +} + +func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) { + let _ = selfController.presentVoiceMessageDiscardAlert(action: { [weak selfController] in + guard let selfController else { + return + } + let filter: ChatListNodePeersFilter = [.onlyWriteable, .includeSavedMessages, .excludeDisabled, .doNotSearchMessages] + var attemptSelectionImpl: ((EnginePeer) -> Void)? + let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams( + context: selfController.context, + updatedPresentationData: selfController.updatedPresentationData, + filter: filter, + hasFilters: true, + title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle, + attemptSelection: { peer, _ in + attemptSelectionImpl?(peer) + }, + multipleSelection: false, + forwardedMessageIds: [], + selectForumThreads: true + )) + let context = selfController.context + attemptSelectionImpl = { [weak selfController, weak controller] peer in + guard let selfController, let controller = controller else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + controller.peerSelected = { [weak selfController, weak controller] peer, threadId in + guard let selfController, let strongController = controller else { + return + } + let peerId = peer.id + //let accountPeerId = selfController.context.account.peerId + + var isPinnedMessages = false + if case .pinnedMessages = selfController.presentationInterfaceState.subject { + isPinnedMessages = true + } + + if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages { + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) }) + selfController.updateItemNodesSearchTextHighlightStates() + selfController.searchResultsController = nil + strongController.dismiss() + } else { + if let navigationController = selfController.navigationController as? NavigationController { + for controller in navigationController.viewControllers { + if let maybeChat = controller as? ChatControllerImpl { + if case .peer(peerId) = maybeChat.chatLocation { + var isChatPinnedMessages = false + if case .pinnedMessages = maybeChat.presentationInterfaceState.subject { + isChatPinnedMessages = true + } + if !isChatPinnedMessages { + maybeChat.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }) }) + selfController.dismiss() + strongController.dismiss() + return + } + } + } + } + } + + let _ = (ChatInterfaceState.update(engine: selfController.context.engine, peerId: peerId, threadId: threadId, { currentState in + return currentState.withUpdatedReplyMessageSubject(replySubject) + }) + |> deliverOnMainQueue).startStandalone(completed: { [weak selfController] in + guard let selfController else { + return + } + let proceed: (ChatController) -> Void = { [weak selfController] chatController in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withoutSelectionState() }) }) + + let navigationController: NavigationController? + if let parentController = selfController.parentController { + navigationController = (parentController.navigationController as? NavigationController) + } else { + navigationController = selfController.effectiveNavigationController + } + + if let navigationController = navigationController { + var viewControllers = navigationController.viewControllers + if threadId != nil { + viewControllers.insert(chatController, at: viewControllers.count - 2) + } else { + viewControllers.insert(chatController, at: viewControllers.count - 1) + } + navigationController.setViewControllers(viewControllers, animated: false) + + selfController.controllerNavigationDisposable.set((chatController.ready.get() + |> SwiftSignalKit.filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak navigationController] _ in + viewControllers.removeAll(where: { $0 is PeerSelectionController }) + navigationController?.setViewControllers(viewControllers, animated: true) + })) + } + } + if let threadId = threadId { + let _ = (selfController.context.sharedContext.chatControllerForForumThread(context: selfController.context, peerId: peerId, threadId: threadId) + |> deliverOnMainQueue).startStandalone(next: { chatController in + proceed(chatController) + }) + } else { + let chatController = ChatControllerImpl(context: selfController.context, chatLocation: .peer(id: peerId)) + chatController.activateInput(type: .text) + proceed(chatController) + } + }) + } + } + selfController.chatDisplayNode.dismissInput() + selfController.effectiveNavigationController?.pushViewController(controller) + }) +} + +private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, replySelectionState: Promise) -> ContextController.Source? { + guard let peerId = selfController.chatLocation.peerId else { + return nil + } + + let initialUrlPreview: ChatPresentationInterfaceState.UrlPreview? + if selfController.presentationInterfaceState.interfaceState.editMessage != nil { + initialUrlPreview = selfController.presentationInterfaceState.editingUrlPreview + } else { + initialUrlPreview = selfController.presentationInterfaceState.urlPreview + } + + guard let initialUrlPreview else { + return nil + } + + let linkOptions = combineLatest(queue: .mainQueue(), + selfController.presentationInterfaceStatePromise.get(), + replySelectionState.get() + ) + |> map { state, replySelectionState -> ChatControllerSubject.LinkOptions in + let urlPreview: ChatPresentationInterfaceState.UrlPreview + if state.interfaceState.editMessage != nil { + urlPreview = state.editingUrlPreview ?? initialUrlPreview + } else { + urlPreview = state.urlPreview ?? initialUrlPreview + } + + var webpageHasLargeMedia = false + if case let .Loaded(content) = urlPreview.webPage.content { + if let isMediaLargeByDefault = content.isMediaLargeByDefault { + if isMediaLargeByDefault { + webpageHasLargeMedia = true + } + } else { + webpageHasLargeMedia = true + } + } + + let composeInputText: NSAttributedString = state.interfaceState.effectiveInputState.inputText + + var replyMessageId: EngineMessage.Id? + var replyQuote: String? + + if state.interfaceState.editMessage == nil { + replyMessageId = state.interfaceState.replyMessageSubject?.messageId + replyQuote = replySelectionState.quote?.text + } + + let inputText = chatInputStateStringWithAppliedEntities(composeInputText.string, entities: generateChatInputTextEntities(composeInputText, generateLinks: false)) + + var largeMedia = false + if webpageHasLargeMedia { + largeMedia = urlPreview.largeMedia ?? true + } else { + largeMedia = false + } + + return ChatControllerSubject.LinkOptions( + messageText: composeInputText.string, + messageEntities: generateChatInputTextEntities(composeInputText, generateLinks: true), + hasAlternativeLinks: detectUrls(inputText).count > 1, + replyMessageId: replyMessageId, + replyQuote: replyQuote, + url: urlPreview.url, + webpage: urlPreview.webPage, + linkBelowText: urlPreview.positionBelowText, + largeMedia: largeMedia + ) + } + |> distinctUntilChanged + + guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions))), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else { + return nil + } + chatController.canReadHistory.set(false) + + let items = linkOptions + |> deliverOnMainQueue + |> map { [weak selfController] linkOptions -> ContextController.Items in + guard let selfController else { + return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list([])) + } + var items: [ContextMenuItem] = [] + + do { + items.append(.action(ContextMenuActionItem(text: linkOptions.linkBelowText ? selfController.presentationData.strings.Conversation_MessageOptionsLinkMoveUp : selfController.presentationData.strings.Conversation_MessageOptionsLinkMoveDown, icon: { theme in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: linkOptions.linkBelowText ? "message_preview_sort_above" : "message_preview_sort_below" + ), action: { [weak selfController] _, f in + selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + if state.interfaceState.editMessage != nil { + guard var urlPreview = state.editingUrlPreview else { + return state + } + urlPreview.positionBelowText = !urlPreview.positionBelowText + return state.updatedEditingUrlPreview(urlPreview) + } else { + guard var urlPreview = state.urlPreview else { + return state + } + urlPreview.positionBelowText = !urlPreview.positionBelowText + return state.updatedUrlPreview(urlPreview) + } + }) + }))) + } + + if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault { + let shrinkTitle: String + let enlargeTitle: String + if let file = content.file, file.isVideo { + shrinkTitle = selfController.presentationData.strings.Conversation_MessageOptionsShrinkVideo + enlargeTitle = selfController.presentationData.strings.Conversation_MessageOptionsEnlargeVideo + } else { + shrinkTitle = selfController.presentationData.strings.Conversation_MessageOptionsShrinkImage + enlargeTitle = selfController.presentationData.strings.Conversation_MessageOptionsEnlargeImage + } + + items.append(.action(ContextMenuActionItem(text: linkOptions.largeMedia ? shrinkTitle : enlargeTitle, icon: { _ in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !linkOptions.largeMedia ? "message_preview_media_large" : "message_preview_media_small" + ), action: { [weak selfController] _, f in + selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + if state.interfaceState.editMessage != nil { + guard var urlPreview = state.editingUrlPreview else { + return state + } + if let largeMedia = urlPreview.largeMedia { + urlPreview.largeMedia = !largeMedia + } else { + urlPreview.largeMedia = false + } + return state.updatedEditingUrlPreview(urlPreview) + } else { + guard var urlPreview = state.urlPreview else { + return state + } + if let largeMedia = urlPreview.largeMedia { + urlPreview.largeMedia = !largeMedia + } else { + urlPreview.largeMedia = false + } + return state.updatedUrlPreview(urlPreview) + } + }) + }))) + } + + if !items.isEmpty { + items.append(.separator) + } + + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + }))) + + items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_LinkOptionsCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController, weak chatController] c, f in + guard let selfController else { + return + } + + selfController.chatDisplayNode.dismissUrlPreview() + + let _ = chatController + + f(.default) + }))) + + return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items)) + } + + var webpageCache: [String: TelegramMediaWebpage] = [:] + chatController.performOpenURL = { [weak selfController] message, url, progress in + guard let selfController else { + return + } + + if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewState { + if let webpage = webpageCache[updatedUrlPreviewState.detectedUrl] { + progress?.set(.single(false)) + + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in + if state.interfaceState.editMessage != nil { + if var urlPreview = state.editingUrlPreview { + urlPreview.url = updatedUrlPreviewState.detectedUrl + urlPreview.webPage = webpage + + return state.updatedEditingUrlPreview(urlPreview) + } else { + return state + } + } else { + if var urlPreview = state.urlPreview { + urlPreview.url = updatedUrlPreviewState.detectedUrl + urlPreview.webPage = webpage + + return state.updatedUrlPreview(urlPreview) + } else { + return state + } + } + }) + } else { + progress?.set(.single(true)) + let _ = (signal + |> afterDisposed { + progress?.set(.single(false)) + } + |> deliverOnMainQueue).start(next: { [weak selfController] result in + guard let selfController else { + return + } + + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in + if state.interfaceState.editMessage != nil { + if let webpage = result(nil), var urlPreview = state.editingUrlPreview { + urlPreview.url = updatedUrlPreviewState.detectedUrl + urlPreview.webPage = webpage + webpageCache[updatedUrlPreviewState.detectedUrl] = webpage + + return state.updatedEditingUrlPreview(urlPreview) + } else { + return state + } + } else { + if let webpage = result(nil), var urlPreview = state.urlPreview { + urlPreview.url = updatedUrlPreviewState.detectedUrl + urlPreview.webPage = webpage + webpageCache[updatedUrlPreviewState.detectedUrl] = webpage + + return state.updatedUrlPreview(urlPreview) + } else { + return state + } + } + }) + }) + } + } + } + + return ContextController.Source( + id: AnyHashable(OptionsId.link), + title: selfController.presentationData.strings.Conversation_MessageOptionsTabLink, + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + items: items + ) +} + +func presentChatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) { + presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .link) +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift new file mode 100644 index 00000000000..d5ca6f582d6 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -0,0 +1,130 @@ +// MARK: Nicegram +import NGTranslate +import NGUI +// +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramNotices +import ChatSendMessageActionUI +import AccountContext + +func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, node: ASDisplayNode, gesture: ContextGesture) { + guard let peerId = selfController.chatLocation.peerId, let textInputView = selfController.chatDisplayNode.textInputView(), let layout = selfController.validLayout else { + return + } + let previousSupportedOrientations = selfController.supportedOrientations + if layout.size.width > layout.size.height { + selfController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .landscape) + } else { + selfController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + + let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: selfController.context.sharedContext.accountManager, count: 4).startStandalone() + + var hasEntityKeyboard = false + if case .media = selfController.presentationInterfaceState.inputMode { + hasEntityKeyboard = true + } + + let _ = (selfController.context.account.viewTracker.peerView(peerId) + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView in + guard let selfController, let peer = peerViewMainPeer(peerView) else { + return + } + var sendWhenOnlineAvailable = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime > until { + sendWhenOnlineAvailable = true + } + } + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + sendWhenOnlineAvailable = false + } + + if sendWhenOnlineAvailable { + let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: selfController.context.sharedContext.accountManager, count: 4).startStandalone() + } + + 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 + } + selfController.supportedOrientations = previousSupportedOrientations + }, sendMessage: { [weak selfController] mode in + guard let selfController else { + return + } + switch mode { + case .generic: + selfController.controllerInteraction?.sendCurrentMessage(false) + case .silently: + selfController.controllerInteraction?.sendCurrentMessage(true) + case .whenOnline: + selfController.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp) { [weak selfController] in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: selfController.presentationInterfaceState.subject != .scheduledMessages, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + }) + selfController.openScheduledMessages() + } + } + }, /* MARK: Nicegram TranslateEnteredMessage (translate + chooseLanguage) */ translate: { [weak selfController] in + guard let selfController else { return } + let chatId = selfController.chatLocation.peerId + let textToTranslate = selfController.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string + let _ = (translateEnteredText(text: textToTranslate, chatId: chatId, context: selfController.context) + |> deliverOnMainQueue).start( + next: { translated in + selfController.updateChatPresentationInterfaceState(interactive: true, { state in + let newTextInputState = ChatTextInputState(inputText: NSAttributedString(string: translated)) + return state.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEffectiveInputState(newTextInputState) + } + }) + }, error: { error in + let errorDescription: String + switch error { + case .toLanguageNotFound: + errorDescription = "Messages.TranslateError.ToLanguageNotFound" + case .translate: + errorDescription = "Messages.TranslateError" + } + let c = getIAPErrorController(context: selfController.context, errorDescription, selfController.presentationData) + selfController.controllerInteraction?.presentGlobalOverlayController(c, nil) + }) + }, chooseLanguage: { [weak selfController] in + guard let selfController else { return } + let chatId = selfController.chatLocation.peerId + let _ = (getLanguageCode(forChatWith: chatId, context: selfController.context) + |> deliverOnMainQueue).start(next: { code in + let c = languageListController(context: selfController.context, selectedLanguageCode: code, selectLanguage: { code in + setLanguageCode(code, forChatWith: chatId) + }) + c.navigationPresentation = .modal + selfController.push(c) + }) + }, schedule: { [weak selfController] in + guard let selfController else { + return + } + selfController.controllerInteraction?.scheduleCurrentMessage() + }) + controller.emojiViewProvider = selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider + selfController.sendMessageActionsController = controller + if layout.isNonExclusive { + selfController.present(controller, in: .window(.root)) + } else { + selfController.presentInGlobalOverlay(controller, with: nil) + } + }) +} diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift new file mode 100644 index 00000000000..245f78c609c --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -0,0 +1,516 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ChatPresentationInterfaceState +import ChatInterfaceState +import TelegramNotices +import PresentationDataUtils +import TelegramCallsUI +import AttachmentUI + +func updateChatPresentationInterfaceStateImpl( + selfController: ChatControllerImpl, + transition: ContainedViewLayoutTransition, + interactive: Bool, + saveInterfaceState: Bool, + _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, + completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void +) { + var completion = externalCompletion + var temporaryChatPresentationInterfaceState = f(selfController.presentationInterfaceState) + + if selfController.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup || selfController.presentationInterfaceState.keyboardButtonsMessage?.id != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.id { + if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let keyboardMarkup = keyboardButtonsMessage.visibleButtonKeyboardMarkup { + if selfController.presentationInterfaceState.interfaceState.editMessage == nil && selfController.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in + return .inputButtons(persistent: keyboardMarkup.flags.contains(.persistent)) + }) + } + + if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { + if temporaryChatPresentationInterfaceState.interfaceState.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ + $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: keyboardButtonsMessage.id, + quote: nil + )).withUpdatedMessageActionsState({ value in + var value = value + value.processedSetupReplyMessageId = keyboardButtonsMessage.id + return value + }) }) + } + } + } else { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ mode in + if case .inputButtons = mode { + return .text + } else { + return mode + } + }) + } + } + + if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, keyboardButtonsMessage.requestsSetupReply { + if temporaryChatPresentationInterfaceState.interfaceState.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: keyboardButtonsMessage.id, + quote: nil + )).withUpdatedMessageActionsState({ value in + var value = value + value.processedSetupReplyMessageId = keyboardButtonsMessage.id + return value + }) }) + } + } + + let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, context: selfController.context) + var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) + + let contextQueryUpdates = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: selfController.context, currentQueryStates: &selfController.contextQueryStates, requestBotLocationStatus: { [weak selfController] peerId in + guard let selfController else { + return + } + let _ = (ApplicationSpecificNotice.updateInlineBotLocationRequestState(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970 + 10 * 60)) + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] value in + guard let selfController, value else { + return + } + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in + guard let selfController else { + return + } + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() + })]), in: .window(.root)) + }) + }) + + for (kind, update) in contextQueryUpdates { + switch update { + case .remove: + if let (_, disposable) = selfController.contextQueryStates[kind] { + disposable.dispose() + selfController.contextQueryStates.removeValue(forKey: kind) + + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { _ in + return nil + }) + } + if case .contextRequest = kind { + selfController.performingInlineSearch.set(false) + } + case let .update(query, signal): + let currentQueryAndDisposable = selfController.contextQueryStates[kind] + currentQueryAndDisposable?.1.dispose() + + var inScope = true + var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? + selfController.contextQueryStates[kind] = (query, (signal + |> deliverOnMainQueue).startStrict(next: { [weak selfController] result in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInputQueryResult(queryKind: kind, { previousResult in + return result(previousResult) + }) + }) + } + }, error: { [weak selfController] error in + guard let selfController else { + return + } + if case .contextRequest = kind { + selfController.performingInlineSearch.set(false) + } + + switch error { + case .generic: + break + case let .inlineBotLocationRequest(peerId): + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in + guard let selfController else { + return + } + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).startStandalone() + }), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in + guard let selfController else { + return + } + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() + })]), in: .window(.root)) + } + }, completed: { [weak selfController] in + guard let selfController else { + return + } + if case .contextRequest = kind { + selfController.performingInlineSearch.set(false) + } + })) + inScope = false + if let inScopeResult = inScopeResult { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in + return inScopeResult(previousResult) + }) + } else { + if case .contextRequest = kind { + selfController.performingInlineSearch.set(true) + } + } + + if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + if case .contextRequest = query { + let _ = (ApplicationSpecificNotice.getSecretChatInlineBotUsage(accountManager: selfController.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] value in + guard let selfController, !value else { + return + } + let _ = ApplicationSpecificNotice.setSecretChatInlineBotUsage(accountManager: selfController.context.sharedContext.accountManager).startStandalone() + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_SecretChatContextBotAlert, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + } + } + } + } + + var isBot = false + if let peer = updatedChatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil { + isBot = true + } else { + isBot = false + } + selfController.chatDisplayNode.historyNode.chatHasBots = updatedChatPresentationInterfaceState.hasBots || isBot + + if let (updatedSearchQuerySuggestionState, updatedSearchQuerySuggestionSignal) = searchQuerySuggestionResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: selfController.context, currentQuery: selfController.searchQuerySuggestionState?.0) { + selfController.searchQuerySuggestionState?.1.dispose() + var inScope = true + var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? + selfController.searchQuerySuggestionState = (updatedSearchQuerySuggestionState, (updatedSearchQuerySuggestionSignal |> deliverOnMainQueue).startStrict(next: { [weak selfController] result in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedSearchQuerySuggestionResult { previousResult in + return result(previousResult) + } + }) + } + })) + inScope = false + if let inScopeResult = inScopeResult { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedSearchQuerySuggestionResult { previousResult in + return inScopeResult(previousResult) + } + } + } + + if let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0) { + selfController.urlPreviewQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? + let linkPreviews: Signal + if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + linkPreviews = interactiveChatLinkPreviewsEnabled(accountManager: selfController.context.sharedContext.accountManager, displayAlert: { [weak selfController] f in + guard let selfController else { + return + } + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_SecretLinkPreviewAlert, actions: [ + TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Yes, action: { + f.f(true) + }), TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_No, action: { + f.f(false) + })]), in: .window(.root)) + }) + } else { + var bannedEmbedLinks = false + if let channel = selfController.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banEmbedLinks) != nil { + bannedEmbedLinks = true + } else if let group = selfController.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banEmbedLinks) { + bannedEmbedLinks = true + } + if bannedEmbedLinks { + linkPreviews = .single(false) + } else { + linkPreviews = .single(true) + } + } + let filteredPreviewSignal = linkPreviews + |> take(1) + |> mapToSignal { value -> Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError> in + if value { + return updatedUrlPreviewSignal + } else { + return .single({ _ in return nil }) + } + } + + selfController.urlPreviewQueryState = (updatedUrlPreviewState, (filteredPreviewSignal |> deliverOnMainQueue).startStrict(next: { [weak selfController] (result) in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { + if let updatedUrlPreviewState, let webpage = result($0.urlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedUrlPreviewState.detectedUrl, + webPage: webpage, + positionBelowText: $0.urlPreview?.positionBelowText ?? true, + largeMedia: $0.urlPreview?.largeMedia + ) + return $0.updatedUrlPreview(updatedPreview) + } else { + return $0.updatedUrlPreview(nil) + } + }) + } + })) + inScope = false + if let inScopeResult = inScopeResult { + if let updatedUrlPreviewState, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedUrlPreviewState.detectedUrl, + webPage: webpage, + positionBelowText: updatedChatPresentationInterfaceState.urlPreview?.positionBelowText ?? true, + largeMedia: updatedChatPresentationInterfaceState.urlPreview?.largeMedia + ) + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(updatedPreview) + } else { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil) + } + } + } + + 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) { + selfController.editingUrlPreviewQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? + selfController.editingUrlPreviewQueryState = (updatedEditingUrlPreviewState, (updatedEditingUrlPreviewSignal |> deliverOnMainQueue).startStrict(next: { [weak selfController] result in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { + if let updatedEditingUrlPreviewState, let webpage = result($0.editingUrlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedEditingUrlPreviewState.detectedUrl, + webPage: webpage, + positionBelowText: $0.editingUrlPreview?.positionBelowText ?? true, + largeMedia: $0.editingUrlPreview?.largeMedia + ) + return $0.updatedEditingUrlPreview(updatedPreview) + } else { + return $0.updatedEditingUrlPreview(nil) + } + }) + } + })) + inScope = false + if let inScopeResult = inScopeResult { + if let updatedEditingUrlPreviewState, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedEditingUrlPreviewState.detectedUrl, + webPage: webpage, + positionBelowText: updatedChatPresentationInterfaceState.editingUrlPreview?.positionBelowText ?? true, + largeMedia: updatedChatPresentationInterfaceState.editingUrlPreview?.largeMedia + ) + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(updatedPreview) + } else { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil) + } + } + } + + if let replyMessageId = updatedChatPresentationInterfaceState.interfaceState.replyMessageSubject?.messageId { + if selfController.replyMessageState?.0 != replyMessageId { + selfController.replyMessageState?.1.dispose() + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedReplyMessage(nil) + let disposable = MetaDisposable() + selfController.replyMessageState = (replyMessageId, disposable) + disposable.set((selfController.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: replyMessageId)) + |> deliverOnMainQueue).start(next: { [weak selfController] message in + guard let selfController else { + return + } + if message != selfController.presentationInterfaceState.replyMessage.flatMap(EngineMessage.init) { + selfController.updateChatPresentationInterfaceState(interactive: false, { presentationInterfaceState in + return presentationInterfaceState.updatedReplyMessage(message?._asMessage()) + }) + } + })) + } + } else { + if let replyMessageState = selfController.replyMessageState { + selfController.replyMessageState = nil + replyMessageState.1.dispose() + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedReplyMessage(nil) + } + } + + if let updated = selfController.updateSearch(updatedChatPresentationInterfaceState) { + updatedChatPresentationInterfaceState = updated + } + + let recordingActivityValue: ChatRecordingActivity + if let mediaRecordingState = updatedChatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { + switch mediaRecordingState { + case .audio: + recordingActivityValue = .voice + case .video(ChatVideoRecordingStatus.recording, _): + recordingActivityValue = .instantVideo + default: + recordingActivityValue = .none + } + } else { + recordingActivityValue = .none + } + if recordingActivityValue != selfController.recordingActivityValue { + selfController.recordingActivityValue = recordingActivityValue + selfController.recordingActivityPromise.set(recordingActivityValue) + } + + if (selfController.presentationInterfaceState.interfaceState.selectionState == nil) != (updatedChatPresentationInterfaceState.interfaceState.selectionState == nil) { + selfController.isSelectingMessagesUpdated?(updatedChatPresentationInterfaceState.interfaceState.selectionState != nil) + selfController.updateNextChannelToReadVisibility() + } + + selfController.presentationInterfaceState = updatedChatPresentationInterfaceState + + selfController.updateSlowmodeStatus() + + switch updatedChatPresentationInterfaceState.inputMode { + case .media: + break + default: + selfController.chatDisplayNode.collapseInput() + } + + if selfController.isNodeLoaded { + selfController.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, transition: transition, interactive: interactive, completion: completion) + } else { + completion(.immediate) + } + + let updatedServiceTasks = serviceTasksForChatPresentationIntefaceState(context: selfController.context, chatPresentationInterfaceState: updatedChatPresentationInterfaceState, updateState: { [weak selfController] f in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(animated: false, interactive: false, f) + + //selfController.chatDisplayNode.updateChatPresentationInterfaceState(f(selfController.chatDisplayNode.chatPresentationInterfaceState), transition: transition, interactive: false, completion: { _ in }) + }) + for (id, begin) in updatedServiceTasks { + if selfController.stateServiceTasks[id] == nil { + selfController.stateServiceTasks[id] = begin() + } + } + var removedServiceTaskIds: [AnyHashable] = [] + for (id, _) in selfController.stateServiceTasks { + if updatedServiceTasks[id] == nil { + removedServiceTaskIds.append(id) + } + } + for id in removedServiceTaskIds { + selfController.stateServiceTasks.removeValue(forKey: id)?.dispose() + } + + if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, subject: selfController.subject, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.leftNavigationButton, target: selfController, selector: #selector(selfController.leftNavigationButtonAction)) { + if selfController.leftNavigationButton != button { + var animated = transition.isAnimated + if let currentButton = selfController.leftNavigationButton?.action, currentButton == button.action { + animated = false + } + animated = false + selfController.navigationItem.setLeftBarButton(button.buttonItem, animated: animated) + selfController.leftNavigationButton = button + } + } else if let _ = selfController.leftNavigationButton { + selfController.navigationItem.setLeftBarButton(nil, animated: transition.isAnimated) + 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 selfController.rightNavigationButton != button { + var animated = transition.isAnimated + if let currentButton = selfController.rightNavigationButton?.action, currentButton == button.action { + animated = false + } + if case .replyThread = selfController.chatLocation { + animated = false + } + selfController.navigationItem.setRightBarButton(button.buttonItem, animated: animated) + selfController.rightNavigationButton = button + } + } else if let _ = selfController.rightNavigationButton { + selfController.navigationItem.setRightBarButton(nil, animated: transition.isAnimated) + selfController.rightNavigationButton = nil + } + + if let controllerInteraction = selfController.controllerInteraction { + if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState { + controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState + let isBlackout = controllerInteraction.selectionState != nil + let previousCompletion = completion + completion = { [weak selfController] transition in + previousCompletion(transition) + + guard let selfController else { + return + } + (selfController.navigationController as? NavigationController)?.updateMasterDetailsBlackout(isBlackout ? .master : nil, transition: transition) + } + selfController.updateItemNodesSelectionStates(animated: transition.isAnimated) + } + } + + if saveInterfaceState { + selfController.saveInterfaceState(includeScrollState: false) + } + + if let navigationController = selfController.navigationController as? NavigationController, isTopmostChatController(selfController) { + var voiceChatOverlayController: VoiceChatOverlayController? + for controller in navigationController.globalOverlayControllers { + if let controller = controller as? VoiceChatOverlayController { + voiceChatOverlayController = controller + break + } + } + + if let controller = voiceChatOverlayController { + controller.updateVisibility() + } + } + + if let currentMenuWebAppController = selfController.currentMenuWebAppController, !selfController.presentationInterfaceState.showWebView { + selfController.currentMenuWebAppController = nil + if let currentMenuWebAppController = currentMenuWebAppController as? AttachmentController { + currentMenuWebAppController.ensureUnfocused = false + } + currentMenuWebAppController.dismiss(animated: true, completion: nil) + } + + selfController.presentationInterfaceStatePromise.set(selfController.presentationInterfaceState) +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c01ccd29d51..e2355fb4ab7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -90,6 +90,7 @@ import ChatSendMessageActionUI import ChatTextLinkEditUI import WebUI import PremiumUI +import PremiumGiftAttachmentScreen import ImageTransparency import StickerPackPreviewUI import TextNodeWithEntities @@ -116,10 +117,18 @@ import ChatContextQuery import PeerReportScreen import PeerSelectionController import SaveToCameraRoll - -#if DEBUG -import os.signpost -#endif +import ChatMessageDateAndStatusNode +import ReplyAccessoryPanelNode +import TextSelectionNode +import ChatMessagePollBubbleContentNode +import ChatMessageItem +import ChatMessageItemImpl +import ChatMessageItemView +import ChatMessageItemCommon +import ChatMessageAnimatedStickerItemNode +import ChatMessageBubbleItemNode +import ChatNavigationButton +import WebsiteType public enum ChatControllerPeekActions { case standard @@ -133,20 +142,20 @@ public final class ChatControllerOverlayPresentationData { } } -private enum ChatLocationInfoData { +enum ChatLocationInfoData { case peer(Promise) case replyThread(Promise) case feed } -private enum ChatRecordingActivity { +enum ChatRecordingActivity { case voice case instantVideo case none } public enum NavigateToMessageLocation { - case id(MessageId, Double?) + case id(MessageId, NavigateToMessageParams) case index(MessageIndex) case upperBound(PeerId) @@ -173,7 +182,7 @@ public enum NavigateToMessageLocation { } } -private func isTopmostChatController(_ controller: ChatControllerImpl) -> Bool { +func isTopmostChatController(_ controller: ChatControllerImpl) -> Bool { if let _ = controller.navigationController { var hasOther = false controller.window?.forEachController({ c in @@ -188,7 +197,7 @@ private func isTopmostChatController(_ controller: ChatControllerImpl) -> Bool { return true } -private func calculateSlowmodeActiveUntilTimestamp(account: Account, untilTimestamp: Int32?) -> Int32? { +func calculateSlowmodeActiveUntilTimestamp(account: Account, untilTimestamp: Int32?) -> Int32? { guard let untilTimestamp = untilTimestamp else { return nil } @@ -201,7 +210,7 @@ private func calculateSlowmodeActiveUntilTimestamp(account: Account, untilTimest } } -private struct ScrolledToMessageId: Equatable { +struct ScrolledToMessageId: Equatable { struct AllowedReplacementDirections: OptionSet { var rawValue: Int32 @@ -213,74 +222,43 @@ private struct ScrolledToMessageId: Equatable { var allowedReplacementDirection: AllowedReplacementDirections } -#if DEBUG -private final class SignpostData { - @available(iOSApplicationExtension 12.0, iOS 12.0, *) - final class Impl { - let signpostLog: OSLog - let signpostId: OSSignpostID - - init() { - self.signpostLog = OSLog( - subsystem: "org.telegram.Telegram-iOS", - category: "ChatAppear" - ) - self.signpostId = OSSignpostID(log: self.signpostLog) - } - } - - private static var _impl: AnyObject? = { - if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { - return Impl() - } else { - return nil - } - }() - - @available(iOSApplicationExtension 12.0, iOS 12.0, *) - static var impl: Impl { - return self._impl! as! Impl - } -} -#endif - public final class ChatControllerImpl: TelegramBaseController, ChatController, GalleryHiddenMediaTarget, UIDropInteractionDelegate { - private var validLayout: ContainerViewLayout? + var validLayout: ContainerViewLayout? public weak var parentController: ViewController? - private let currentChatListFilter: Int32? - private let chatNavigationStack: [ChatNavigationStackItem] + let currentChatListFilter: Int32? + let chatNavigationStack: [ChatNavigationStackItem] public var peekActions: ChatControllerPeekActions = .standard - private var didSetup3dTouch: Bool = false + var didSetup3dTouch: Bool = false - private let context: AccountContext + let context: AccountContext public let chatLocation: ChatLocation public let subject: ChatControllerSubject? - private var botStart: ChatControllerInitialBotStart? - private var attachBotStart: ChatControllerInitialAttachBotStart? - private var botAppStart: ChatControllerInitialBotAppStart? + var botStart: ChatControllerInitialBotStart? + var attachBotStart: ChatControllerInitialAttachBotStart? + var botAppStart: ChatControllerInitialBotAppStart? - private let peerDisposable = MetaDisposable() - private let titleDisposable = MetaDisposable() - private var accountPeerDisposable: Disposable? - private let navigationActionDisposable = MetaDisposable() - private var networkStateDisposable: Disposable? + let peerDisposable = MetaDisposable() + let titleDisposable = MetaDisposable() + var accountPeerDisposable: Disposable? + let navigationActionDisposable = MetaDisposable() + var networkStateDisposable: Disposable? - private let messageIndexDisposable = MetaDisposable() + let messageIndexDisposable = MetaDisposable() - private let _chatLocationInfoReady = Promise() - private var didSetChatLocationInfoReady = false - private let chatLocationInfoData: ChatLocationInfoData + let _chatLocationInfoReady = Promise() + var didSetChatLocationInfoReady = false + let chatLocationInfoData: ChatLocationInfoData - private let cachedDataReady = Promise() - private var didSetCachedDataReady = false + let cachedDataReady = Promise() + var didSetCachedDataReady = false - private let wallpaperReady = Promise() - private let presentationReady = Promise() + let wallpaperReady = Promise() + let presentationReady = Promise() - private var presentationInterfaceState: ChatPresentationInterfaceState + var presentationInterfaceState: ChatPresentationInterfaceState let presentationInterfaceStatePromise: ValuePromise public var presentationInterfaceStateSignal: Signal { return self.presentationInterfaceStatePromise.get() |> map { $0 } @@ -290,142 +268,145 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return self.presentationInterfaceState.interfaceState.selectionState?.selectedIds } - private let chatThemeEmoticonPromise = Promise() - private let chatWallpaperPromise = Promise() + let chatThemeEmoticonPromise = Promise() + let chatWallpaperPromise = Promise() - private var chatTitleView: ChatTitleView? - private var leftNavigationButton: ChatNavigationButton? - private var rightNavigationButton: ChatNavigationButton? - private var chatInfoNavigationButton: ChatNavigationButton? + var chatTitleView: ChatTitleView? + var leftNavigationButton: ChatNavigationButton? + var rightNavigationButton: ChatNavigationButton? + var chatInfoNavigationButton: ChatNavigationButton? - private var moreBarButton: MoreHeaderButton - private var moreInfoNavigationButton: ChatNavigationButton? + var moreBarButton: MoreHeaderButton + var moreInfoNavigationButton: ChatNavigationButton? - private var peerView: PeerView? - private var threadInfo: EngineMessageHistoryThread.Info? + var peerView: PeerView? + var threadInfo: EngineMessageHistoryThread.Info? - private var historyStateDisposable: Disposable? + var historyStateDisposable: Disposable? - private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() - private let temporaryHiddenGalleryMediaDisposable = MetaDisposable() + let galleryHiddenMesageAndMediaDisposable = MetaDisposable() + let temporaryHiddenGalleryMediaDisposable = MetaDisposable() - private let chatBackgroundNode: WallpaperBackgroundNode + let chatBackgroundNode: WallpaperBackgroundNode private(set) var controllerInteraction: ChatControllerInteraction? - private var interfaceInteraction: ChatPanelInterfaceInteraction? + var interfaceInteraction: ChatPanelInterfaceInteraction? - private let messageContextDisposable = MetaDisposable() - private let controllerNavigationDisposable = MetaDisposable() - private let sentMessageEventsDisposable = MetaDisposable() - private let failedMessageEventsDisposable = MetaDisposable() - private let sentPeerMediaMessageEventsDisposable = MetaDisposable() - private weak var currentFailedMessagesAlertController: ViewController? - private let messageActionCallbackDisposable = MetaDisposable() - private let messageActionUrlAuthDisposable = MetaDisposable() - private let editMessageDisposable = MetaDisposable() - private let editMessageErrorsDisposable = MetaDisposable() - private let enqueueMediaMessageDisposable = MetaDisposable() - private var resolvePeerByNameDisposable: MetaDisposable? - private var shareStatusDisposable: MetaDisposable? - private var clearCacheDisposable: MetaDisposable? - private var bankCardDisposable: MetaDisposable? - private var hasActiveGroupCallDisposable: Disposable? - private var sendAsPeersDisposable: Disposable? - private var preloadAttachBotIconsDisposables: DisposableSet? - private var keepMessageCountersSyncrhonizedDisposable: Disposable? - private var saveMediaDisposable: MetaDisposable? + let messageContextDisposable = MetaDisposable() + let controllerNavigationDisposable = MetaDisposable() + let sentMessageEventsDisposable = MetaDisposable() + let failedMessageEventsDisposable = MetaDisposable() + let sentPeerMediaMessageEventsDisposable = MetaDisposable() + weak var currentFailedMessagesAlertController: ViewController? + let messageActionCallbackDisposable = MetaDisposable() + let messageActionUrlAuthDisposable = MetaDisposable() + let editMessageDisposable = MetaDisposable() + let editMessageErrorsDisposable = MetaDisposable() + let enqueueMediaMessageDisposable = MetaDisposable() + var resolvePeerByNameDisposable: MetaDisposable? + var shareStatusDisposable: MetaDisposable? + var clearCacheDisposable: MetaDisposable? + var bankCardDisposable: MetaDisposable? + var hasActiveGroupCallDisposable: Disposable? + var sendAsPeersDisposable: Disposable? + var preloadAttachBotIconsDisposables: DisposableSet? + var keepMessageCountersSyncrhonizedDisposable: Disposable? + var saveMediaDisposable: MetaDisposable? + var giveawayStatusDisposable: MetaDisposable? + var nameColorDisposable: Disposable? - private let editingMessage = ValuePromise(nil, ignoreRepeated: true) - private let startingBot = ValuePromise(false, ignoreRepeated: true) - private let unblockingPeer = ValuePromise(false, ignoreRepeated: true) - private let searching = ValuePromise(false, ignoreRepeated: true) - private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() - private let loadingMessage = Promise(nil) - private let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) + let editingMessage = ValuePromise(nil, ignoreRepeated: true) + let startingBot = ValuePromise(false, ignoreRepeated: true) + let unblockingPeer = ValuePromise(false, ignoreRepeated: true) + let searching = ValuePromise(false, ignoreRepeated: true) + let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() + let loadingMessage = Promise(nil) + let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) - private var stateServiceTasks: [AnyHashable: Disposable] = [:] + var stateServiceTasks: [AnyHashable: Disposable] = [:] - private var preloadHistoryPeerId: PeerId? - private let preloadHistoryPeerIdDisposable = MetaDisposable() + var preloadHistoryPeerId: PeerId? + let preloadHistoryPeerIdDisposable = MetaDisposable() - private var preloadNextChatPeerId: PeerId? - private let preloadNextChatPeerIdDisposable = MetaDisposable() + var preloadNextChatPeerId: PeerId? + let preloadNextChatPeerIdDisposable = MetaDisposable() - private let botCallbackAlertMessage = Promise(nil) - private var botCallbackAlertMessageDisposable: Disposable? + let botCallbackAlertMessage = Promise(nil) + var botCallbackAlertMessageDisposable: Disposable? - private var selectMessagePollOptionDisposables: DisposableDict? - private var selectPollOptionFeedback: HapticFeedback? + var selectMessagePollOptionDisposables: DisposableDict? + var selectPollOptionFeedback: HapticFeedback? - private var resolveUrlDisposable: MetaDisposable? + var resolveUrlDisposable: MetaDisposable? - private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] - private var searchQuerySuggestionState: (ChatPresentationInputQuery?, Disposable)? - private var urlPreviewQueryState: (String?, Disposable)? - private var editingUrlPreviewQueryState: (String?, Disposable)? - private var searchState: ChatSearchState? + var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] + var searchQuerySuggestionState: (ChatPresentationInputQuery?, Disposable)? + var urlPreviewQueryState: (UrlPreviewState?, Disposable)? + var editingUrlPreviewQueryState: (UrlPreviewState?, Disposable)? + var replyMessageState: (EngineMessage.Id, Disposable)? + var searchState: ChatSearchState? - private var shakeFeedback: HapticFeedback? + var shakeFeedback: HapticFeedback? - private var recordingModeFeedback: HapticFeedback? - private var recorderFeedback: HapticFeedback? - private var audioRecorderValue: ManagedAudioRecorder? - private var audioRecorder = Promise() - private var audioRecorderDisposable: Disposable? - private var audioRecorderStatusDisposable: Disposable? + var recordingModeFeedback: HapticFeedback? + var recorderFeedback: HapticFeedback? + var audioRecorderValue: ManagedAudioRecorder? + var audioRecorder = Promise() + var audioRecorderDisposable: Disposable? + var audioRecorderStatusDisposable: Disposable? - private var videoRecorderValue: InstantVideoController? - private var videoRecorder = Promise() - private var videoRecorderDisposable: Disposable? + var videoRecorderValue: InstantVideoController? + var videoRecorder = Promise() + var videoRecorderDisposable: Disposable? - private var buttonKeyboardMessageDisposable: Disposable? - private var cachedDataDisposable: Disposable? - private var chatUnreadCountDisposable: Disposable? - private var buttonUnreadCountDisposable: Disposable? - private var chatUnreadMentionCountDisposable: Disposable? - private var peerInputActivitiesDisposable: Disposable? + var buttonKeyboardMessageDisposable: Disposable? + var cachedDataDisposable: Disposable? + var chatUnreadCountDisposable: Disposable? + var buttonUnreadCountDisposable: Disposable? + var chatUnreadMentionCountDisposable: Disposable? + var peerInputActivitiesDisposable: Disposable? - private var peerInputActivitiesPromise = Promise<[(Peer, PeerInputActivity)]>() - private var interactiveEmojiSyncDisposable = MetaDisposable() + var peerInputActivitiesPromise = Promise<[(Peer, PeerInputActivity)]>() + var interactiveEmojiSyncDisposable = MetaDisposable() - private var recentlyUsedInlineBotsValue: [Peer] = [] - private var recentlyUsedInlineBotsDisposable: Disposable? + var recentlyUsedInlineBotsValue: [Peer] = [] + var recentlyUsedInlineBotsDisposable: Disposable? - private var unpinMessageDisposable: MetaDisposable? - - private let typingActivityPromise = Promise(false) - private var inputActivityDisposable: Disposable? - private var recordingActivityValue: ChatRecordingActivity = .none - private let recordingActivityPromise = ValuePromise(.none, ignoreRepeated: true) - private var recordingActivityDisposable: Disposable? - private var acquiredRecordingActivityDisposable: Disposable? - private let choosingStickerActivityPromise = ValuePromise(false) - private var choosingStickerActivityDisposable: Disposable? + var unpinMessageDisposable: MetaDisposable? + + let typingActivityPromise = Promise(false) + var inputActivityDisposable: Disposable? + var recordingActivityValue: ChatRecordingActivity = .none + let recordingActivityPromise = ValuePromise(.none, ignoreRepeated: true) + var recordingActivityDisposable: Disposable? + var acquiredRecordingActivityDisposable: Disposable? + let choosingStickerActivityPromise = ValuePromise(false) + var choosingStickerActivityDisposable: Disposable? - private var searchDisposable: MetaDisposable? + var searchDisposable: MetaDisposable? - private var historyNavigationStack = ChatHistoryNavigationStack() + var historyNavigationStack = ChatHistoryNavigationStack() public let canReadHistory = ValuePromise(true, ignoreRepeated: true) - private var reminderActivity: NSUserActivity? - private var isReminderActivityEnabled: Bool = false + var reminderActivity: NSUserActivity? + var isReminderActivityEnabled: Bool = false - private var canReadHistoryValue = false - private var canReadHistoryDisposable: Disposable? + var canReadHistoryValue = false + var canReadHistoryDisposable: Disposable? - private var themeEmoticonAndDarkAppearancePreviewPromise = Promise<(String?, Bool?)>((nil, nil)) - private var didSetPresentationData = false - private var presentationData: PresentationData - private var presentationDataPromise = Promise() + var themeEmoticonAndDarkAppearancePreviewPromise = Promise<(String?, Bool?)>((nil, nil)) + var didSetPresentationData = false + var presentationData: PresentationData + var presentationDataPromise = Promise() override public var updatedPresentationData: (PresentationData, Signal) { return (self.presentationData, self.presentationDataPromise.get()) } - private var presentationDataDisposable: Disposable? + var presentationDataDisposable: Disposable? - private var automaticMediaDownloadSettings: MediaAutoDownloadSettings - private var automaticMediaDownloadSettingsDisposable: Disposable? + var automaticMediaDownloadSettings: MediaAutoDownloadSettings + var automaticMediaDownloadSettingsDisposable: Disposable? - private var disableStickerAnimationsPromise = ValuePromise(false) - private var disableStickerAnimationsValue = false + var disableStickerAnimationsPromise = ValuePromise(false) + var disableStickerAnimationsValue = false var disableStickerAnimations: Bool { get { return self.disableStickerAnimationsValue @@ -433,100 +414,100 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.disableStickerAnimationsPromise.set(newValue) } } - private var stickerSettings: ChatInterfaceStickerSettings - private var stickerSettingsDisposable: Disposable? + var stickerSettings: ChatInterfaceStickerSettings + var stickerSettingsDisposable: Disposable? - private var applicationInForegroundDisposable: Disposable? - private var applicationInFocusDisposable: Disposable? + var applicationInForegroundDisposable: Disposable? + var applicationInFocusDisposable: Disposable? - private let checksTooltipDisposable = MetaDisposable() - private var shouldDisplayChecksTooltip = false + let checksTooltipDisposable = MetaDisposable() + var shouldDisplayChecksTooltip = false - private let peerSuggestionsDisposable = MetaDisposable() - private let peerSuggestionsDismissDisposable = MetaDisposable() - private var displayedConvertToGigagroupSuggestion = false + let peerSuggestionsDisposable = MetaDisposable() + let peerSuggestionsDismissDisposable = MetaDisposable() + var displayedConvertToGigagroupSuggestion = false - private var checkedPeerChatServiceActions = false + var checkedPeerChatServiceActions = false - private var willAppear = false - private var didAppear = false - private var scheduledActivateInput: ChatControllerActivateInput? + var willAppear = false + var didAppear = false + var scheduledActivateInput: ChatControllerActivateInput? - private var raiseToListen: RaiseToListenManager? - private var voicePlaylistDidEndTimestamp: Double = 0.0 + var raiseToListen: RaiseToListenManager? + var voicePlaylistDidEndTimestamp: Double = 0.0 - private weak var emojiTooltipController: TooltipController? - private weak var sendingOptionsTooltipController: TooltipController? - private weak var searchResultsTooltipController: TooltipController? - private weak var messageTooltipController: TooltipController? - private weak var videoUnmuteTooltipController: TooltipController? - private var didDisplayVideoUnmuteTooltip = false - private var didDisplaySendWhenOnlineTip = false - private weak var silentPostTooltipController: TooltipController? - private weak var mediaRecordingModeTooltipController: TooltipController? - private weak var mediaRestrictedTooltipController: TooltipController? - private var mediaRestrictedTooltipControllerMode = true - private weak var checksTooltipController: TooltipController? - private weak var copyProtectionTooltipController: TooltipController? + weak var emojiTooltipController: TooltipController? + weak var sendingOptionsTooltipController: TooltipController? + weak var searchResultsTooltipController: TooltipController? + weak var messageTooltipController: TooltipController? + weak var videoUnmuteTooltipController: TooltipController? + var didDisplayVideoUnmuteTooltip = false + var didDisplaySendWhenOnlineTip = false + weak var silentPostTooltipController: TooltipController? + weak var mediaRecordingModeTooltipController: TooltipController? + weak var mediaRestrictedTooltipController: TooltipController? + var mediaRestrictedTooltipControllerMode = true + weak var checksTooltipController: TooltipController? + weak var copyProtectionTooltipController: TooltipController? - private var currentMessageTooltipScreens: [(TooltipScreen, ListViewItemNode)] = [] + var currentMessageTooltipScreens: [(TooltipScreen, ListViewItemNode)] = [] - private weak var slowmodeTooltipController: ChatSlowmodeHintController? + weak var slowmodeTooltipController: ChatSlowmodeHintController? - private weak var currentContextController: ContextController? + weak var currentContextController: ContextController? - private weak var sendMessageActionsController: ChatSendMessageActionSheetController? - private var searchResultsController: ChatSearchResultsController? + weak var sendMessageActionsController: ChatSendMessageActionSheetController? + var searchResultsController: ChatSearchResultsController? - private weak var themeScreen: ChatThemeScreen? + weak var themeScreen: ChatThemeScreen? - private weak var currentPinchController: PinchController? - private weak var currentPinchSourceItemNode: ListViewItemNode? + weak var currentPinchController: PinchController? + weak var currentPinchSourceItemNode: ListViewItemNode? - private var screenCaptureManager: ScreenCaptureDetectionManager? - private let chatAdditionalDataDisposable = MetaDisposable() + var screenCaptureManager: ScreenCaptureDetectionManager? + let chatAdditionalDataDisposable = MetaDisposable() - private var reportIrrelvantGeoNoticePromise = Promise() - private var reportIrrelvantGeoNotice: Bool? - private var reportIrrelvantGeoDisposable: Disposable? + var reportIrrelvantGeoNoticePromise = Promise() + var reportIrrelvantGeoNotice: Bool? + var reportIrrelvantGeoDisposable: Disposable? - private var hasScheduledMessages: Bool = false + var hasScheduledMessages: Bool = false - private var volumeButtonsListener: VolumeButtonsListener? + var volumeButtonsListener: VolumeButtonsListener? - private var beginMediaRecordingRequestId: Int = 0 - private var lockMediaRecordingRequestId: Int? + var beginMediaRecordingRequestId: Int = 0 + var lockMediaRecordingRequestId: Int? - private var updateSlowmodeStatusDisposable = MetaDisposable() - private var updateSlowmodeStatusTimerValue: Int32? + var updateSlowmodeStatusDisposable = MetaDisposable() + var updateSlowmodeStatusTimerValue: Int32? - private var isDismissed = false + var isDismissed = false - private var focusOnSearchAfterAppearance: (ChatSearchDomain, String)? + var focusOnSearchAfterAppearance: (ChatSearchDomain, String)? - private let keepPeerInfoScreenDataHotDisposable = MetaDisposable() - private let preloadAvatarDisposable = MetaDisposable() + let keepPeerInfoScreenDataHotDisposable = MetaDisposable() + let preloadAvatarDisposable = MetaDisposable() - private let peekData: ChatPeekTimeout? - private let peekTimerDisposable = MetaDisposable() + let peekData: ChatPeekTimeout? + let peekTimerDisposable = MetaDisposable() - private let createVoiceChatDisposable = MetaDisposable() + let createVoiceChatDisposable = MetaDisposable() - private let selectAddMemberDisposable = MetaDisposable() - private let addMemberDisposable = MetaDisposable() + let selectAddMemberDisposable = MetaDisposable() + let addMemberDisposable = MetaDisposable() - private var shouldDisplayDownButton = false + var shouldDisplayDownButton = false - private var hasEmbeddedTitleContent = false - private var isEmbeddedTitleContentHidden = false + var hasEmbeddedTitleContent = false + var isEmbeddedTitleContentHidden = false - private let chatLocationContextHolder: Atomic + let chatLocationContextHolder: Atomic - private weak var attachmentController: AttachmentController? - private weak var currentMenuWebAppController: ViewController? - private weak var currentWebAppController: ViewController? + weak var attachmentController: AttachmentController? + weak var currentMenuWebAppController: ViewController? + weak var currentWebAppController: ViewController? - private weak var currentImportMessageTooltip: UndoOverlayController? + weak var currentImportMessageTooltip: UndoOverlayController? public override var customData: Any? { return self.chatLocation @@ -542,7 +523,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private var scheduledScrollToMessageId: (MessageId, Double?)? + override public var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? { + return .widthMultiplier(factor: 0.35, min: 16.0, max: 200.0) + } + + var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)? public var purposefulAction: (() -> Void)? var updatedClosedPinnedMessageId: ((MessageId) -> Void)? @@ -550,23 +535,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G public var isSelectingMessagesUpdated: ((Bool) -> Void)? - private let scrolledToMessageId = ValuePromise(nil, ignoreRepeated: true) - private var scrolledToMessageIdValue: ScrolledToMessageId? = nil { + let scrolledToMessageId = ValuePromise(nil, ignoreRepeated: true) + var scrolledToMessageIdValue: ScrolledToMessageId? = nil { didSet { self.scrolledToMessageId.set(self.scrolledToMessageIdValue) } } - private var translationStateDisposable: Disposable? - private var premiumGiftSuggestionDisposable: Disposable? + var translationStateDisposable: Disposable? + var premiumGiftSuggestionDisposable: Disposable? - private var nextChannelToReadDisposable: Disposable? - private var offerNextChannelToRead = false + var nextChannelToReadDisposable: Disposable? + var offerNextChannelToRead = false - private var inviteRequestsContext: PeerInvitationImportersContext? - private var inviteRequestsDisposable = MetaDisposable() + var inviteRequestsContext: PeerInvitationImportersContext? + var inviteRequestsDisposable = MetaDisposable() - private var overlayTitle: String? { + var overlayTitle: String? { var title: String? if let threadInfo = self.threadInfo { title = threadInfo.title @@ -578,12 +563,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return title } - private var currentSpeechHolder: SpeechSynthesizerHolder? + var currentSpeechHolder: SpeechSynthesizerHolder? + + var powerSavingMonitoringDisposable: Disposable? - private var powerSavingMonitoringDisposable: Disposable? + var avatarNode: ChatAvatarNavigationNode? + var storyStats: PeerStoryStats? - private var avatarNode: ChatAvatarNavigationNode? - private var storyStats: PeerStoryStats? + var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)? + var performOpenURL: ((Message?, String, Promise?) -> Void)? public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) { let _ = ChatControllerCount.modify { value in @@ -647,7 +635,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.stickerSettings = ChatInterfaceStickerSettings() - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.presentationInterfaceStatePromise = ValuePromise(self.presentationInterfaceState) var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none @@ -770,6 +758,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .full: break } + } else if let _ = media as? TelegramMediaGiveaway { + var signal = strongSelf.context.engine.payments.premiumGiveawayInfo(peerId: message.id.peerId, messageId: message.id) + let disposable: MetaDisposable + if let current = strongSelf.giveawayStatusDisposable { + disposable = current + } else { + disposable = MetaDisposable() + strongSelf.giveawayStatusDisposable = disposable + } + + let progressSignal = Signal { subscriber in + return EmptyDisposable + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.startStrict() + + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + disposable.set((signal + |> deliverOnMainQueue).startStrict(next: { [weak self] info in + if let strongSelf = self, let info { + strongSelf.displayGiveawayStatusInfo(messageId: message.id, giveawayInfo: info) + } + })) + + return true } else if let action = media as? TelegramMediaAction { if !displayVoiceMessageDiscardAlert() { return false @@ -778,7 +797,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .pinnedMessageUpdated: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil)) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil))) break } } @@ -787,7 +806,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .gameScore: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil)) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil))) break } } @@ -942,7 +961,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .setSameChatWallpaper: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.controllerInteraction?.navigateToMessage(message.id, attribute.messageId) + strongSelf.controllerInteraction?.navigateToMessage(message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) return true } } @@ -953,6 +972,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration)) strongSelf.push(controller) return true + case let .giftCode(slug, _, _, _, _): + strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id) + return true case let .suggestedProfilePhoto(image): strongSelf.chatDisplayNode.dismissInput() if let image = image { @@ -1114,7 +1136,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, openPeerMention: { [weak self] mention in if let strongSelf = self { - strongSelf.controllerInteraction?.openPeerMention(mention) + strongSelf.controllerInteraction?.openPeerMention(mention, nil) } }, openPeer: { [weak self] peer in if let strongSelf = self { @@ -1194,7 +1216,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, openPeer: { [weak self] peer, navigation, fromMessage, source in var expandAvatar = false if case let .groupParticipant(storyStats, avatarHeaderNode) = source { - if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNode { + if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNodeImpl { self?.openStories(peerId: peer.id, avatarHeaderNode: avatarHeaderNode, avatarNode: nil) return } else { @@ -1206,8 +1228,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G fromReactionMessageId = fromMessage?.id } self?.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: fromReactionMessageId, expandAvatar: expandAvatar) - }, openPeerMention: { [weak self] name in - self?.openPeerMention(name) + }, openPeerMention: { [weak self] name, progress in + self?.openPeerMention(name, progress: progress) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in guard let strongSelf = self, strongSelf.isNodeLoaded else { return @@ -1344,9 +1366,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, - isStatusSelection: false, - isReactionSelection: true, - isEmojiSelection: false, + subject: .reaction, hasTrending: false, topReactionItems: reactionItems, areUnicodeEmojiEnabled: false, @@ -2019,10 +2039,52 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(pinchController) }, openMessageContextActions: { message, node, rect, gesture in gesture?.cancel() - }, navigateToMessage: { [weak self] fromId, id in - self?.navigateToMessage(from: fromId, to: .id(id, nil), forceInCurrentChat: fromId.peerId == id.peerId) + }, navigateToMessage: { [weak self] fromId, id, params in + guard let self else { + return + } + + let continueNavigation: () -> Void = { [weak self] in + guard let self else { + return + } + self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId) + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: id.peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] toPeer in + guard let self else { + return + } + + if params.quote != nil { + if let toPeer { + switch toPeer { + case let .channel(channel): + if channel.username == nil && channel.usernames.isEmpty { + switch channel.participationStatus { + case .kicked, .left: + self.controllerInteraction?.attemptedNavigationToPrivateQuote(toPeer._asPeer()) + return + case .member: + break + } + } + default: + break + } + } else { + self.controllerInteraction?.attemptedNavigationToPrivateQuote(nil) + return + } + } + + continueNavigation() + }) }, navigateToMessageStandalone: { [weak self] id in - self?.navigateToMessage(from: nil, to: .id(id, nil), forceInCurrentChat: false) + self?.navigateToMessage(from: nil, to: .id(id, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: false) }, navigateToThreadMessage: { [weak self] peerId, threadId, messageId in if let context = self?.context, let navigationController = self?.effectiveNavigationController { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, keepStack: .always).startStandalone() @@ -2034,7 +2096,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let subject = strongSelf.subject, case .forwardedMessages = subject, !value { + if let subject = strongSelf.subject, case .messageOptions = subject, !value { let selectedCount = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds.count ?? 0 let updatedSelectedCount = selectedCount - ids.count if updatedSelectedCount < 1 { @@ -2077,7 +2139,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -2089,9 +2151,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let peerId = strongSelf.chatLocation.peerId if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = strongSelf.chatDisplayNode.interactiveEmojis, interactiveEmojis.emojis.contains(text) { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { - strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in guard let strongSelf = self else { @@ -2128,7 +2190,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var current = current current = current.updatedInterfaceState { interfaceState in var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageId(nil) + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil) if clearInput { interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) } @@ -2187,7 +2249,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] if silentPosting { let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting) strongSelf.sendMessages(transformedMessages) @@ -2216,7 +2278,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - strongSelf.sendMessages([.message(text: text, attributes: [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))])], inlineStickers: [file.fileId : file], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)], commit: false) + strongSelf.sendMessages([.message(text: text, attributes: [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))])], inlineStickers: [file.fileId : file], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)], commit: false) } } else { strongSelf.interfaceInteraction?.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: attribute])) @@ -2253,7 +2315,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }.updatedInputMode { current in if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { return .media(mode: mode, expanded: nil, focused: focused) } @@ -2263,7 +2325,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, nil) - var messages = [EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + var messages = [EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] if silentPosting { messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true) strongSelf.sendMessages(messages) @@ -2592,8 +2654,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.openPeer(peer: nil, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil, peekData: nil), fromMessage: nil, peerTypes: peerTypes) } } - }, openUrl: { [weak self] url, concealed, _, message in + }, openUrl: { [weak self] urlData in if let strongSelf = self { + let url = urlData.url + let concealed = urlData.concealed + let message = urlData.message + let progress = urlData.progress + var skipConcealedAlert = false if let author = message?.author, author.isVerified { skipConcealedAlert = true @@ -2603,7 +2670,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId) } - strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message) + if let performOpenURL = strongSelf.performOpenURL { + performOpenURL(message, url, progress) + } else { + strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution, progress: progress) + } } }, shareCurrentLocation: { [weak self] in if let strongSelf = self { @@ -2620,7 +2691,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).startStandalone(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root)) } @@ -2644,7 +2715,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId) |> deliverOnMainQueue).startStandalone(next: { peer in if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }) } @@ -2679,7 +2750,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } }) } }, nil) @@ -2688,13 +2759,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: command, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + var replyToMessageId: EngineMessageReplySubject? + if postAsReply, let messageId { + replyToMessageId = EngineMessageReplySubject(messageId: messageId, quote: nil) + } + strongSelf.sendMessages([.message(text: command, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { strongSelf.chatDisplayNode.dismissInput() - openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) if case .overlay = strongSelf.presentationInterfaceState.mode { strongSelf.chatDisplayNode.dismissAsOverlay() @@ -2705,7 +2780,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { strongSelf.chatDisplayNode.dismissInput() - openChatWallpaper(context: strongSelf.context, message: message, present: { [weak self] c, a in + strongSelf.context.sharedContext.openChatWallpaper(context: strongSelf.context, message: message, present: { [weak self] c, a in self?.push(c) }) }) @@ -3013,7 +3088,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.sendMessages([.message(text: command, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: command, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } })) } @@ -3205,7 +3280,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.controllerInteraction?.openUrl(url.url, false, false, message) + strongSelf.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: false, external: false, message: message)) } })) } @@ -3284,7 +3359,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, openSearch: { }, setupReply: { [weak self] messageId in - self?.interfaceInteraction?.setupReplyMessage(messageId, { _ in }) + self?.interfaceInteraction?.setupReplyMessage(messageId, { _, f in f() }) }, canSetupReply: { [weak self] message in if !message.flags.contains(.Incoming) { if !message.flags.intersection([.Failed, .Sending, .Unsent]).isEmpty { @@ -3307,6 +3382,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } return .none + }, canSendMessages: { [weak self] in + guard let self else { + return false + } + return canSendMessagesToChat(self.presentationInterfaceState) }, navigateToFirstDateMessage: { [weak self] timestamp, alreadyThere in guard let strongSelf = self else { return @@ -3638,7 +3718,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp))) } else { - strongSelf.navigateToMessage(messageLocation: .id(messageId, Double(timestamp)), animated: true, forceInCurrentChat: true) + strongSelf.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: Double(timestamp), quote: nil)), animated: true, forceInCurrentChat: true) } } } @@ -3652,7 +3732,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: time) { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } }) if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { @@ -3694,7 +3774,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let inlineStickers: [MediaId: TelegramMediaFile] = [:] - strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).startStrict(next: { result in + strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).startStrict(next: { result in }, error: { error in })) } @@ -3702,10 +3782,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, delay: true) } - }, performTextSelectionAction: { [weak self] canCopy, text, action in + }, performTextSelectionAction: { [weak self] message, canCopy, text, action in guard let strongSelf = self else { return } + + if let performTextSelectionAction = strongSelf.performTextSelectionAction { + performTextSelectionAction(message, canCopy, text, action) + return + } + switch action { case .copy: storeAttributedTextInPasteboard(text) @@ -3784,7 +3870,52 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { f() } - + case let .quote(range): + let completion: (ContainedViewLayoutTransition?) -> Void = { transition in + guard let self else { + return + } + if let currentContextController = self.currentContextController { + self.currentContextController = nil + + if let transition { + currentContextController.dismissWithCustomTransition(transition: transition) + } else { + currentContextController.dismiss(completion: {}) + } + } + } + if let messageId = message?.id, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) ?? message { + var quoteData: EngineMessageReplyQuote? + + let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound) + let quoteText = (message.text as NSString).substring(with: nsRange) + + let trimmedText = trimStringWithEntities(string: quoteText, entities: messageTextEntitiesInRange(entities: message.textEntitiesAttribute?.entities ?? [], range: nsRange, onlyQuoteable: true), maxLength: quoteMaxLength(appConfig: strongSelf.context.currentAppConfiguration.with({ $0 }))) + if !trimmedText.string.isEmpty { + quoteData = EngineMessageReplyQuote(text: trimmedText.string, entities: trimmedText.entities, media: nil) + } + + let replySubject = ChatInterfaceState.ReplyMessageSubject( + messageId: message.id, + quote: quoteData + ) + + if canSendMessagesToChat(strongSelf.presentationInterfaceState) { + let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion) + strongSelf.updateItemNodesSearchTextHighlightStates() + strongSelf.chatDisplayNode.ensureInputViewFocused() + }, alertAction: { + completion(nil) + }, delay: true) + } else { + moveReplyMessageToAnotherChat(selfController: strongSelf, replySubject: replySubject) + completion(nil) + } + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: completion) + } } }, displayImportedMessageTooltip: { [weak self] _ in guard let strongSelf = self else { @@ -3946,7 +4077,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.canReadHistory.set(false) - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in self?.canReadHistory.set(true) } @@ -3976,7 +4107,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.openMessageReplies(messageId: threadMessageId, displayProgressInMessage: message.id, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: false) } } else { - strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, nil)) + strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))) } break } @@ -4041,9 +4172,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) } else { - strongSelf.presentOldMediaPicker(fileMode: false, editingMedia: true, present: { [weak self] c, _ in - self?.effectiveNavigationController?.pushViewController(c) - }, completion: { signals, _, _ in + strongSelf.presentOldMediaPicker(fileMode: false, editingMedia: true, completion: { signals, _, _ in self?.interfaceInteraction?.setupEditMessage(messageId, { _ in }) self?.editMessageMediaWithLegacySignals(signals) }) @@ -4381,7 +4510,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .peer(id, messageId, startParam): if case let .peer(currentPeerId) = self.chatLocation, currentPeerId == id { if let messageId { - self.navigateToMessage(from: nil, to: .id(messageId, nil), rememberInStack: false) + self.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false) } } else { let navigationData: ChatControllerInteractionNavigateToPeer @@ -4390,7 +4519,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { var subject: ChatControllerSubject? if let messageId = messageId { - subject = .message(id: .id(messageId), highlight: true, timecode: nil) + subject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) } navigationData = .chat(textInputState: nil, subject: subject, peekData: nil) } @@ -4404,7 +4533,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .join(_, joinHash): self.controllerInteraction?.openJoinLink(joinHash) case let .webPage(_, url): - self.controllerInteraction?.openUrl(url, false, false, nil) + self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false)) } }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in guard let self else { @@ -4668,6 +4797,61 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) }) + }, openNoAdsDemo: { [weak self] in + guard let self else { + return + } + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: self.context, subject: .noAds, action: { + let controller = PremiumIntroScreen(context: self.context, source: .ads) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + self.push(controller) + }, displayGiveawayParticipationStatus: { [weak self] messageId in + guard let self else { + return + } + let disposable: MetaDisposable + if let current = self.giveawayStatusDisposable { + disposable = current + } else { + disposable = MetaDisposable() + self.giveawayStatusDisposable = disposable + } + disposable.set((self.context.engine.payments.premiumGiveawayInfo(peerId: messageId.peerId, messageId: messageId) + |> deliverOnMainQueue).start(next: { [weak self] info in + guard let self, let info else { + return + } + let content: UndoOverlayContent + switch info { + case let .ongoing(_, status): + switch status { + case .notAllowed: + content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_NotAllowed, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore) + case .participating: + content = .succeed(text: self.presentationData.strings.Chat_Giveaway_Toast_Participating, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore) + case .notQualified: + content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_NotQualified, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore) + case .almostOver: + content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_AlmostOver, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore) + } + case .finished: + content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_Ended, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore) + } + let controller = UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in + if case .undo = action, let self { + self.displayGiveawayStatusInfo(messageId: messageId, giveawayInfo: info) + return true + } + return false + }) + self.present(controller, in: .current) + + })) }, requestMessageUpdate: { [weak self] id, scroll in if let self { self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) @@ -4785,6 +4969,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) self.push(storyContainerScreen) }) + }, attemptedNavigationToPrivateQuote: { [weak self] peer in + guard let self else { + return + } + let text: String + if let peer = peer as? TelegramChannel { + if case .broadcast = peer.info { + text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateChannel + } else { + text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateGroup + } + } else if peer is TelegramGroup { + text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateGroup + } else { + text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateChat + } + self.controllerInteraction?.displayUndo(.info(title: nil, text: text, timeout: nil, customUndoText: nil)) }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency @@ -4793,7 +4994,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G //if chatLocation.threadId == nil { if let peerId = chatLocation.peerId, peerId != context.account.peerId { switch subject { - case .pinnedMessages, .scheduledMessages, .forwardedMessages: + case .pinnedMessages, .scheduledMessages, .messageOptions: break default: self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId) @@ -4814,6 +5015,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G //} self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer) + + if case .messageOptions = self.subject { + self.chatTitleView?.disableAnimations = true + } + self.navigationItem.titleView = self.chatTitleView self.chatTitleView?.longPressed = { [weak self] in // MARK: - Nicegram (search by longpress in restricted chat) @@ -4887,7 +5093,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.canReadHistory.set(false) - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) contextController.dismissed = { [weak self] in self?.canReadHistory.set(true) } @@ -4935,12 +5141,44 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in if let strongSelf = self { let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { - return $0.updatedIsPremium(isPremium) + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in + return state.updatedIsPremium(isPremium) }) } }) + if let chatPeerId = chatLocation.peerId { + self.nameColorDisposable = (context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] accountPeer, chatPeer in + guard let self, let accountPeer, let chatPeer else { + return + } + var nameColor: PeerNameColor? + if case let .channel(channel) = chatPeer, case .broadcast = channel.info { + nameColor = chatPeer.nameColor + } else { + nameColor = accountPeer.nameColor + } + var accountPeerColor: ChatPresentationInterfaceState.AccountPeerColor? + if let nameColor { + let colors = self.context.peerNameColors.get(nameColor) + var style: ChatPresentationInterfaceState.AccountPeerColor.Style = .solid + if colors.tertiary != nil { + style = .tripleDashed + } else if colors.secondary != nil { + style = .doubleDashed + } + accountPeerColor = ChatPresentationInterfaceState.AccountPeerColor(style: style) + } + self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in + return state.updatedAccountPeerColor(accountPeerColor) + }) + }) + } + do { let peerId = chatLocationPeerId if case let .peer(peerView) = self.chatLocationInfoData, let peerId = peerId { @@ -4993,7 +5231,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var isScheduledOrPinnedMessages = false switch subject { - case .scheduledMessages, .pinnedMessages, .forwardedMessages: + case .scheduledMessages, .pinnedMessages, .messageOptions: isScheduledOrPinnedMessages = true default: break @@ -5023,7 +5261,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return message?.totalCount } |> distinctUntilChanged - } else if case let .forwardedMessages(peerIds, messageIds, options) = subject { + } else if case let .messageOptions(peerIds, messageIds, info) = subject { displayedCountSignal = self.presentationInterfaceStatePromise.get() |> map { state -> Int? in if let selectionState = state.interfaceState.selectionState { @@ -5038,73 +5276,95 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> take(1) let presentationData = self.presentationData - subtitleTextSignal = combineLatest(peers, options, displayedCountSignal) - |> map { peersView, options, count in - let peers = peersView.peers.values - if !peers.isEmpty { - if peers.count == 1, let peer = peers.first { - if let peer = peer as? TelegramUser { - let displayName = EnginePeer(peer).compactDisplayTitle - if count == 1 { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardHidden(displayName).string - } else { - return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardVisible(displayName).string - } - } else { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardHidden(displayName).string + + switch info { + case let .forward(forward): + subtitleTextSignal = combineLatest(peers, forward.options, displayedCountSignal) + |> map { peersView, options, count in + let peers = peersView.peers.values + if !peers.isEmpty { + if peers.count == 1, let peer = peers.first { + if let peer = peer as? TelegramUser { + let displayName = EnginePeer(peer).compactDisplayTitle + if count == 1 { + if options.hideNames { + return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardHidden(displayName).string + } else { + return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardVisible(displayName).string + } } else { - return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardVisible(displayName).string + if options.hideNames { + return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardHidden(displayName).string + } else { + return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardVisible(displayName).string + } } - } - } else if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - if count == 1 { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardHidden + } else if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if count == 1 { + if options.hideNames { + return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardHidden + } else { + return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardVisible + } } else { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardVisible + if options.hideNames { + return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardHidden + } else { + return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardVisible + } } } else { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardHidden + if count == 1 { + if options.hideNames { + return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardHidden + } else { + return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardVisible + } } else { - return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardVisible + if options.hideNames { + return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardHidden + } else { + return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardVisible + } } } } else { if count == 1 { if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardHidden + return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardHidden } else { - return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardVisible + return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardVisible } } else { if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardHidden + return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardHidden } else { - return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardVisible + return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardVisible } } } } else { - if count == 1 { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardVisible - } - } else { - if options.hideNames { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardHidden - } else { - return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardVisible - } - } + return nil } - } else { - return nil } + case let .reply(reply): + subtitleTextSignal = reply.selectionState.get() + |> map { selectionState -> String? in + if !selectionState.canQuote { + return nil + } + return presentationData.strings.Chat_SubtitleQuoteSelectionTip + } + case let .link(link): + subtitleTextSignal = link.options + |> map { options -> String? in + if options.hasAlternativeLinks { + return presentationData.strings.Chat_SubtitleLinkListTip + } else { + return nil + } + } + |> distinctUntilChanged } } @@ -5117,9 +5377,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { hasPeerInfo = .single(true) } + + enum MessageOptionsTitleInfo { + case reply(hasQuote: Bool) + } + let messageOptionsTitleInfo: Signal + if case let .messageOptions(_, _, info) = self.subject { + switch info { + case .forward, .link: + messageOptionsTitleInfo = .single(nil) + case let .reply(reply): + messageOptionsTitleInfo = reply.selectionState.get() + |> map { selectionState -> Bool in + return selectionState.quote != nil + } + |> distinctUntilChanged + |> map { hasQuote -> MessageOptionsTitleInfo in + return .reply(hasQuote: hasQuote) + } + } + } else { + messageOptionsTitleInfo = .single(nil) + } - self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo in + self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo, messageOptionsTitleInfo) + |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo, messageOptionsTitleInfo in if let strongSelf = self { var isScheduledMessages = false if case .scheduledMessages = presentationInterfaceState.subject { @@ -5127,8 +5409,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let peer = peerViewMainPeer(peerView) { - if case .forwardedMessages = presentationInterfaceState.subject { - if displayedCount == 1 { + if case let .messageOptions(_, _, info) = presentationInterfaceState.subject { + if case .reply = info { + let titleContent: ChatTitleContent + if case let .reply(hasQuote) = messageOptionsTitleInfo, hasQuote { + titleContent = .custom(presentationInterfaceState.strings.Chat_TitleQuoteSelection, subtitleText, false) + } else { + titleContent = .custom(presentationInterfaceState.strings.Chat_TitleReply, subtitleText, false) + } + if strongSelf.chatTitleView?.titleContent != titleContent { + if strongSelf.chatTitleView?.titleContent != nil { + strongSelf.chatTitleView?.animateLayoutTransition() + } + strongSelf.chatTitleView?.titleContent = titleContent + } + } else if case .link = info { + strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitleLinkOptions, subtitleText, false) + } else if displayedCount == 1 { strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitleSingle, subtitleText, false) } else { strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitle(Int32(displayedCount ?? 1)), subtitleText, false) @@ -5658,7 +5955,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var isScheduledOrPinnedMessages = false switch subject { - case .scheduledMessages, .pinnedMessages, .forwardedMessages: + case .scheduledMessages, .pinnedMessages, .messageOptions: isScheduledOrPinnedMessages = true default: break @@ -6462,7 +6759,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - if case let .forwardedMessages(_, messageIds, _) = self.subject, messageIds.count > 1 { + if case let .messageOptions(_, messageIds, _) = self.subject, messageIds.count > 1 { self.updateChatPresentationInterfaceState(interactive: false, { state in return state.updatedInterfaceState({ $0.withUpdatedSelectedMessages(messageIds) }) }) @@ -6507,6 +6804,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G info.1.dispose() } self.urlPreviewQueryState?.1.dispose() + self.editingUrlPreviewQueryState?.1.dispose() + self.replyMessageState?.1.dispose() self.audioRecorderDisposable?.dispose() self.audioRecorderStatusDisposable?.dispose() self.videoRecorderDisposable?.dispose() @@ -6556,6 +6855,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.premiumGiftSuggestionDisposable?.dispose() self.powerSavingMonitoringDisposable?.dispose() self.saveMediaDisposable?.dispose() + self.giveawayStatusDisposable?.dispose() + self.nameColorDisposable?.dispose() self.choosingStickerActivityDisposable?.dispose() self.automaticMediaDownloadSettingsDisposable?.dispose() self.stickerSettingsDisposable?.dispose() @@ -6576,7 +6877,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func themeAndStringsUpdated() { + func themeAndStringsUpdated() { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) switch self.presentationInterfaceState.mode { case .standard: @@ -6603,7 +6904,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.currentContextController?.updateTheme(presentationData: self.presentationData) } - private func updateNavigationBarPresentation() { + func updateNavigationBarPresentation() { let navigationBarTheme: NavigationBarTheme if self.hasEmbeddedTitleContent { @@ -6617,7 +6918,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, hasEmbeddedTitleContent: self.hasEmbeddedTitleContent) } - private func topPinnedMessageSignal(latest: Bool) -> Signal { + func topPinnedMessageSignal(latest: Bool) -> Signal { var pinnedPeerId: EnginePeer.Id? let threadId = self.chatLocation.threadId let loadState: Signal = self.chatDisplayNode.historyNode.historyState.get() @@ -6693,7 +6994,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal { let location: ChatHistoryLocation if let anchorMessageId = anchorMessageId { - location = .InitialSearch(location: .id(anchorMessageId), count: count, highlight: false) + location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false) } else { location = .Initial(count: count) } @@ -7278,7 +7579,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let topPinnedMessage: Signal if let subject = self.subject { switch subject { - case .forwardedMessages, .pinnedMessages, .scheduledMessages: + case .messageOptions, .pinnedMessages, .scheduledMessages: topPinnedMessage = .single(nil) default: topPinnedMessage = self.topPinnedMessageSignal(latest: false) @@ -7770,20 +8071,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut)) } - self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex, initial in - if let strongSelf = self, case let .message(index) = toIndex { + self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toSubject, initial in + if let strongSelf = self, case let .message(index) = toSubject.index { if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id { if messageId.peerId == index.id.peerId { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } } else if let controllerInteraction = strongSelf.controllerInteraction { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { - let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) + let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: initial) strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: []) - strongSelf.messageContextDisposable.set((Signal.complete() |> delay(0.7, queue: Queue.mainQueue())).startStrict(completed: { + var hasQuote = false + if let quote = toSubject.quote { + if message.text.contains(quote) { + hasQuote = true + } else { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Chat_ToastQuoteNotFound, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) + } + } + + strongSelf.messageContextDisposable.set((Signal.complete() |> delay(hasQuote ? 1.5 : 0.7, queue: Queue.mainQueue())).startStrict(completed: { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { if controllerInteraction.highlightedState == highlightedState { controllerInteraction.highlightedState = nil @@ -7792,9 +8102,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) - if let (messageId, maybeTimecode) = strongSelf.scheduledScrollToMessageId { + if let (messageId, params) = strongSelf.scheduledScrollToMessageId { strongSelf.scheduledScrollToMessageId = nil - if let timecode = maybeTimecode, message.id == messageId { + if let timecode = params.timestamp, message.id == messageId { Queue.mainQueue().after(0.2) { let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(timecode)) } @@ -7840,9 +8150,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { isScheduledMessages = false } - let duration: Double = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.animationDuration : 0.18 - let curve: ContainedViewLayoutTransitionCurve = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.verticalAnimationCurve : .easeInOut - let controlPoints: (Float, Float, Float, Float) = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.verticalAnimationControlPoints : (0.5, 0.33, 0.0, 0.0) + let duration: Double = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNodeImpl.animationDuration : 0.18 + let curve: ContainedViewLayoutTransitionCurve = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNodeImpl.verticalAnimationCurve : .easeInOut + let controlPoints: (Float, Float, Float, Float) = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNodeImpl.verticalAnimationControlPoints : (0.5, 0.33, 0.0, 0.0) let shouldUseFastMessageSendAnimation = strongSelf.chatDisplayNode.shouldUseFastMessageSendAnimation @@ -7907,7 +8217,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var correlationIds: [Int64] = [] for message in messages { switch message { - case let .message(_, _, _, _, _, _, _, correlationId, _): + case let .message(_, _, _, _, _, _, _, _, correlationId, _): if let correlationId = correlationId { correlationIds.append(correlationId) } @@ -8092,18 +8402,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.dismissUrlPreview = { [weak self] in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.interfaceState.editMessage { - if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { - $0.withUpdatedEditMessage($0.editMessage.flatMap { $0.withUpdatedDisableUrlPreview(link) }) + if let link = strongSelf.presentationInterfaceState.editingUrlPreview?.url { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in + return presentationInterfaceState.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEditMessage(interfaceState.editMessage.flatMap { editMessage in + var editMessage = editMessage + if !editMessage.disableUrlPreviews.contains(link) { + editMessage.disableUrlPreviews.append(link) + } + return editMessage + }) } }) } } else { - if let (link, _) = strongSelf.presentationInterfaceState.urlPreview { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { - $0.withUpdatedComposeDisableUrlPreview(link) + if let link = strongSelf.presentationInterfaceState.urlPreview?.url { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in + return presentationInterfaceState.updatedInterfaceState { interfaceState in + var composeDisableUrlPreviews = interfaceState.composeDisableUrlPreviews + if !composeDisableUrlPreviews.contains(link) { + composeDisableUrlPreviews.append(link) + } + return interfaceState.withUpdatedComposeDisableUrlPreviews(composeDisableUrlPreviews) } }) } @@ -8114,7 +8434,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { if let messageId = strongSelf.historyNavigationStack.removeLast() { - strongSelf.navigateToMessage(from: nil, to: .id(messageId.id, nil), rememberInStack: false) + strongSelf.navigateToMessage(from: nil, to: .id(messageId.id, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false) } else { if case .known = strongSelf.chatDisplayNode.historyNode.visibleContentOffset() { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -8137,7 +8457,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch result { case let .result(messageId): if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil)) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil))) } case .loading: break @@ -8195,7 +8515,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .result(messageId): if let messageId = messageId { strongSelf.chatDisplayNode.historyNode.suspendReadingReactions = true - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), scrollPosition: .center(.top), completion: { + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), scrollPosition: .center(.top), completion: { guard let strongSelf = self else { return } @@ -8445,20 +8765,38 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if canSendMessagesToChat(strongSelf.presentationInterfaceState) { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(message.id) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ + $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: message.id, + quote: nil + )) + }).updatedSearch(nil).updatedShowCommands(false) }, completion: { t in + completion(t, {}) + }) strongSelf.updateItemNodesSearchTextHighlightStates() strongSelf.chatDisplayNode.ensureInputViewFocused() } else { - completion(.immediate) + completion(.immediate, {}) } }, alertAction: { - completion(.immediate) + completion(.immediate, {}) }, delay: true) } else { - completion(.immediate) + let replySubject = ChatInterfaceState.ReplyMessageSubject( + messageId: messageId, + quote: nil + ) + completion(.immediate, { + guard let self else { + return + } + moveReplyMessageToAnotherChat(selfController: self, replySubject: replySubject) + }) } } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(nil) }) }, completion: completion) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: { t in + completion(t, {}) + }) } }, setupEditMessage: { [weak self] messageId, completion in if let strongSelf = self, strongSelf.isNodeLoaded { @@ -8477,29 +8815,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - var updated = state.updatedInterfaceState { - var entities: [MessageTextEntity] = [] - for attribute in message.attributes { - if let attribute = attribute as? TextEntitiesMessageAttribute { - entities = attribute.entities - break - } - } - var inputTextMaxLength: Int32 = 4096 - var webpageUrl: String? - for media in message.media { - if media is TelegramMediaImage || media is TelegramMediaFile { - inputTextMaxLength = strongSelf.context.userLimits.maxCaptionLength - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - webpageUrl = content.url - } + var entities: [MessageTextEntity] = [] + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute.entities + break } - let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities) - var disableUrlPreview: String? - if let detectedWebpageUrl = detectUrl(inputText), webpageUrl == nil { - disableUrlPreview = detectedWebpageUrl + } + var inputTextMaxLength: Int32 = 4096 + var webpageUrl: String? + for media in message.media { + if media is TelegramMediaImage || media is TelegramMediaFile { + inputTextMaxLength = strongSelf.context.userLimits.maxCaptionLength + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + webpageUrl = content.url } - return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreview: disableUrlPreview, inputTextMaxLength: inputTextMaxLength)) + } + + let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities) + var disableUrlPreviews: [String] = [] + if webpageUrl == nil { + disableUrlPreviews = detectUrls(inputText) + } + + var updated = state.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength)) } updated = updatedChatEditInterfaceMessageState(state: updated, message: message) @@ -8723,213 +9063,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState(f($0.forwardOptionsState ?? ChatInterfaceForwardOptionsState(hideNames: false, hideCaptions: false, unhideNamesOnCaptionChange: false))) }) }) } }, presentForwardOptions: { [weak self] sourceNode in - if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId { - let presentationData = strongSelf.presentationData - - let forwardOptions = strongSelf.presentationInterfaceStatePromise.get() - |> map { state -> ChatControllerSubject.ForwardOptions in - var hideNames = state.interfaceState.forwardOptionsState?.hideNames ?? false - if peerId.namespace == Namespaces.Peer.SecretChat { - hideNames = true - } - return ChatControllerSubject.ForwardOptions(hideNames: hideNames, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false) - } - |> distinctUntilChanged - - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .forwardedMessages(peerIds: [peerId], ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], options: forwardOptions), botStart: nil, mode: .standard(previewing: true)) - chatController.canReadHistory.set(false) - - let messageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] - let messagesCount: Signal - if let chatController = chatController as? ChatControllerImpl, messageIds.count > 1 { - messagesCount = .single(messageIds.count) - |> then( - chatController.presentationInterfaceStatePromise.get() - |> map { state -> Int in - return state.interfaceState.selectionState?.selectedIds.count ?? 1 - } - ) - } else { - messagesCount = .single(1) - } - - let accountPeerId = strongSelf.context.account.peerId - let items = combineLatest(forwardOptions, strongSelf.context.account.postbox.messagesAtIds(messageIds), messagesCount) - |> map { forwardOptions, messages, messagesCount -> [ContextMenuItem] in - var items: [ContextMenuItem] = [] - - var hasCaptions = false - var uniquePeerIds = Set() - - var hasOther = false - var hasNotOwnMessages = false - for message in messages { - if let author = message.effectiveAuthor { - if !uniquePeerIds.contains(author.id) { - uniquePeerIds.insert(author.id) - } - if message.id.peerId == accountPeerId && message.forwardInfo == nil { - } else { - hasNotOwnMessages = true - } - } - - var isDice = false - var isMusic = false - for media in message.media { - if let media = media as? TelegramMediaFile, media.isMusic { - isMusic = true - } else if media is TelegramMediaDice { - isDice = true - } else { - if !message.text.isEmpty { - if media is TelegramMediaImage || media is TelegramMediaFile { - hasCaptions = true - } - } - } - } - if !isDice && !isMusic { - hasOther = true - } - } - - var canHideNames = hasNotOwnMessages && hasOther - if case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { - canHideNames = false - } - let hideNames = forwardOptions.hideNames - let hideCaptions = forwardOptions.hideCaptions - - if canHideNames { - items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_ShowSendersName : presentationData.strings.Conversation_ForwardOptions_ShowSendersNames, icon: { theme in - if hideNames { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - } - }, action: { [weak self] _, f in - self?.interfaceInteraction?.updateForwardOptionsState({ current in - var updated = current - updated.hideNames = false - updated.hideCaptions = false - updated.unhideNamesOnCaptionChange = false - return updated - }) - }))) - - items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames, icon: { theme in - if hideNames { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - } else { - return nil - } - }, action: { _, f in - self?.interfaceInteraction?.updateForwardOptionsState({ current in - var updated = current - updated.hideNames = true - updated.unhideNamesOnCaptionChange = false - return updated - }) - }))) - - items.append(.separator) - } - - if hasCaptions { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowCaption, icon: { theme in - if hideCaptions { - return nil - } else { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - } - }, action: { [weak self] _, f in - self?.interfaceInteraction?.updateForwardOptionsState({ current in - var updated = current - updated.hideCaptions = false - if canHideNames { - if updated.unhideNamesOnCaptionChange { - updated.unhideNamesOnCaptionChange = false - updated.hideNames = false - } - } - return updated - }) - }))) - - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { theme in - if hideCaptions { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - } else { - return nil - } - }, action: { _, f in - self?.interfaceInteraction?.updateForwardOptionsState({ current in - var updated = current - updated.hideCaptions = true - if canHideNames { - if !updated.hideNames { - updated.hideNames = true - updated.unhideNamesOnCaptionChange = true - } - } - return updated - }) - }))) - - items.append(.separator) - } - - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ChangeRecipient, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in - self?.interfaceInteraction?.forwardCurrentForwardMessages() - - f(.default) - }))) - - items.append(.action(ContextMenuActionItem(text: messagesCount == 1 ? presentationData.strings.Conversation_ForwardOptions_SendMessage : presentationData.strings.Conversation_ForwardOptions_SendMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak chatController] c, f in - guard let strongSelf = self else { - return - } - if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds { - var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] - forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) } - strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) }) - } - - self?.controllerInteraction?.sendCurrentMessage(false) - - f(.default) - }))) - - return items - } - - strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - - strongSelf.canReadHistory.set(false) - - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) - contextController.dismissed = { [weak self] in - self?.canReadHistory.set(true) - } - contextController.dismissedForCancel = { [weak chatController] in - if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds { - var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] - forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) } - strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) }) - } - } - contextController.immediateItemsTransitionAnimation = true - strongSelf.presentInGlobalOverlay(contextController) - } - }, shareSelectedMessages: { [weak self] in - if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { - strongSelf.commitPurposefulAction() - let _ = (strongSelf.context.engine.data.get(EngineDataMap( - selectedIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) - )) - |> map { messages -> [EngineMessage] in - return messages.values.compactMap { $0 } + guard let self else { + return + } + presentChatForwardOptions(selfController: self, sourceNode: sourceNode) + }, presentReplyOptions: { [weak self] sourceNode in + guard let self else { + return + } + presentChatReplyOptions(selfController: self, sourceNode: sourceNode) + }, presentLinkOptions: { [weak self] sourceNode in + guard let self else { + return + } + presentChatLinkOptions(selfController: self, sourceNode: sourceNode) + }, shareSelectedMessages: { [weak self] in + if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { + strongSelf.commitPurposefulAction() + let _ = (strongSelf.context.engine.data.get(EngineDataMap( + selectedIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) + )) + |> map { messages -> [EngineMessage] in + return messages.values.compactMap { $0 } } |> deliverOnMainQueue).startStandalone(next: { messages in if let strongSelf = self, !messages.isEmpty { @@ -8987,15 +9142,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.openStickers(beginWithEmoji: false) strongSelf.mediaRecordingModeTooltipController?.dismissImmediately() }, editMessage: { [weak self] in - if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage { + guard let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage else { + return + } + + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId)) + |> deliverOnMainQueue).start(next: { [weak strongSelf] message in + guard let strongSelf, let message else { + return + } + var disableUrlPreview = false - if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { - if editMessage.disableUrlPreview == link { + + var webpage: TelegramMediaWebpage? + var webpagePreviewAttribute: WebpagePreviewMessageAttribute? + if let urlPreview = strongSelf.presentationInterfaceState.editingUrlPreview { + if editMessage.disableUrlPreviews.contains(urlPreview.url) { disableUrlPreview = true + } else { + webpage = urlPreview.webPage + webpagePreviewAttribute = WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true, isSafe: false) } } let text = trimChatInputText(convertMarkdownToAttributes(editMessage.inputState.inputText)) + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) var entitiesAttribute: TextEntitiesMessageAttribute? if !entities.isEmpty { @@ -9039,11 +9210,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } + if text.length == 0 { + if strongSelf.presentationInterfaceState.editMessageState?.mediaReference != nil { + } else if message.media.contains(where: { media in + switch media { + case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaMap: + return true + default: + return false + } + }) { + } else { + if strongSelf.recordingModeFeedback == nil { + strongSelf.recordingModeFeedback = HapticFeedback() + strongSelf.recordingModeFeedback?.prepareError() + } + strongSelf.recordingModeFeedback?.error() + return + } + } + var updatingMedia = false let media: RequestEditMessageMedia if let editMediaReference = strongSelf.presentationInterfaceState.editMessageState?.mediaReference { media = .update(editMediaReference) updatingMedia = true + } else if let webpage { + media = .update(.standalone(media: webpage)) } else { media = .keep } @@ -9054,8 +9247,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let currentMessage = currentMessage { let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? [] - if currentMessage.text != text.string || currentEntities != entities || updatingMedia || disableUrlPreview { - strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview) + let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false) + + if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview { + strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) } } @@ -9067,7 +9262,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } }) - } + }) }, beginMessageSearch: { [weak self] domain, query in guard let strongSelf = self else { return @@ -9219,7 +9414,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateItemNodesSearchTextHighlightStates() } }, navigateToMessage: { [weak self] messageId, dropStack, forceInCurrentChat, statusSubject in - self?.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject) + self?.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject) }, navigateToChat: { [weak self] peerId in guard let strongSelf = self else { return @@ -9277,13 +9472,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { messageText = command } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } }) } }, nil) @@ -9292,7 +9487,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: messageText, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: messageText, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) strongSelf.interfaceInteraction?.updateShowCommands { _ in return false } @@ -9336,12 +9531,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G bannedMediaInput = true } else if channel.hasBannedPermission(.banSendVoice) != nil { if !isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) return } } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { if isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) return } } @@ -9350,12 +9545,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G bannedMediaInput = true } else if group.hasBannedPermission(.banSendVoice) { if !isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) return } } else if group.hasBannedPermission(.banSendInstantVideos) { if isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) return } } @@ -9825,7 +10020,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) - contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) } return } else { @@ -9844,7 +10039,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) - contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) return } else { @@ -10479,113 +10674,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(slowmodeTooltipController) }, displaySendMessageOptions: { [weak self] node, gesture in - if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId, let textInputNode = strongSelf.chatDisplayNode.textInputNode(), let layout = strongSelf.validLayout { - let previousSupportedOrientations = strongSelf.supportedOrientations - if layout.size.width > layout.size.height { - strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .landscape) - } else { - strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - } - - let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).startStandalone() - - var hasEntityKeyboard = false - if case .media = strongSelf.presentationInterfaceState.inputMode { - hasEntityKeyboard = true - } - - let _ = (strongSelf.context.account.viewTracker.peerView(peerId) - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in - guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { - return - } - var sendWhenOnlineAvailable = false - if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime > until { - sendWhenOnlineAvailable = true - } - } - if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { - sendWhenOnlineAvailable = false - } - - if sendWhenOnlineAvailable { - let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).startStandalone() - } - - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, canSendWhenOnline: sendWhenOnlineAvailable, completion: { [weak self] in - if let strongSelf = self { - strongSelf.supportedOrientations = previousSupportedOrientations - } - }, sendMessage: { [weak self] mode in - if let strongSelf = self { - switch mode { - case .generic: - strongSelf.controllerInteraction?.sendCurrentMessage(false) - case .silently: - strongSelf.controllerInteraction?.sendCurrentMessage(true) - case .whenOnline: - strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp) { [weak self] in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } - }) - strongSelf.openScheduledMessages() - } - } - } - } - }, /* MARK: Nicegram TranslateEnteredMessage (translate + chooseLanguage) */ translate: { [weak self] in - guard let strongSelf = self else { return } - let chatId = strongSelf.chatLocation.peerId - let textToTranslate = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string - let _ = (translateEnteredText(text: textToTranslate, chatId: chatId, context: strongSelf.context) - |> deliverOnMainQueue).start( - next: { translated in - strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in - let newTextInputState = ChatTextInputState(inputText: NSAttributedString(string: translated)) - return state.updatedInterfaceState { interfaceState in - return interfaceState.withUpdatedEffectiveInputState(newTextInputState) - } - }) - }, error: { error in - let errorDescription: String - switch error { - case .toLanguageNotFound: - errorDescription = "Messages.TranslateError.ToLanguageNotFound" - case .translate: - errorDescription = "Messages.TranslateError" - } - let c = getIAPErrorController(context: strongSelf.context, errorDescription, strongSelf.presentationData) - strongSelf.controllerInteraction?.presentGlobalOverlayController(c, nil) - }) - }, chooseLanguage: { [weak self] in - guard let strongSelf = self else { return } - let chatId = strongSelf.chatLocation.peerId - let _ = (getLanguageCode(forChatWith: chatId, context: strongSelf.context) - |> deliverOnMainQueue).start(next: { code in - let c = languageListController(context: strongSelf.context, selectedLanguageCode: code, selectLanguage: { code in - setLanguageCode(code, forChatWith: chatId) - }) - c.navigationPresentation = .modal - strongSelf.push(c) - }) - }, schedule: { [weak self] in - if let strongSelf = self { - strongSelf.controllerInteraction?.scheduleCurrentMessage() - } - }) - controller.emojiViewProvider = strongSelf.chatDisplayNode.textInputPanelNode?.emojiViewProvider - strongSelf.sendMessageActionsController = controller - if layout.isNonExclusive { - strongSelf.present(controller, in: .window(.root)) - } else { - strongSelf.presentInGlobalOverlay(controller, with: nil) - } - }) + guard let self else { + return } + chatMessageDisplaySendMessageOptions(selfController: self, node: node, gesture: gesture) }, openScheduledMessages: { [weak self] in if let strongSelf = self { strongSelf.openScheduledMessages() @@ -10621,7 +10713,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } unarchiveAutomaticallyArchivedPeer(account: strongSelf.context.account, peerId: peerId) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_UnarchiveDone, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_UnarchiveDone, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) }, scrollToTop: { [weak self] in guard let strongSelf = self else { return @@ -10634,7 +10726,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let navigationController = strongSelf.effectiveNavigationController { - let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: .id($0), highlight: true, timecode: nil) } + let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) } strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadResult), subject: subject, keepStack: .always)) } }, activatePinnedListPreview: { [weak self] node, gesture in @@ -10697,7 +10789,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }, joinGroupCall: { [weak self] activeCall in guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { @@ -10714,7 +10806,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable) }, presentGigagroupHelp: { [weak self] in if let strongSelf = self { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } }, editMessageMedia: { [weak self] messageId, draw in if let strongSelf = self { @@ -10888,9 +10980,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) let range = textInputState.selectionRange - inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text) - let selectionPosition = range.lowerBound + (text.string as NSString).length + let updatedText = NSMutableAttributedString(attributedString: text) + if range.lowerBound < inputText.length { + if let quote = inputText.attribute(ChatTextInputAttributes.quote, at: range.lowerBound, effectiveRange: nil) { + updatedText.addAttribute(ChatTextInputAttributes.quote, value: quote, range: NSRange(location: 0, length: updatedText.length)) + } + } + inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: updatedText) + + let selectionPosition = range.lowerBound + (updatedText.string as NSString).length return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) } @@ -11015,7 +11114,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { return } - strongSelf.presentAttachmentPremiumGift() + strongSelf.presentAttachmentMenu(subject: .gift) Queue.mainQueue().after(0.5) { let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId).startStandalone() } @@ -11407,24 +11506,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.displayNodeDidLoad() } - private var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState? + var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState? - private func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) { + func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) { self.storedAnimateFromSnapshotState = snapshotState } override public func viewWillAppear(_ animated: Bool) { - #if DEBUG - if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { - os_signpost( - .begin, - log: SignpostData.impl.signpostLog, - name: "Appear", - signpostID: SignpostData.impl.signpostId - ) - } - #endif - super.viewWillAppear(animated) if self.willAppear { @@ -11497,20 +11585,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G // } - private var returnInputViewFocus = false + var returnInputViewFocus = false override public func viewDidAppear(_ animated: Bool) { - #if DEBUG - if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { - os_signpost( - .end, - log: SignpostData.impl.signpostLog, - name: "Appear", - signpostID: SignpostData.impl.signpostId - ) - } - #endif - super.viewDidAppear(animated) // MARK: Nicegram NGCard @@ -11887,7 +11964,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) let controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: { let context = strongSelf.context @@ -12027,8 +12104,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func saveInterfaceState(includeScrollState: Bool = true) { - if case .forwardedMessages = self.subject { + func saveInterfaceState(includeScrollState: Bool = true) { + if case .messageOptions = self.subject { return } @@ -12052,7 +12129,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage) if case .peer = self.chatLocation, let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { - interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageId(nil) + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil) } let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in return interfaceState @@ -12068,7 +12145,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus) } - private func canManagePin() -> Bool { + func canManagePin() -> Bool { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return false } @@ -12094,10 +12171,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return canManagePin } - private var suspendNavigationBarLayout: Bool = false - private var suspendedNavigationBarLayout: ContainerViewLayout? - private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 - private var additionalNavigationBarHitTestSlop: CGFloat = 0.0 + var suspendNavigationBarLayout: Bool = false + var suspendedNavigationBarLayout: ContainerViewLayout? + var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 + var additionalNavigationBarHitTestSlop: CGFloat = 0.0 override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { if self.suspendNavigationBarLayout { @@ -12107,6 +12184,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) } + override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { + return nil + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.suspendNavigationBarLayout = true super.containerLayoutUpdated(layout, transition: transition) @@ -12165,422 +12246,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, saveInterfaceState: saveInterfaceState, f, completion: completion) } - func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { - var completion = externalCompletion - var temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) - - if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup || self.presentationInterfaceState.keyboardButtonsMessage?.id != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.id { - if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let keyboardMarkup = keyboardButtonsMessage.visibleButtonKeyboardMarkup { - if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in - return .inputButtons(persistent: keyboardMarkup.flags.contains(.persistent)) - }) - } - - if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { - if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ value in - var value = value - value.processedSetupReplyMessageId = keyboardButtonsMessage.id - return value - }) }) - } - } - } else { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ mode in - if case .inputButtons = mode { - return .text - } else { - return mode - } - }) - } - } - - if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, keyboardButtonsMessage.requestsSetupReply { - if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ value in - var value = value - value.processedSetupReplyMessageId = keyboardButtonsMessage.id - return value - }) }) - } - } - - let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, context: self.context) - var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) - - let contextQueryUpdates = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: self.context, currentQueryStates: &self.contextQueryStates, requestBotLocationStatus: { [weak self] peerId in - guard let strongSelf = self else { - return - } - let _ = (ApplicationSpecificNotice.updateInlineBotLocationRequestState(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970 + 10 * 60)) - |> deliverOnMainQueue).startStandalone(next: { value in - guard let strongSelf = self, value else { - return - } - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() - })]), in: .window(.root)) - }) - }) - - for (kind, update) in contextQueryUpdates { - switch update { - case .remove: - if let (_, disposable) = self.contextQueryStates[kind] { - disposable.dispose() - self.contextQueryStates.removeValue(forKey: kind) - - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { _ in - return nil - }) - } - if case .contextRequest = kind { - self.performingInlineSearch.set(false) - } - case let .update(query, signal): - let currentQueryAndDisposable = self.contextQueryStates[kind] - currentQueryAndDisposable?.1.dispose() - - var inScope = true - var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? - self.contextQueryStates[kind] = (query, (signal - |> deliverOnMainQueue).startStrict(next: { [weak self] result in - if let strongSelf = self { - if Thread.isMainThread && inScope { - inScope = false - inScopeResult = result - } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInputQueryResult(queryKind: kind, { previousResult in - return result(previousResult) - }) - }) - } - } - }, error: { [weak self] error in - if let strongSelf = self { - if case .contextRequest = kind { - strongSelf.performingInlineSearch.set(false) - } - - switch error { - case .generic: - break - case let .inlineBotLocationRequest(peerId): - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).startStandalone() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() - })]), in: .window(.root)) - } - } - }, completed: { [weak self] in - if let strongSelf = self { - if case .contextRequest = kind { - strongSelf.performingInlineSearch.set(false) - } - } - })) - inScope = false - if let inScopeResult = inScopeResult { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in - return inScopeResult(previousResult) - }) - } else { - if case .contextRequest = kind { - self.performingInlineSearch.set(true) - } - } - - if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { - if case .contextRequest = query { - let _ = (ApplicationSpecificNotice.getSecretChatInlineBotUsage(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] value in - if let strongSelf = self, !value { - let _ = ApplicationSpecificNotice.setSecretChatInlineBotUsage(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone() - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_SecretChatContextBotAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - }) - } - } - } - } - - var isBot = false - if let peer = updatedChatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil { - isBot = true - } else { - isBot = false - } - self.chatDisplayNode.historyNode.chatHasBots = updatedChatPresentationInterfaceState.hasBots || isBot - - if let (updatedSearchQuerySuggestionState, updatedSearchQuerySuggestionSignal) = searchQuerySuggestionResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: context, currentQuery: self.searchQuerySuggestionState?.0) { - self.searchQuerySuggestionState?.1.dispose() - var inScope = true - var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? - self.searchQuerySuggestionState = (updatedSearchQuerySuggestionState, (updatedSearchQuerySuggestionSignal |> deliverOnMainQueue).startStrict(next: { [weak self] result in - if let strongSelf = self { - if Thread.isMainThread && inScope { - inScope = false - inScopeResult = result - } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedSearchQuerySuggestionResult { previousResult in - return result(previousResult) - } - }) - } - } - })) - inScope = false - if let inScopeResult = inScopeResult { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedSearchQuerySuggestionResult { previousResult in - return inScopeResult(previousResult) - } - } - } - - if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: self.context, currentQuery: self.urlPreviewQueryState?.0) { - self.urlPreviewQueryState?.1.dispose() - var inScope = true - var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? - let linkPreviews: Signal - if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { - linkPreviews = interactiveChatLinkPreviewsEnabled(accountManager: self.context.sharedContext.accountManager, displayAlert: { [weak self] f in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_SecretLinkPreviewAlert, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { - f.f(true) - }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: { - f.f(false) - })]), in: .window(.root)) - } - }) - } else { - var bannedEmbedLinks = false - if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banEmbedLinks) != nil { - bannedEmbedLinks = true - } else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banEmbedLinks) { - bannedEmbedLinks = true - } - if bannedEmbedLinks { - linkPreviews = .single(false) - } else { - linkPreviews = .single(true) - } - } - let filteredPreviewSignal = linkPreviews - |> take(1) - |> mapToSignal { value -> Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError> in - if value { - return updatedUrlPreviewSignal - } else { - return .single({ _ in return nil }) - } - } - - self.urlPreviewQueryState = (updatedUrlPreviewUrl, (filteredPreviewSignal |> deliverOnMainQueue).startStrict(next: { [weak self] (result) in - if let strongSelf = self { - if Thread.isMainThread && inScope { - inScope = false - inScopeResult = result - } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) { - return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) - } else { - return $0.updatedUrlPreview(nil) - } - }) - } - } - })) - inScope = false - if let inScopeResult = inScopeResult { - if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.1) { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) - } else { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil) - } - } - } - - let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext - let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText - if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: self.context, currentQuery: self.editingUrlPreviewQueryState?.0) { - self.editingUrlPreviewQueryState?.1.dispose() - var inScope = true - var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? - self.editingUrlPreviewQueryState = (updatedEditingUrlPreviewUrl, (updatedEditingUrlPreviewSignal |> deliverOnMainQueue).startStrict(next: { [weak self] result in - if let strongSelf = self { - if Thread.isMainThread && inScope { - inScope = false - inScopeResult = result - } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.1) { - return $0.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) - } else { - return $0.updatedEditingUrlPreview(nil) - } - }) - } - } - })) - inScope = false - if let inScopeResult = inScopeResult { - if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.1) { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) - } else { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil) - } - } - } - - if let updated = self.updateSearch(updatedChatPresentationInterfaceState) { - updatedChatPresentationInterfaceState = updated - } - - let recordingActivityValue: ChatRecordingActivity - if let mediaRecordingState = updatedChatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { - switch mediaRecordingState { - case .audio: - recordingActivityValue = .voice - case .video(ChatVideoRecordingStatus.recording, _): - recordingActivityValue = .instantVideo - default: - recordingActivityValue = .none - } - } else { - recordingActivityValue = .none - } - if recordingActivityValue != self.recordingActivityValue { - self.recordingActivityValue = recordingActivityValue - self.recordingActivityPromise.set(recordingActivityValue) - } - - if (self.presentationInterfaceState.interfaceState.selectionState == nil) != (updatedChatPresentationInterfaceState.interfaceState.selectionState == nil) { - self.isSelectingMessagesUpdated?(updatedChatPresentationInterfaceState.interfaceState.selectionState != nil) - self.updateNextChannelToReadVisibility() - } - - self.presentationInterfaceState = updatedChatPresentationInterfaceState - - self.updateSlowmodeStatus() - - switch updatedChatPresentationInterfaceState.inputMode { - case .media: - break - default: - self.chatDisplayNode.collapseInput() - } - - if self.isNodeLoaded { - self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, transition: transition, interactive: interactive, completion: completion) - } else { - completion(.immediate) - } - - let updatedServiceTasks = serviceTasksForChatPresentationIntefaceState(context: self.context, chatPresentationInterfaceState: updatedChatPresentationInterfaceState, updateState: { [weak self] f in - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, f) - - //strongSelf.chatDisplayNode.updateChatPresentationInterfaceState(f(strongSelf.chatDisplayNode.chatPresentationInterfaceState), transition: transition, interactive: false, completion: { _ in }) - }) - for (id, begin) in updatedServiceTasks { - if self.stateServiceTasks[id] == nil { - self.stateServiceTasks[id] = begin() - } - } - var removedServiceTaskIds: [AnyHashable] = [] - for (id, _) in self.stateServiceTasks { - if updatedServiceTasks[id] == nil { - removedServiceTaskIds.append(id) - } - } - for id in removedServiceTaskIds { - self.stateServiceTasks.removeValue(forKey: id)?.dispose() - } - - if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, subject: self.subject, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { - if self.leftNavigationButton != button { - var animated = transition.isAnimated - if let currentButton = self.leftNavigationButton?.action, currentButton == button.action { - animated = false - } - animated = false - self.navigationItem.setLeftBarButton(button.buttonItem, animated: animated) - self.leftNavigationButton = button - } - } else if let _ = self.leftNavigationButton { - self.navigationItem.setLeftBarButton(nil, animated: transition.isAnimated) - self.leftNavigationButton = nil - } - - if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton, moreInfoNavigationButton: self.moreInfoNavigationButton) { - if self.rightNavigationButton != button { - var animated = transition.isAnimated - if let currentButton = self.rightNavigationButton?.action, currentButton == button.action { - animated = false - } - if case .replyThread = self.chatLocation { - animated = false - } - self.navigationItem.setRightBarButton(button.buttonItem, animated: animated) - self.rightNavigationButton = button - } - } else if let _ = self.rightNavigationButton { - self.navigationItem.setRightBarButton(nil, animated: transition.isAnimated) - self.rightNavigationButton = nil - } - - if let controllerInteraction = self.controllerInteraction { - if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState { - controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState - let isBlackout = controllerInteraction.selectionState != nil - completion = { [weak self] transition in - completion(transition) - (self?.navigationController as? NavigationController)?.updateMasterDetailsBlackout(isBlackout ? .master : nil, transition: transition) - } - self.updateItemNodesSelectionStates(animated: transition.isAnimated) - } - } - - if saveInterfaceState { - self.saveInterfaceState(includeScrollState: false) - } - - if let navigationController = self.navigationController as? NavigationController, isTopmostChatController(self) { - var voiceChatOverlayController: VoiceChatOverlayController? - for controller in navigationController.globalOverlayControllers { - if let controller = controller as? VoiceChatOverlayController { - voiceChatOverlayController = controller - break - } - } - - if let controller = voiceChatOverlayController { - controller.updateVisibility() - } - } - - if let currentMenuWebAppController = self.currentMenuWebAppController, !self.presentationInterfaceState.showWebView { - self.currentMenuWebAppController = nil - if let currentMenuWebAppController = currentMenuWebAppController as? AttachmentController { - currentMenuWebAppController.ensureUnfocused = false - } - currentMenuWebAppController.dismiss(animated: true, completion: nil) - } - - self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) + func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { + updateChatPresentationInterfaceStateImpl( + selfController: self, + transition: transition, + interactive: interactive, + saveInterfaceState: saveInterfaceState, + f, + completion: completion + ) } - private func updateItemNodesSelectionStates(animated: Bool) { + func updateItemNodesSelectionStates(animated: Bool) { self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateSelectionState(animated: animated) @@ -12594,7 +12271,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func updatePollTooltipMessageState(animated: Bool) { + func updatePollTooltipMessageState(animated: Bool) { self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageBubbleItemNode { for contentNode in itemNode.contentNodes { @@ -12607,7 +12284,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func updateItemNodesSearchTextHighlightStates() { + func updateItemNodesSearchTextHighlightStates() { var searchString: String? var resultsMessageIndices: [MessageIndex]? if let search = self.presentationInterfaceState.search, let resultsState = search.resultsState, !resultsState.messageIndices.isEmpty { @@ -12628,7 +12305,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func updateItemNodesHighlightedStates(animated: Bool) { + func updateItemNodesHighlightedStates(animated: Bool) { self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateHighlightedState(animated: animated) @@ -12652,7 +12329,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - @objc private func moreButtonPressed() { + @objc func moreButtonPressed() { self.moreBarButton.play() self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) } @@ -12698,7 +12375,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.navigationButtonAction(.cancelMessageSelection) } - private func navigationButtonAction(_ action: ChatNavigationButtonAction) { + func navigationButtonAction(_ action: ChatNavigationButtonAction) { switch action { case .spacer, .toggleInfoPanel: break @@ -13173,7 +12850,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable.set((signal |> deliverOnMainQueue).startStrict(completed: { [weak self] in if let strongSelf = self, let _ = strongSelf.validLayout { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } })) @@ -13211,8 +12888,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { - if let message = messages.first, case let .message(text, attributes, _, maybeMediaReference, _, _, _, _, _) = message, let mediaReference = maybeMediaReference { + func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { + if let message = messages.first, case let .message(text, attributes, _, maybeMediaReference, _, _, _, _, _, _) = message, let mediaReference = maybeMediaReference { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var entities: [MessageTextEntity] = [] for attribute in attributes { @@ -13240,14 +12917,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func editMessageMediaWithLegacySignals(_ signals: [Any]) { + func editMessageMediaWithLegacySignals(_ signals: [Any]) { let _ = (legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals) |> deliverOnMainQueue).startStandalone(next: { [weak self] messages in self?.editMessageMediaWithMessages(messages.map { $0.message }) }) } - private func getCaptionPanelView() -> TGCaptionPanelView? { + func getCaptionPanelView() -> TGCaptionPanelView? { var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { isScheduledMessages = true @@ -13262,7 +12939,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) as? TGCaptionPanelView } - private func openCamera(cameraView: TGAttachmentCameraView? = nil) { + func openCamera(cameraView: TGAttachmentCameraView? = nil) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -13452,7 +13129,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if justInstalled { let content: UndoOverlayContent // if bot.flags.contains(.showInSettings) { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0) + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) // } else { // content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0) // } @@ -13522,18 +13199,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentAttachmentPremiumGift() { - self.presentAttachmentMenu(subject: .gift) - } - - private enum AttachMenuSubject { + enum AttachMenuSubject { case `default` case edit(mediaOptions: MessageMediaEditingOptions, mediaReference: AnyMediaReference) case bot(id: PeerId, payload: String?, justInstalled: Bool) case gift } - private func presentAttachmentMenu(subject: AttachMenuSubject) { + func presentAttachmentMenu(subject: AttachMenuSubject) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -13699,12 +13372,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let content: UndoOverlayContent if botJustInstalled { if bot.flags.contains(.showInSettings) { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0) + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) } else { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0) + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) } } else { - content = .info(title: nil, text: strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil) + content = .info(title: nil, text: strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil, customUndoText: nil) } strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) } else { @@ -13797,7 +13470,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { return } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message])) |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { @@ -13835,14 +13508,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -13853,7 +13526,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = currentLocationController.swap(controller) }) case .contact: - let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) + let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true, requirePhoneNumbers: true)) contactsController.presentScheduleTimePicker = { [weak self] completion in if let strongSelf = self { strongSelf.presentScheduleTimePicker(completion: completion) @@ -13871,7 +13544,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - textEnqueueMessage = .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + textEnqueueMessage = .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) } if peers.count > 1 { var enqueueMessages: [EnqueueMessage] = [] @@ -13900,17 +13573,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let media = media { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) enqueueMessages.append(message) } } @@ -13961,13 +13634,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if contactData.isPrimitive { let phone = contactData.basicData.phoneNumbers[0].value let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -13976,7 +13649,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) } else { let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in @@ -13986,13 +13659,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let phone = contactData.basicData.phoneNumbers[0].value if let vCardData = contactData.serializedVCard() { let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -14001,7 +13674,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) } }), completed: nil, cancelled: nil) @@ -14019,7 +13692,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .gift: let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in + let controller = PremiumGiftAttachmentScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in if let strongSelf = self { strongSelf.push(c) } @@ -14042,8 +13715,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G fromAttachMenu = false } let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageId, threadId: strongSelf.chatLocation.threadId) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) controller.openUrl = { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) } @@ -14053,7 +13726,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G controller.completion = { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } @@ -14080,9 +13753,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G Queue.mainQueue().after(0.3) { let content: UndoOverlayContent if bot.flags.contains(.showInSettings) { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0) + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) } else { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0) + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) } attachmentController.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) } @@ -14100,7 +13773,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { + func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -14198,9 +13871,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { - self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, present: { [weak self] c, _ in - self?.effectiveNavigationController?.pushViewController(c) - }, completion: { signals, silentPosting, scheduleTime in + self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in if !inputText.string.isEmpty { strongSelf.clearInputText() } @@ -14391,10 +14062,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentFileGallery(editingMessage: Bool = false) { - self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, present: { [weak self] c, _ in - self?.effectiveNavigationController?.pushViewController(c) - }, completion: { [weak self] signals, silentPosting, scheduleTime in + func presentFileGallery(editingMessage: Bool = false) { + self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { [weak self] signals, silentPosting, scheduleTime in if editingMessage { self?.editMessageMediaWithLegacySignals(signals) } else { @@ -14403,7 +14072,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentICloudFileGallery(editingMessage: Bool = false) { + func presentICloudFileGallery(editingMessage: Bool = false) { let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), @@ -14425,7 +14094,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals) |> deliverOnMainQueue).startStrict(next: { results in if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject for item in results { if let item = item { @@ -14485,7 +14154,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } if let _ = groupingKey, messages.count % 10 == 0 { @@ -14502,7 +14171,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -14516,7 +14185,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentFileMediaPickerOptions(editingMessage: Bool) { + func presentFileMediaPickerOptions(editingMessage: Bool) { let actionSheet = ActionSheetController(presentationData: self.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in @@ -14540,7 +14209,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(actionSheet, in: .window(.root)) } - private func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { + func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -14601,7 +14270,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G present(controller, mediaPickerContext) } - private func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) { + func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any], Bool, Int32) -> Void) { let engine = self.context.engine let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) @@ -14709,13 +14378,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } strongSelf.chatDisplayNode.dismissInput() - present(legacyController, LegacyAssetPickerContext(controller: controller)) + strongSelf.effectiveNavigationController?.pushViewController(legacyController) } }) }) } - private func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { + func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -14829,7 +14498,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentLocationPicker() { + func presentLocationPicker() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -14851,14 +14520,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -14869,7 +14538,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentContactPicker() { + func presentContactPicker() { let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) contactsController.navigationPresentation = .modal self.chatDisplayNode.dismissInput() @@ -14901,17 +14570,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let media = media { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) enqueueMessages.append(message) } } @@ -14962,17 +14631,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if contactData.isPrimitive { let phone = contactData.basicData.phoneNumbers[0].value let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.sendMessages([message]) } else { let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in @@ -14982,17 +14651,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let phone = contactData.basicData.phoneNumbers[0].value if let vCardData = contactData.serializedVCard() { let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.sendMessages([message]) } }), completed: nil, cancelled: nil) @@ -15005,7 +14674,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func displayPollSolution(solution: TelegramMediaPollResults.Solution, sourceNode: ASDisplayNode, isAutomatic: Bool) { + func displayPollSolution(solution: TelegramMediaPollResults.Solution, sourceNode: ASDisplayNode, isAutomatic: Bool) { var maybeFoundItemNode: ChatMessageItemView? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { @@ -15062,7 +14731,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .textMention(mention): switch action { case .tap: - strongSelf.controllerInteraction?.openPeerMention(mention) + strongSelf.controllerInteraction?.openPeerMention(mention, nil) case .longTap: strongSelf.controllerInteraction?.longTap(.mention(mention), nil) } @@ -15155,7 +14824,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .textMention(mention): switch action { case .tap: - strongSelf.controllerInteraction?.openPeerMention(mention) + strongSelf.controllerInteraction?.openPeerMention(mention, nil) case .longTap: strongSelf.controllerInteraction?.longTap(.mention(mention), nil) } @@ -15186,7 +14855,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(tooltipScreen, in: .current) } - private func displayPsa(type: String, sourceNode: ASDisplayNode, isAutomatic: Bool) { + func displayPsa(type: String, sourceNode: ASDisplayNode, isAutomatic: Bool) { var maybeFoundItemNode: ChatMessageItemView? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { @@ -15269,7 +14938,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .textMention(mention): switch action { case .tap: - strongSelf.controllerInteraction?.openPeerMention(mention) + strongSelf.controllerInteraction?.openPeerMention(mention, nil) case .longTap: strongSelf.controllerInteraction?.longTap(.mention(mention), nil) } @@ -15313,7 +14982,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(tooltipScreen, in: .current) } - private func configurePollCreation(isQuiz: Bool? = nil) -> CreatePollControllerImpl? { + func configurePollCreation(isQuiz: Bool? = nil) -> CreatePollControllerImpl? { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return nil } @@ -15321,13 +14990,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -15346,13 +15015,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isClosed: false, deadlineTimeout: poll.deadlineTimeout )), + threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [] ) - strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)]) + strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) }) } @@ -15361,7 +15031,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return transformEnqueueMessages(messages, silentPosting: silentPosting) } - @discardableResult private func dismissAllUndoControllers() -> UndoOverlayController? { + @discardableResult func dismissAllUndoControllers() -> UndoOverlayController? { var currentOverlayController: UndoOverlayController? self.window?.forEachController({ controller in @@ -15379,7 +15049,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return currentOverlayController } - private func displayPremiumStickerTooltip(file: TelegramMediaFile, message: Message) { + func displayPremiumStickerTooltip(file: TelegramMediaFile, message: Message) { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) guard !premiumConfiguration.isPremiumDisabled else { return @@ -15417,7 +15087,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func displayEmojiPackTooltip(file: TelegramMediaFile, message: Message) { + func displayEmojiPackTooltip(file: TelegramMediaFile, message: Message) { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) guard !premiumConfiguration.isPremiumDisabled else { return @@ -15469,7 +15139,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func displayDiceTooltip(dice: TelegramMediaDice) { + func displayDiceTooltip(dice: TelegramMediaDice) { guard let _ = dice.value else { return } @@ -15514,20 +15184,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let value = value { self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, context: self.context, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState), action == .undo { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } return false }), in: .current) } } - private func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { - var defaultReplyMessageId: MessageId? + func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { + var defaultReplyMessageSubject: EngineMessageReplySubject? switch self.chatLocation { case .peer: break case let .replyThread(replyThreadMessage): - defaultReplyMessageId = replyThreadMessage.messageId + defaultReplyMessageSubject = EngineMessageReplySubject(messageId: replyThreadMessage.messageId, quote: nil) case .feed: break } @@ -15535,11 +15205,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return messages.map { message in var message = message - if let defaultReplyMessageId = defaultReplyMessageId { + if let defaultReplyMessageSubject = defaultReplyMessageSubject { switch message { - case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): if replyToMessageId == nil { - message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: defaultReplyMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: defaultReplyMessageSubject, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) } case .forward: break @@ -15573,7 +15243,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { + func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { guard let peerId = self.chatLocation.peerId else { return } @@ -15605,7 +15275,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { + func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!) |> deliverOnMainQueue).startStrict(next: { [weak self] items in if let strongSelf = self { @@ -15663,15 +15333,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if addedTransitions.count > 1 { - var transitions: [(Int64, ChatMessageTransitionNode.Source, () -> Void)] = [] + var transitions: [(Int64, ChatMessageTransitionNodeImpl.Source, () -> Void)] = [] for (correlationId, uniqueIds, initiated) in addedTransitions { - var source: ChatMessageTransitionNode.Source? + var source: ChatMessageTransitionNodeImpl.Source? if uniqueIds.count > 1 { - source = .groupedMediaInput(ChatMessageTransitionNode.Source.GroupedMediaInput(extractSnapshots: { + source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) })) } else if let uniqueId = uniqueIds.first { - source = .mediaInput(ChatMessageTransitionNode.Source.MediaInput(extractSnapshot: { + source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: { return getAnimatedTransitionSource?(uniqueId) })) } @@ -15681,13 +15351,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.chatDisplayNode.messageTransitionNode.add(grouped: transitions) } else if let (correlationId, uniqueIds, initiated) = addedTransitions.first { - var source: ChatMessageTransitionNode.Source? + var source: ChatMessageTransitionNodeImpl.Source? if uniqueIds.count > 1 { - source = .groupedMediaInput(ChatMessageTransitionNode.Source.GroupedMediaInput(extractSnapshots: { + source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) })) } else if let uniqueId = uniqueIds.first { - source = .mediaInput(ChatMessageTransitionNode.Source.MediaInput(extractSnapshot: { + source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: { return getAnimatedTransitionSource?(uniqueId) })) } @@ -15699,19 +15369,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } completionImpl?() }, usedCorrelationId) - strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }, media: true) + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }, media: true) if let _ = scheduleTime { completion() @@ -15720,7 +15390,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func displayPasteMenu(_ subjects: [MediaPickerScreen.Subject.Media]) { + func displayPasteMenu(_ subjects: [MediaPickerScreen.Subject.Media]) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -15748,43 +15418,43 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func enqueueGifData(_ data: Data) { + func enqueueGifData(_ data: Data) { self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) + strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) } })) } - private func enqueueVideoData(_ data: Data) { + func enqueueVideoData(_ data: Data) { self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) + strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) } })) } - private func enqueueStickerImage(_ image: UIImage, isMemoji: Bool) { + func enqueueStickerImage(_ image: UIImage, isMemoji: Bool) { let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0)) self.enqueueMediaMessageDisposable.set((convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).startStrict(next: { [weak self] data in if let strongSelf = self, !data.isEmpty { @@ -15797,24 +15467,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G fileAttributes.append(.ImageSize(size: PixelDimensions(size))) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) + strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) } })) } - private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { + func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { if !canSendMessagesToChat(self.presentationInterfaceState) { return } @@ -15832,8 +15502,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId - if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, threadId: self.chatLocation.threadId, botId: results.botId, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { + let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, threadId: self.chatLocation.threadId, botId: results.botId, result: result, replyToMessageId: replyMessageSubject?.subjectModel, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() @@ -15843,9 +15513,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if resetTextInputState { state = state.updatedInterfaceState { interfaceState in var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageId(nil) + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil) interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) - interfaceState = interfaceState.withUpdatedComposeDisableUrlPreview(nil) + interfaceState = interfaceState.withUpdatedComposeDisableUrlPreviews([]) return interfaceState } } @@ -15871,7 +15541,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func firstLoadedMessageToListen() -> Message? { + func firstLoadedMessageToListen() -> Message? { var messageToListen: Message? self.chatDisplayNode.historyNode.forEachMessageInCurrentHistoryView { message in if message.flags.contains(.Incoming) && message.tags.contains(.voiceOrInstantVideo) { @@ -15887,9 +15557,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return messageToListen } - private var raiseToListenActivateRecordingTimer: SwiftSignalKit.Timer? + var raiseToListenActivateRecordingTimer: SwiftSignalKit.Timer? - private func activateRaiseGesture() { + func activateRaiseGesture() { self.raiseToListenActivateRecordingTimer?.invalidate() self.raiseToListenActivateRecordingTimer = nil if let messageToListen = self.firstLoadedMessageToListen() { @@ -15903,13 +15573,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func deactivateRaiseGesture() { + func deactivateRaiseGesture() { self.raiseToListenActivateRecordingTimer?.invalidate() self.raiseToListenActivateRecordingTimer = nil self.dismissMediaRecorder(.preview) } - private func requestAudioRecorder(beginWithTone: Bool) { + func requestAudioRecorder(beginWithTone: Bool) { if self.audioRecorderValue == nil { if self.recorderFeedback == nil { self.recorderFeedback = HapticFeedback() @@ -15921,7 +15591,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func requestVideoRecorder() { + func requestVideoRecorder() { guard let peerId = self.chatLocation.peerId else { return } @@ -15945,17 +15615,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let correlationId = Int64.random(in: 0 ..< Int64.max) let updatedMessage = message - .withUpdatedReplyToMessageId(replyMessageId) + .withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) .withUpdatedCorrelationId(correlationId) var usedCorrelationId = false if strongSelf.chatDisplayNode.shouldAnimateMessageTransition, let extractedView = videoController.extractVideoSnapshot() { usedCorrelationId = true - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .videoMessage(ChatMessageTransitionNode.Source.VideoMessage(view: extractedView)), initiated: { [weak videoController] in + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .videoMessage(ChatMessageTransitionNodeImpl.Source.VideoMessage(view: extractedView)), initiated: { [weak videoController] in videoController?.hideVideoSnapshot() guard let strongSelf = self else { return @@ -15971,7 +15641,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, usedCorrelationId ? correlationId : nil) @@ -15996,7 +15666,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func dismissMediaRecorder(_ action: ChatFinishMediaRecordingAction) { + func dismissMediaRecorder(_ action: ChatFinishMediaRecordingAction) { var updatedAction = action var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { @@ -16067,7 +15737,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if strongSelf.chatDisplayNode.shouldAnimateMessageTransition, let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode, let micButton = textInputPanelNode.micButton { usedCorrelationId = true - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .audioMicInput(ChatMessageTransitionNode.Source.AudioMicInput(micButton: micButton)), initiated: { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .audioMicInput(ChatMessageTransitionNodeImpl.Source.AudioMicInput(micButton: micButton)), initiated: { guard let strongSelf = self else { return } @@ -16082,12 +15752,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, usedCorrelationId ? correlationId : nil) - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil @@ -16116,7 +15786,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func stopMediaRecorder() { + func stopMediaRecorder() { if let audioRecorderValue = self.audioRecorderValue { if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { self.dismissMediaRecorder(.preview) @@ -16137,7 +15807,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func lockMediaRecorder() { + func lockMediaRecorder() { if self.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedInputTextPanelState { panelState in @@ -16149,14 +15819,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.videoRecorderValue?.lockVideo() } - private func deleteMediaRecording() { + func deleteMediaRecording() { self.chatDisplayNode.updateRecordedMediaDeleted(true) self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedRecordedMediaPreview(nil) }) } - private func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil) { + func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil) { self.chatDisplayNode.updateRecordedMediaDeleted(false) if let recordedMediaPreview = self.presentationInterfaceState.recordedMediaPreview { @@ -16179,12 +15849,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { @@ -16210,7 +15880,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func updateSearch(_ interfaceState: ChatPresentationInterfaceState) -> ChatPresentationInterfaceState? { + func updateSearch(_ interfaceState: ChatPresentationInterfaceState) -> ChatPresentationInterfaceState? { guard let peerId = self.chatLocation.peerId else { return nil } @@ -16374,7 +16044,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func scrollToEndOfHistory() { - let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0) let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView @@ -16438,7 +16108,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func scrollToStartOfHistory() { - let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0) let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView @@ -16526,7 +16196,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func openCalendarSearch(timestamp: Int32) { + func openCalendarSearch(timestamp: Int32) { guard let peerId = self.chatLocation.peerId else { return } @@ -16573,7 +16243,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.loadingMessage.set(.single(nil)) if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: true) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true) } } })) @@ -16610,7 +16280,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.loadingMessage.set(.single(nil)) if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: true) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true) } } })) @@ -16634,12 +16304,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }))) } - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } ) @@ -16694,7 +16364,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func openMessageReplies(messageId: MessageId, displayProgressInMessage: MessageId?, isChannelPost: Bool, atMessage atMessageId: MessageId?, displayModalProgress: Bool) { + func openMessageReplies(messageId: MessageId, displayProgressInMessage: MessageId?, isChannelPost: Bool, atMessage atMessageId: MessageId?, displayModalProgress: Bool) { guard let navigationController = self.effectiveNavigationController else { return } @@ -16767,9 +16437,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let subject: ChatControllerSubject? if let atMessageId = atMessageId { - subject = .message(id: .id(atMessageId), highlight: true, timecode: nil) + subject = .message(id: .id(atMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) } else if let index = result.scrollToLowerBoundMessage { - subject = .message(id: .id(index.id), highlight: false, timecode: nil) + subject = .message(id: .id(index.id), highlight: nil, timecode: nil) } else { subject = nil } @@ -16804,7 +16474,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.navigateToMessage(from: nil, to: messageLocation, scrollPosition: scrollPosition, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, animated: animated, completion: completion, customPresentProgress: customPresentProgress) } - private func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, forceInCurrentChat: Bool = false, dropStack: Bool = false, animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil, statusSubject: ChatLoadingMessageSubject = .generic) { + func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, forceInCurrentChat: Bool = false, dropStack: Bool = false, animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil, statusSubject: ChatLoadingMessageSubject = .generic) { if self.isNodeLoaded { var fromIndex: MessageIndex? @@ -16833,6 +16503,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (combineLatest( self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)), self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local) + |> mapToSignal { result -> Signal<[Message], NoError> in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } ) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, messages in guard let self, let peer = peer else { @@ -16850,7 +16526,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { navigateToLocation = .peer(peer) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always)) }) } else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) { let _ = (self.context.engine.data.get( @@ -16867,7 +16543,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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)) } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always)) + var quote: String? + if case let .id(_, params) = messageLocation { + quote = params.quote + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always)) } }) } else if forceInCurrentChat { @@ -16894,7 +16575,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G delayCompletion = false } - self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition) + var quote: String? + if case let .id(_, params) = messageLocation { + quote = params.quote + } + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) if delayCompletion { Queue.mainQueue().after(0.25, { @@ -16906,16 +16591,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - if case let .id(_, maybeTimecode) = messageLocation, let timecode = maybeTimecode { + if case let .id(_, params) = messageLocation, let timecode = params.timestamp { let _ = self.controllerInteraction?.openMessage(message, .timecode(timecode)) } } else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject { self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) } else { - if case let .id(messageId, maybeTimecode) = messageLocation, let timecode = maybeTimecode { - self.scheduledScrollToMessageId = (messageId, timecode) + if case let .id(messageId, params) = messageLocation, params.timestamp != nil { + self.scheduledScrollToMessageId = (messageId, params) + } + var progress: Promise? + if case let .id(_, params) = messageLocation { + progress = params.progress } self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) + let searchLocation: ChatHistoryInitialSearchLocation switch messageLocation { case let .id(id, _): @@ -16929,9 +16619,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G searchLocation = .index(.absoluteUpperBound()) } } - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + var historyView: Signal + historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) - let signal = historyView + var signal: Signal<(MessageIndex?, Bool), NoError> + signal = historyView |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in switch historyView { case .Loading: @@ -16952,11 +16644,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return SignalTakeAction(passthrough: true, complete: !index.1) }) + /*#if DEBUG + signal = .single((nil, true)) |> then(signal |> delay(2.0, queue: .mainQueue())) + #endif*/ + var cancelImpl: (() -> Void)? let presentationData = self.presentationData let displayTime = CACurrentMediaTime() let progressSignal = Signal { [weak self] subscriber in - if case .generic = statusSubject { + if let progress { + progress.set(.single(true)) + return ActionDisposable { + Queue.mainQueue().async() { + progress.set(.single(false)) + } + } + } else if case .generic = statusSubject { let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { if CACurrentMediaTime() - displayTime > 1.5 { cancelImpl?() @@ -17026,22 +16729,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.historyNavigationStack.add(fromIndex) } self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) - let signal = historyView - |> mapToSignal { historyView -> Signal in - switch historyView { - case .Loading: - return .complete() - case let .HistoryView(view, _, _, _, _, _, _): - for entry in view.entries { - if entry.message.id == messageLocation.messageId { - return .single(entry.message.index) - } + + var quote: String? + if case let .id(_, params) = messageLocation { + quote = params.quote + } + + let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + var signal: Signal + signal = historyView + |> mapToSignal { historyView -> Signal in + switch historyView { + case .Loading: + return .complete() + case let .HistoryView(view, _, _, _, _, _, _): + for entry in view.entries { + if entry.message.id == messageLocation.messageId { + return .single(entry.message.index) } - return .single(nil) - } + } + return .single(nil) } - |> take(1) + } + |> take(1) + self.messageIndexDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { [weak self] index in if let strongSelf = self { if let index = index { @@ -17055,7 +16766,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let navigationController = strongSelf.effectiveNavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) })) + var quote: String? + if case let .id(_, params) = messageLocation { + quote = params.quote + } + + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil) })) } }) completion?() @@ -17073,7 +16789,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let navigationController = self.effectiveNavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) })) } completion?() }) @@ -17085,18 +16801,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } // MARK: Nicegram (cloud + asCopy) - private func forwardMessages(messageIds: [MessageId], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool = false, cloud: Bool = false, asCopy: Bool = false) { + func forwardMessages(messageIds: [MessageId], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool = false, cloud: Bool = false, asCopy: Bool = false) { let _ = (self.context.engine.data.get(EngineDataMap( messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) )) |> deliverOnMainQueue).startStandalone(next: { [weak self] messages in + let sortedMessages = messages.values.compactMap { $0?._asMessage() }.sorted { lhs, rhs in + return lhs.id < rhs.id + } // MARK: Nicegram (cloud + asCopy) - self?.forwardMessages(messages: messages.values.compactMap { $0?._asMessage() }, options: options, resetCurrent: resetCurrent, cloud: cloud, asCopy: asCopy) + self?.forwardMessages(messages: sortedMessages, options: options, resetCurrent: resetCurrent, cloud: cloud, asCopy: asCopy) }) } // MARK: Nicegram (cloud + asCopy) - private func forwardMessages(messages: [Message], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool, cloud: Bool = false, asCopy: Bool = false) { + func forwardMessages(messages: [Message], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool, cloud: Bool = false, asCopy: Bool = false) { let _ = self.presentVoiceMessageDiscardAlert(action: { // MARK: Nicegram @@ -17195,7 +16914,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } } } @@ -17472,7 +17191,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } // - private func openPeer(peer: EnginePeer?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) { + func openPeer(peer: EnginePeer?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) { let _ = self.presentVoiceMessageDiscardAlert(action: { if case let .peer(currentPeerId) = self.chatLocation, peer?.id == currentPeerId { switch navigation { @@ -17487,6 +17206,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .text }) }) + } else { + self.playShakeAnimation() } case let .withBotStartPayload(botStart): self.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -17624,13 +17345,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNode?, avatarNode: AvatarNode?) { + func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNodeImpl?, avatarNode: AvatarNode?) { if let avatarNode = avatarHeaderNode?.avatarNode ?? avatarNode { StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: avatarNode) } } - private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) { + func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil, progress: Promise? = nil) { let _ = self.presentVoiceMessageDiscardAlert(action: { let disposable: MetaDisposable if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable { @@ -17644,13 +17365,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var cancelImpl: (() -> Void)? let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - self?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() + if progress != nil { + return ActionDisposable { + } + } else { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } } } } @@ -17668,29 +17394,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.resolvePeerByNameDisposable?.set(nil) } disposable.set((resolveSignal - |> take(1) - |> mapToSignal { peer -> Signal in - return .single(peer?._asPeer()) - } - |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self { - if let peer = peer { + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .progress: + progress?.set(.single(true)) + case let .result(peer): + progress?.set(.single(false)) + + if let peer { var navigation = navigation if case .default = navigation { - if let peer = peer as? TelegramUser, peer.botInfo != nil { + if case let .user(user) = peer, user.botInfo != nil { navigation = .chat(textInputState: nil, subject: nil, peekData: nil) } } - strongSelf.openResolved(result: .peer(peer, navigation), sourceMessageId: sourceMessageId) + self.openResolved(result: .peer(peer._asPeer(), navigation), sourceMessageId: sourceMessageId) } else { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) }) } - private func openHashtag(_ hashtag: String, peerName: String?) { + func openHashtag(_ hashtag: String, peerName: String?) { guard let peerId = self.chatLocation.peerId else { return } @@ -17701,6 +17431,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var resolveSignal: Signal if let peerName = peerName { resolveSignal = self.context.engine.peers.resolvePeerByName(name: peerName) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in if let peer = peer { return .single(peer._asPeer()) @@ -17748,7 +17484,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func unblockPeer() { + func unblockPeer() { guard case let .peer(peerId) = self.chatLocation else { return } @@ -17770,7 +17506,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })).startStrict()) } - private func reportPeer() { + func reportPeer() { guard let renderedPeer = self.presentationInterfaceState.renderedPeer, let peer = renderedPeer.chatMainPeer, let chatPeer = renderedPeer.peer else { return } @@ -17886,7 +17622,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func shareAccountContact() { + func shareAccountContact() { let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) |> deliverOnMainQueue).startStandalone(next: { [weak self] accountPeer in guard let strongSelf = self else { @@ -17931,7 +17667,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func addPeerContact() { + func addPeerContact() { if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser, let peerStatusSettings = self.presentationInterfaceState.contactStatus?.peerStatusSettings, let contactData = DeviceContactExtendedData(peer: EnginePeer(peer)) { self.present(context.sharedContext.makeDeviceContactInfoController(context: context, subject: .create(peer: peer, contactData: contactData, isSharing: true, shareViaException: peerStatusSettings.contains(.addExceptionWhenAddingContact), completion: { [weak self] peer, stableId, contactData in guard let strongSelf = self else { @@ -17947,7 +17683,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func dismissPeerContactOptions() { + func dismissPeerContactOptions() { guard case let .peer(peerId) = self.chatLocation else { return } @@ -17964,7 +17700,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })).startStrict()) } - private func deleteChat(reportChatSpam: Bool) { + func deleteChat(reportChatSpam: Bool) { guard case let .peer(peerId) = self.chatLocation else { return } @@ -17976,7 +17712,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: true).startStandalone() } - private func startBot(_ payload: String?) { + func startBot(_ payload: String?) { guard case let .peer(peerId) = self.chatLocation else { return } @@ -17992,7 +17728,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func openResolved(result: ResolvedUrl, sourceMessageId: MessageId?, forceExternal: Bool = false, concealed: Bool = false, commit: @escaping () -> Void = {}) { + func openResolved(result: ResolvedUrl, sourceMessageId: MessageId?, forceExternal: Bool = false, concealed: Bool = false, commit: @escaping () -> Void = {}) { guard let peerId = self.chatLocation.peerId else { return } @@ -18002,13 +17738,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let dismissWebAppContollers: () -> Void = { - if let currentWebAppController = strongSelf.currentWebAppController { - strongSelf.currentWebAppController = nil - currentWebAppController.dismiss(animated: true, completion: nil) - } else if let currentWebAppController = strongSelf.currentMenuWebAppController { - strongSelf.currentMenuWebAppController = nil - currentWebAppController.dismiss(animated: true, completion: nil) - } +// if let currentWebAppController = strongSelf.currentWebAppController { +// strongSelf.currentWebAppController = nil +// currentWebAppController.dismiss(animated: true, completion: nil) +// } else if let currentWebAppController = strongSelf.currentMenuWebAppController { +// strongSelf.currentMenuWebAppController = nil +// currentWebAppController.dismiss(animated: true, completion: nil) +// } } switch navigation { @@ -18017,7 +17753,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .peer(peerId.id) = strongSelf.chatLocation { if let subject = subject, case let .message(messageSubject, _, timecode) = subject { if case let .id(messageId) = messageSubject { - strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, timecode)) + strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil))) } } else { self?.playShakeAnimation() @@ -18066,7 +17802,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G commit() case let .withBotApp(botAppStart): let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) - |> deliverOnMainQueue).startStrict(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in if let strongSelf = self, let peer { strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, concealed: concealed, commit: { dismissWebAppContollers() @@ -18096,19 +17832,45 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, contentContext: nil) } - private func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, commit: @escaping () -> Void = {}) { + func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, allowInlineWebpageResolution: Bool = false, progress: Promise? = nil, commit: @escaping () -> Void = {}) { self.commitPurposefulAction() - let _ = self.presentVoiceMessageDiscardAlert(action: { - openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in + if allowInlineWebpageResolution, let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url { + if content.instantPage != nil { + if let navigationController = self.navigationController as? NavigationController { + switch instantPageType(of: content) { + case .album: + break + default: + progress?.set(.single(false)) + self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController) + return + } + } + } else if content.file == nil, (content.image == nil || content.isMediaLargeByDefault == true || content.isMediaLargeByDefault == nil), let embedUrl = content.embedUrl, !embedUrl.isEmpty { + progress?.set(.single(false)) + if let controllerInteraction = self.controllerInteraction { + if controllerInteraction.openMessage(message, .default) { + return + } + } + } + } + + let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in + guard let self else { + return + } + let disposable = openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in self?.present(c, in: .window(.root)) }, openResolved: { [weak self] resolved in self?.openResolved(result: resolved, sourceMessageId: message?.id, forceExternal: forceExternal, concealed: concealed, commit: commit) - }) + }, progress: progress) + self.navigationActionDisposable.set(disposable) }, performAction: true) } - private func openUrlIn(_ url: String) { + func openUrlIn(_ url: String) { let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { @@ -18120,7 +17882,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(actionSheet, in: .window(.root)) } - private func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { + func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { guard let peerId = self.chatLocation.peerId else { return } @@ -18202,7 +17964,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { + func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? @@ -18291,7 +18053,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if canDisplayContextMenu, let contextController = contextController { - contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) } else { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in @@ -18311,7 +18073,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func presentClearCacheSuggestion() { + func presentClearCacheSuggestion() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -18405,7 +18167,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedReportReason(reason).updatedInterfaceState { $0.withUpdatedSelectedMessages([]) } }) } - private func displayMediaRecordingTooltip() { + func displayMediaRecordingTooltip() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -18478,7 +18240,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func displaySendWhenOnlineTooltip() { + func displaySendWhenOnlineTooltip() { guard let rect = self.chatDisplayNode.frameForInputActionButton(), self.effectiveNavigationController?.topViewController === self, let peerId = self.chatLocation.peerId else { return } @@ -18532,7 +18294,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func displaySendingOptionsTooltip() { + func displaySendingOptionsTooltip() { guard let rect = self.chatDisplayNode.frameForInputActionButton(), self.effectiveNavigationController?.topViewController === self else { return } @@ -18552,7 +18314,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func displayEmojiTooltip() { + func displayEmojiTooltip() { guard let rect = self.chatDisplayNode.frameForEmojiButton(), self.effectiveNavigationController?.topViewController === self else { return } @@ -18572,7 +18334,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func displayChecksTooltip() { + func displayChecksTooltip() { self.checksTooltipController?.dismiss() var latestNode: (Int32, ASDisplayNode)? @@ -18611,7 +18373,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func dismissAllTooltips() { + func dismissAllTooltips() { self.emojiTooltipController?.dismiss() self.sendingOptionsTooltipController?.dismiss() self.searchResultsTooltipController?.dismiss() @@ -18639,7 +18401,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func commitPurposefulAction() { + func commitPurposefulAction() { if let purposefulAction = self.purposefulAction { self.purposefulAction = nil purposefulAction() @@ -18790,11 +18552,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(before: replyMessageId) { + if let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject { + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(before: replyMessageSubject.messageId) { strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in var updatedState = state.updatedInterfaceState({ state in - return state.withUpdatedReplyMessageId(message.id) + return state.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: message.id, + quote: nil + )) }) if updatedState.inputMode == .none { updatedState = updatedState.updatedInputMode({ _ in .text }) @@ -18802,7 +18567,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return updatedState }) - strongSelf.navigateToMessage(messageLocation: .id(message.id, nil), animated: true) + strongSelf.navigateToMessage(messageLocation: .id(message.id, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true) } } else { strongSelf.scrollToEndOfHistory() @@ -18810,7 +18575,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView() strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in var updatedState = state.updatedInterfaceState({ state in - return state.withUpdatedReplyMessageId(lastMessage?.id) + return state.withUpdatedReplyMessageSubject((lastMessage?.id).flatMap { id in + return ChatInterfaceState.ReplyMessageSubject( + messageId: id, + quote: nil + ) + }) }) if updatedState.inputMode == .none { updatedState = updatedState.updatedInputMode({ _ in .text }) @@ -18819,7 +18589,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) if let lastMessage = lastMessage { - strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, nil), animated: true) + strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true) } }) } @@ -18836,29 +18606,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId { + if let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject { let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView() - var updatedReplyMessageId: MessageId? - if replyMessageId == lastMessage?.id { - updatedReplyMessageId = nil - } else if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(after: replyMessageId) { - updatedReplyMessageId = message.id + var updatedReplyMessageSubject: ChatInterfaceState.ReplyMessageSubject? + if replyMessageSubject.messageId == lastMessage?.id { + updatedReplyMessageSubject = nil + } else if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(after: replyMessageSubject.messageId) { + updatedReplyMessageSubject = ChatInterfaceState.ReplyMessageSubject(messageId: message.id, quote: nil) } strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in var updatedState = state.updatedInterfaceState({ state in - return state.withUpdatedReplyMessageId(updatedReplyMessageId) + return state.withUpdatedReplyMessageSubject(updatedReplyMessageSubject) }) if updatedState.inputMode == .none { updatedState = updatedState.updatedInputMode({ _ in .text }) - } else if updatedReplyMessageId == nil { + } else if updatedReplyMessageSubject == nil { updatedState = updatedState.updatedInputMode({ _ in .none }) } return updatedState }) - if let updatedReplyMessageId = updatedReplyMessageId { - strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageId, nil), animated: true) + if let updatedReplyMessageSubject { + strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageSubject.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true) } } } @@ -18957,7 +18727,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func clearInputText() { + func clearInputText() { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in if !state.interfaceState.effectiveInputState.inputText.string.isEmpty { return state.updatedInterfaceState { interfaceState in @@ -18970,7 +18740,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func updateReminderActivity() { + func updateReminderActivity() { if self.isReminderActivityEnabled && false { if #available(iOS 9.0, *) { if self.reminderActivity == nil, case let .peer(peerId) = self.chatLocation, let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer { @@ -18998,7 +18768,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func updateSlowmodeStatus() { + func updateSlowmodeStatus() { if let slowmodeState = self.presentationInterfaceState.slowmodeState, case let .timestamp(slowmodeActiveUntilTimestamp) = slowmodeState.variant { let timestamp = Int32(Date().timeIntervalSince1970) let remainingTime = max(0, slowmodeActiveUntilTimestamp - timestamp) @@ -19028,7 +18798,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func openScheduledMessages() { + func openScheduledMessages() { guard let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else { return } @@ -19037,7 +18807,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G navigationController.pushViewController(controller) } - private func openPinnedMessages(at messageId: MessageId?) { + func openPinnedMessages(at messageId: MessageId?) { let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in guard let self, let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else { return @@ -19060,7 +18830,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func performUpdatedClosedPinnedMessageId(pinnedMessageId: MessageId) { + func performUpdatedClosedPinnedMessageId(pinnedMessageId: MessageId) { let previousClosedPinnedMessageId = self.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId self.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -19107,7 +18877,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) } - private func performRequestedUnpinAllMessages(count: Int, pinnedMessageId: MessageId) { + func performRequestedUnpinAllMessages(count: Int, pinnedMessageId: MessageId) { guard let peerId = self.chatLocation.peerId else { return } @@ -19160,7 +18930,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) } - private func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { guard let peerId = self.chatLocation.peerId else { return } @@ -19192,7 +18962,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { guard case .peer = self.chatLocation else { return } @@ -19203,7 +18973,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(controller, in: .window(.root)) } - private func presentVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool { + func presentVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool { if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { alertAction?() Queue.mainQueue().after(delay ? 0.2 : 0.0) { @@ -19222,7 +18992,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } - private func presentAutoremoveSetup() { + func presentAutoremoveSetup() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -19255,7 +19025,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(controller, in: .window(.root)) } - private func presentChatRequestAdminInfo() { + func presentChatRequestAdminInfo() { if let requestChatTitle = self.presentationInterfaceState.contactStatus?.peerStatusSettings?.requestChatTitle, let requestDate = self.presentationInterfaceState.contactStatus?.peerStatusSettings?.requestChatDate { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -19279,8 +19049,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private var crossfading = false - private func presentCrossfadeSnapshot() { + var crossfading = false + func presentCrossfadeSnapshot() { guard !self.crossfading, let snapshotView = self.view.snapshotView(afterScreenUpdates: false) else { return } @@ -19532,7 +19302,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.controllerInteraction?.playNextOutgoingGift = true } - private var effectiveNavigationController: NavigationController? { + var effectiveNavigationController: NavigationController? { if let navigationController = self.navigationController as? NavigationController { return navigationController } else if case let .inline(navigationController) = self.presentationInterfaceState.mode { @@ -19663,12 +19433,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return self.presentationData.strings.Chat_SendAllowedContentText(itemListString).string } - private func updateNextChannelToReadVisibility() { + func updateNextChannelToReadVisibility() { self.chatDisplayNode.historyNode.offerNextChannelToRead = self.offerNextChannelToRead && self.presentationInterfaceState.interfaceState.selectionState == nil } + + func displayGiveawayStatusInfo(messageId: EngineMessage.Id, giveawayInfo: PremiumGiveawayInfo) { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] message in + guard let self, let message else { + return + } + if let controller = giveawayInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, message: message, giveawayInfo: giveawayInfo) { + self.present(controller, in: .window(.root)) + } + }) + } } -private final class ContextControllerContentSourceImpl: ContextControllerContentSource { +final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController weak var sourceNode: ASDisplayNode? let sourceRect: CGRect? @@ -19701,10 +19483,10 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } final class ChatControllerContextReferenceContentSource: ContextReferenceContentSource { - private let controller: ViewController - private let sourceView: UIView - private let insets: UIEdgeInsets - private let contentInsets: UIEdgeInsets + let controller: ViewController + let sourceView: UIView + let insets: UIEdgeInsets + let contentInsets: UIEdgeInsets init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { self.controller = controller @@ -19718,58 +19500,6 @@ final class ChatControllerContextReferenceContentSource: ContextReferenceContent } } - -extension Peer { - func canSetupAutoremoveTimeout(accountPeerId: PeerId) -> Bool { - if let _ = self as? TelegramSecretChat { - return false - } else if let group = self as? TelegramGroup { - if case .creator = group.role { - return true - } else if case let .admin(rights, _) = group.role { - if rights.rights.contains(.canDeleteMessages) { - return true - } - } - } else if let user = self as? TelegramUser { - if user.id != accountPeerId && user.botInfo == nil { - return true - } - } else if let channel = self as? TelegramChannel { - if channel.hasPermission(.deleteAllMessages) { - return true - } - } - - return false - } -} - -func canAddMessageReactions(message: Message) -> Bool { - if message.id.namespace != Namespaces.Message.Cloud { - return false - } - if let peer = message.peers[message.id.peerId] { - if let _ = peer as? TelegramSecretChat { - return false - } - } else { - return false - } - for media in message.media { - if let _ = media as? TelegramMediaAction { - return false - } else if let story = media as? TelegramMediaStory { - if story.isMention { - return false - } - } else if let _ = media as? TelegramMediaExpiredContent { - return false - } - } - return true -} - enum AllowedReactions { case set(Set) case all diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 1730e1619f3..f8b0b7db849 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -38,6 +38,17 @@ import ChatControllerInteraction import ChatAvatarNavigationNode import AccessoryPanelNode import ForwardAccessoryPanelNode +import ChatOverscrollControl +import ChatInputPanelNode +import ChatInputContextPanelNode +import TextSelectionNode +import ReplyAccessoryPanelNode +import ChatMessageItemView +import ChatMessageSelectionNode +import ManagedDiceAnimationNode +import ChatMessageTransitionNode +import ChatLoadingNode +import ChatRecentActionsController final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -316,7 +327,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var dropDimNode: ASDisplayNode? - let messageTransitionNode: ChatMessageTransitionNode + let messageTransitionNode: ChatMessageTransitionNodeImpl private let presentationContextMarker = ASDisplayNode() @@ -449,96 +460,210 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputContextOverTextPanelContainer = ChatControllerTitlePanelNodeContainer() var source: ChatHistoryListSource - if case let .forwardedMessages(_, messageIds, options) = subject { - let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId), options) - |> map { messages, accountPeer, options -> ([Message], Int32, Bool) in - var messages = messages - let forwardedMessageIds = Set(messages.map { $0.id }) - messages.sort(by: { lhsMessage, rhsMessage in - return lhsMessage.timestamp > rhsMessage.timestamp - }) - messages = messages.map { message in - var flags = message.flags - flags.remove(.Incoming) - flags.remove(.IsIncomingMask) - - var hideNames = options.hideNames - if message.id.peerId == accountPeer.id && message.forwardInfo == nil { - hideNames = true - } - - var attributes = message.attributes - attributes = attributes.filter({ attribute in - if attribute is EditedMessageAttribute { - return false + if case let .messageOptions(_, messageIds, info) = subject { + switch info { + case let .forward(forward): + let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId), forward.options) + |> map { messages, accountPeer, options -> ([Message], Int32, Bool) in + var messages = messages + let forwardedMessageIds = Set(messages.map { $0.id }) + messages.sort(by: { lhsMessage, rhsMessage in + return lhsMessage.index > rhsMessage.index + }) + messages = messages.map { message in + var flags = message.flags + flags.remove(.Incoming) + flags.remove(.IsIncomingMask) + + var hideNames = options.hideNames + if message.id.peerId == accountPeer.id && message.forwardInfo == nil { + hideNames = true } - if let attribute = attribute as? ReplyMessageAttribute { - if !forwardedMessageIds.contains(attribute.messageId) || hideNames { + + var attributes = message.attributes + attributes = attributes.filter({ attribute in + if attribute is EditedMessageAttribute { return false } + if let attribute = attribute as? ReplyMessageAttribute { + if attribute.quote != nil { + } else { + if !forwardedMessageIds.contains(attribute.messageId) || hideNames { + return false + } + } + } + if attribute is ReplyMarkupMessageAttribute { + return false + } + if attribute is ReplyThreadMessageAttribute { + return false + } + if attribute is ViewCountMessageAttribute { + return false + } + if attribute is ForwardCountMessageAttribute { + return false + } + if attribute is ReactionsMessageAttribute { + return false + } + return true + }) + + var messageText = message.text + var messageMedia = message.media + var hasDice = false + + if hideNames { + for media in message.media { + if options.hideCaptions { + if media is TelegramMediaImage || media is TelegramMediaFile { + messageText = "" + break + } + } + if let poll = media as? TelegramMediaPoll { + var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) } + updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil)) + messageMedia = updatedMedia + } + if let _ = media as? TelegramMediaDice { + hasDice = true + } + } } - if attribute is ReplyMarkupMessageAttribute { - return false - } - if attribute is ReplyThreadMessageAttribute { - return false - } - if attribute is ViewCountMessageAttribute{ - return false + + var forwardInfo: MessageForwardInfo? + if let existingForwardInfo = message.forwardInfo { + forwardInfo = MessageForwardInfo(author: existingForwardInfo.author, source: existingForwardInfo.source, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: []) } - if attribute is ForwardCountMessageAttribute { - return false + else { + forwardInfo = MessageForwardInfo(author: message.author, source: nil, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: []) } - if attribute is ReactionsMessageAttribute { - return false + if hideNames && !hasDice { + forwardInfo = nil } - return true + + return message.withUpdatedFlags(flags).withUpdatedText(messageText).withUpdatedMedia(messageMedia).withUpdatedTimestamp(Int32(context.account.network.context.globalTime())).withUpdatedAttributes(attributes).withUpdatedAuthor(accountPeer).withUpdatedForwardInfo(forwardInfo) + } + + return (messages, Int32(messages.count), false) + } + source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, loadMore: nil) + case let .reply(reply): + let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId)) + |> map { messages, accountPeer -> ([Message], Int32, Bool) in + var messages = messages + messages.sort(by: { lhsMessage, rhsMessage in + return lhsMessage.timestamp > rhsMessage.timestamp }) + messages = messages.map { message in + return message + } - var messageText = message.text - var messageMedia = message.media - var hasDice = false - if hideNames { - for media in message.media { - if options.hideCaptions { - if media is TelegramMediaImage || media is TelegramMediaFile { - messageText = "" - break - } - } - if let poll = media as? TelegramMediaPoll { - var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) } - updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil)) - messageMedia = updatedMedia - } - if let _ = media as? TelegramMediaDice { - hasDice = true + return (messages, Int32(messages.count), false) + } + source = .custom(messages: messages, messageId: messageIds.first ?? MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: reply.quote?.text, loadMore: nil) + case let .link(link): + let messages = link.options + |> mapToSignal { options -> Signal<(ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]), NoError> in + let stories: Signal<[StoryId: CodableEntry], NoError> + if case let .Loaded(content) = options.webpage.content, let story = content.story { + stories = context.account.postbox.transaction { transaction -> [StoryId: CodableEntry] in + var result: [StoryId: CodableEntry] = [:] + if let storyValue = transaction.getStory(id: story.storyId) { + result[story.storyId] = storyValue } + return result } + } else { + stories = .single([:]) } - var forwardInfo: MessageForwardInfo? - if let existingForwardInfo = message.forwardInfo { - forwardInfo = MessageForwardInfo(author: existingForwardInfo.author, source: existingForwardInfo.source, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: []) + if let replyMessageId = options.replyMessageId { + return combineLatest( + context.account.postbox.messagesAtIds([replyMessageId]), + context.account.postbox.loadedPeerWithId(context.account.peerId), + stories + ) + |> map { messages, peer, stories -> (ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]) in + return (options, peer, messages.first, stories) + } + } else { + return combineLatest( + context.account.postbox.loadedPeerWithId(context.account.peerId), + stories + ) + |> map { peer, stories -> (ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]) in + return (options, peer, nil, stories) + } } - else { - forwardInfo = MessageForwardInfo(author: message.author, source: nil, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: []) + } + |> map { options, accountPeer, replyMessage, stories -> ([Message], Int32, Bool) in + var peers = SimpleDictionary() + peers[accountPeer.id] = accountPeer + + var associatedMessages = SimpleDictionary() + + var media: [Media] = [] + if case let .Loaded(content) = options.webpage.content { + media.append(TelegramMediaWebpage(webpageId: options.webpage.webpageId, content: .Loaded(content))) } - if hideNames && !hasDice { - forwardInfo = nil + + let associatedStories: [StoryId: CodableEntry] = stories + + var attributes: [MessageAttribute] = [] + + attributes.append(TextEntitiesMessageAttribute(entities: options.messageEntities)) + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !options.linkBelowText, forceLargeMedia: options.largeMedia, isManuallyAdded: true, isSafe: false)) + + if let replyMessage { + associatedMessages[replyMessage.id] = replyMessage + + var mappedQuote: EngineMessageReplyQuote? + if let quote = options.replyQuote { + mappedQuote = EngineMessageReplyQuote(text: quote, entities: [], media: nil) + } + + attributes.append(ReplyMessageAttribute(messageId: replyMessage.id, threadMessageId: nil, quote: mappedQuote, isQuote: mappedQuote != nil)) } - return message.withUpdatedFlags(flags).withUpdatedText(messageText).withUpdatedMedia(messageMedia).withUpdatedTimestamp(Int32(context.account.network.context.globalTime())).withUpdatedAttributes(attributes).withUpdatedAuthor(accountPeer).withUpdatedForwardInfo(forwardInfo) + let message = Message( + stableId: 1, + stableVersion: 1, + id: MessageId(peerId: accountPeer.id, namespace: 0, id: 1), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: Int32(Date().timeIntervalSince1970), + flags: [], + tags: [], + globalTags: [], + localTags: [], + forwardInfo: nil, + author: accountPeer, + text: options.messageText, + attributes: attributes, + media: media, + peers: peers, + associatedMessages: associatedMessages, + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: associatedStories + ) + + return ([message], 1, false) } - - return (messages, Int32(messages.count), false) + source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, loadMore: nil) } - source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), loadMore: nil) } else { source = .default } - var getMessageTransitionNode: (() -> ChatMessageTransitionNode?)? + var getMessageTransitionNode: (() -> ChatMessageTransitionNodeImpl?)? self.historyNode = ChatHistoryListNode(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), messageTransitionNode: { return getMessageTransitionNode?() }) @@ -554,7 +679,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var getContentAreaInScreenSpaceImpl: (() -> CGRect)? var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)? - self.messageTransitionNode = ChatMessageTransitionNode(listNode: self.historyNode, getContentAreaInScreenSpace: { + self.messageTransitionNode = ChatMessageTransitionNodeImpl(listNode: self.historyNode, getContentAreaInScreenSpace: { return getContentAreaInScreenSpaceImpl?() ?? CGRect() }, onTransitionEvent: { transition in onTransitionEventImpl?(transition) @@ -997,6 +1122,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { + var height = self.historyNode.scroller.contentSize.height + height += 3.0 + height = min(height, layout.size.height) + return CGSize(width: layout.size.width, height: height) + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void) { let transition: ContainedViewLayoutTransition if let _ = self.scheduledAnimateInAsOverlayFromNode { @@ -1468,7 +1600,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { if let _ = accessoryPanelNode as? ReplyAccessoryPanelNode { - strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedReplyMessageId(nil) }) + strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedReplyMessageSubject(nil) }) } else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil) }) } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { @@ -2934,8 +3066,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - func textInputNode() -> EditableTextNode? { - return self.textInputPanelNode?.textInputNode + func textInputView() -> UITextView? { + return self.textInputPanelNode?.textInputNode?.textView } func updateRecordedMediaDeleted(_ isDeleted: Bool) { @@ -3152,6 +3284,33 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { switch self.chatPresentationInterfaceState.mode { case .standard(previewing: true): + if let subject = self.controller?.subject, case let .messageOptions(_, _, info) = subject, case .reply = info { + if let controller = self.controller { + if let result = controller.presentationContext.hitTest(view: self.view, point: point, with: event) { + return result + } + } + + if let result = self.historyNode.view.hitTest(self.view.convert(point, to: self.historyNode.view), with: event), let node = result.asyncdisplaykit_node { + if node is TextSelectionNode { + return result + } + } + } else if let subject = self.controller?.subject, case let .messageOptions(_, _, info) = subject, case .link = info { + if let controller = self.controller { + if let result = controller.presentationContext.hitTest(view: self.view, point: point, with: event) { + return result + } + } + + if let result = self.historyNode.view.hitTest(self.view.convert(point, to: self.historyNode.view), with: event), let node = result.asyncdisplaykit_node { + if let textNode = node as? TextAccessibilityOverlayNode { + let _ = textNode + return result + } + } + } + if let result = self.historyNode.view.hitTest(self.view.convert(point, to: self.historyNode.view), with: event), let node = result.asyncdisplaykit_node, node is ChatMessageSelectionNode || node is GridMessageSelectionNode { return result } @@ -3433,6 +3592,54 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return } + if let replyMessageSubject = self.chatPresentationInterfaceState.interfaceState.replyMessageSubject, let quote = replyMessageSubject.quote { + if let replyMessage = self.chatPresentationInterfaceState.replyMessage { + let nsText = replyMessage.text as NSString + var startIndex = 0 + var found = false + while true { + let range = nsText.range(of: quote.text, range: NSRange(location: startIndex, length: nsText.length - startIndex)) + if range.location != NSNotFound { + let subEntities = messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: range, onlyQuoteable: true) + if subEntities == quote.entities { + found = true + break + } + + startIndex = range.upperBound + } else { + break + } + } + + if !found { + let authorName: String = (replyMessage.author.flatMap(EnginePeer.init))?.compactDisplayTitle ?? "" + let errorTextData = self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedText(authorName) + let errorText = errorTextData.string + self.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.context.sharedContext.currentPresentationData.with({ $0 })), title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedTitle, text: errorText, actions: [ + TextAlertAction(type: .genericAction, title: self.chatPresentationInterfaceState.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedActionEdit, action: { [weak self] in + guard let self, let controller = self.controller else { + return + } + controller.updateChatPresentationInterfaceState(interactive: false, { presentationInterfaceState in + return presentationInterfaceState.updatedInterfaceState { interfaceState in + guard var replyMessageSubject = interfaceState.replyMessageSubject else { + return interfaceState + } + replyMessageSubject.quote = nil + return interfaceState.withUpdatedReplyMessageSubject(replyMessageSubject) + } + }) + presentChatLinkOptions(selfController: controller, sourceNode: controller.displayNode) + }), + ], parseMarkdown: true), in: .window(.root)) + + return + } + } + } + let timestamp = CACurrentMediaTime() if self.lastSendTimestamp + 0.15 > timestamp { return @@ -3444,7 +3651,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let trimmedInputText = effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines) let peerId = effectivePresentationInterfaceState.chatLocation.peerId if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText), effectiveInputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil { - messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } else { let inputText = convertMarkdownToAttributes(effectiveInputText) @@ -3456,10 +3663,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } var webpage: TelegramMediaWebpage? - if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil { - attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) - } else { - webpage = self.chatPresentationInterfaceState.urlPreview?.1 + if let urlPreview = self.chatPresentationInterfaceState.urlPreview { + if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) { + attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) + } else { + webpage = urlPreview.webPage + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true, isSafe: false)) + } } var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] @@ -3477,7 +3687,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { bubbleUpEmojiOrStickersets.removeAll() } - messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) + messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: webpage.flatMap(AnyMediaReference.standalone), threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) } } @@ -3525,7 +3735,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if self.shouldAnimateMessageTransition, let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let textInput = inputPanelNode.makeSnapshotForTransition() { usedCorrelationId = correlationId - let source: ChatMessageTransitionNode.Source = .textInput(textInput: textInput, replyPanel: replyPanel) + let source: ChatMessageTransitionNodeImpl.Source = .textInput(textInput: textInput, replyPanel: replyPanel) self.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { }) } @@ -3537,7 +3747,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreview(nil) }) + strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreviews([]) }) strongSelf.ignoreUpdateHeight = false } }, usedCorrelationId) diff --git a/submodules/TelegramUI/Sources/ChatDateSelectionSheet.swift b/submodules/TelegramUI/Sources/ChatDateSelectionSheet.swift deleted file mode 100644 index 658c10305eb..00000000000 --- a/submodules/TelegramUI/Sources/ChatDateSelectionSheet.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import UIKit -import SwiftSignalKit -import Photos -import TelegramPresentationData -import UIKitRuntimeUtils - -final class ChatDateSelectionSheet: ActionSheetController { - private let strings: PresentationStrings - - private let _ready = Promise() - override var ready: Promise { - return self._ready - } - - init(presentationData: PresentationData, completion: @escaping (Int32) -> Void) { - self.strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self._ready.set(.single(true)) - - var updatedValue: Int32? - self.setItemGroups([ - ActionSheetItemGroup(items: [ - ChatDateSelectorItem(strings: self.strings, valueChanged: { value in - updatedValue = value - }), - ActionSheetButtonItem(title: self.strings.Common_Search, action: { [weak self] in - self?.dismissAnimated() - if let updatedValue = updatedValue { - completion(updatedValue) - } - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -private final class ChatDateSelectorItem: ActionSheetItem { - let strings: PresentationStrings - - let valueChanged: (Int32) -> Void - - init(strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) { - self.strings = strings - self.valueChanged = valueChanged - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ChatDateSelectorItemNode(theme: theme, strings: self.strings, valueChanged: self.valueChanged) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class ChatDateSelectorItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - - private let pickerView: UIDatePicker - - private let valueChanged: (Int32) -> Void - - private var currentValue: Int32 { - return Int32(self.pickerView.date.timeIntervalSince1970) - } - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) { - self.theme = theme - self.strings = strings - self.valueChanged = valueChanged - - UILabel.setDateLabel(theme.primaryTextColor) - - self.pickerView = UIDatePicker() - self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .date - self.pickerView.locale = Locale(identifier: strings.baseLanguageCode) - - self.pickerView.minimumDate = Date(timeIntervalSince1970: 1376438400.0) - self.pickerView.maximumDate = Date(timeIntervalSinceNow: 2.0) - - if #available(iOS 13.4, *) { - self.pickerView.preferredDatePickerStyle = .wheels - } - - self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") - self.pickerView.setValue(theme.primaryTextColor, forKey: "highlightColor") - - super.init(theme: theme) - - self.view.addSubview(self.pickerView) - self.pickerView.addTarget(self, action: #selector(self.pickerChanged), for: .valueChanged) - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 157.0) - - self.pickerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 180.0)) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc func pickerChanged() { - self.valueChanged(self.currentValue) - } -} diff --git a/submodules/TelegramUI/Sources/ChatEditMessageMediaContext.swift b/submodules/TelegramUI/Sources/ChatEditMessageMediaContext.swift deleted file mode 100644 index 0ab47ae01ec..00000000000 --- a/submodules/TelegramUI/Sources/ChatEditMessageMediaContext.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import SwiftSignalKit -import Postbox -import TelegramCore -import AccountContext - -private final class MessageContext { - let disposable: Disposable - - init(disposable: Disposable) { - self.disposable = disposable - } - - deinit { - self.disposable.dispose() - } -} - -final class ChatEditMessageMediaContext { - private let context: AccountContext - - private let contexts: [MessageId: MessageContext] = [:] - - init(context: AccountContext) { - self.context = context - } - - func update(id: MessageId, text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, media: RequestEditMessageMedia) { - - } -} diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index 6c7c7ffe6f8..f511b9dafac 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -14,6 +14,7 @@ import ChatPresentationInterfaceState import WallpaperBackgroundNode import ComponentFlow import EmojiStatusComponent +import ChatLoadingNode private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize diff --git a/submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift deleted file mode 100644 index f8158219118..00000000000 --- a/submodules/TelegramUI/Sources/ChatFeedNavigationInputPanelNode.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import Postbox -import SwiftSignalKit -import TelegramPresentationData -import ChatPresentationInterfaceState - -final class ChatFeedNavigationInputPanelNode: ChatInputPanelNode { - private let button: HighlightableButtonNode - - private var presentationInterfaceState: ChatPresentationInterfaceState? - - private var theme: PresentationTheme - private var strings: PresentationStrings - - init(theme: PresentationTheme, strings: PresentationStrings) { - self.theme = theme - self.strings = strings - - self.button = HighlightableButtonNode() - - super.init() - - self.addSubnode(self.button) - - self.button.setAttributedTitle(NSAttributedString(string: "Show Next", font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) - self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) - } - - deinit { - } - - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - if self.theme !== theme || self.strings !== strings { - self.theme = theme - self.strings = strings - - self.button.setAttributedTitle(NSAttributedString(string: strings.Conversation_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - return self.button.view - } else { - return nil - } - } - - @objc func buttonPressed() { - self.interfaceInteraction?.navigateFeed() - } - - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { - if self.presentationInterfaceState != interfaceState { - self.presentationInterfaceState = interfaceState - } - - let buttonSize = self.button.measure(CGSize(width: width - leftInset - rightInset - 80.0, height: 100.0)) - - let panelHeight = defaultHeight(metrics: metrics) - - self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) - - return panelHeight - } - - override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return defaultHeight(metrics: metrics) - } -} - diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 3c49c2552dc..2078f00d802 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -5,6 +5,8 @@ import TemporaryCachedPeerDataManager import Emoji import AccountContext import TelegramPresentationData +import ChatHistoryEntry +import ChatMessageItemCommon func chatHistoryEntriesForView( location: ChatLocation, diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 0cf05e111a1..4d6b8d61386 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -30,6 +30,13 @@ import ChatPresentationInterfaceState import TelegramNotices import ChatControllerInteraction import TranslateUI +import ChatHistoryEntry +import ChatOverscrollControl +import ChatBotInfoItem +import ChatMessageItem +import ChatMessageItemImpl +import ChatMessageItemView +import ChatMessageTransitionNode // MARK: Nicegram AiChat private extension ListViewUpdateSizeAndInsets { @@ -39,12 +46,6 @@ private extension ListViewUpdateSizeAndInsets { } // -extension ChatReplyThreadMessage { - var effectiveTopId: MessageId { - return self.channelMessageId ?? self.messageId - } -} - struct ChatTopVisibleMessageRange: Equatable { var lowerBound: MessageIndex var upperBound: MessageIndex @@ -68,7 +69,7 @@ public enum ChatHistoryListMode: Equatable { enum ChatHistoryViewScrollPosition { case unread(index: MessageIndex) case positionRestoration(index: MessageIndex, relativeOffset: CGFloat) - case index(index: MessageHistoryAnchorIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool) + case index(subject: MessageHistoryScrollToSubject, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool) } enum ChatHistoryViewUpdateType { @@ -139,7 +140,7 @@ struct ChatHistoryViewTransition { var cachedData: CachedPeerData? var cachedDataMessages: [MessageId: Message]? var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? - var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToIndex: MessageHistoryScrollToSubject? var scrolledToSomeIndex: Bool var animateIn: Bool var reason: ChatHistoryViewTransitionReason @@ -159,7 +160,7 @@ struct ChatHistoryListViewTransition { var cachedData: CachedPeerData? var cachedDataMessages: [MessageId: Message]? var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? - var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToIndex: MessageHistoryScrollToSubject? var scrolledToSomeIndex: Bool var peerType: MediaAutoDownloadPeerType var networkType: MediaAutoDownloadNetworkType @@ -220,7 +221,7 @@ extension ListMessageItemInteraction { }, toggleMessagesSelection: { messageId, selected in controllerInteraction.toggleMessagesSelection(messageId, selected) }, openUrl: { url, param1, param2, message in - controllerInteraction.openUrl(url, param1, param2, message) + controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: param1, external: param2, message: message)) }, openInstantPage: { message, data in controllerInteraction.openInstantPage(message, data) }, longTap: { action, message in @@ -238,7 +239,8 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) + // MARK: Nicegram, wantTrButton + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { @@ -256,7 +258,8 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), wantTrButton: wantTrButton) + // MARK: Nicegram, wantTrButton + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), wantTrButton: wantTrButton) case .list: assertionFailure() item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) @@ -283,7 +286,8 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) + // MARK: Nicegram, wantTrButton + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { @@ -301,7 +305,8 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), wantTrButton: wantTrButton) + // MARK: Nicegram, wantTrButton + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), wantTrButton: wantTrButton) case .list: assertionFailure() item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) @@ -395,10 +400,14 @@ private extension ChatHistoryLocationInput { switch self.content { case .Navigation(index: .upperBound, anchorIndex: .upperBound, count: _, highlight: _): return true - case .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: _, scrollPosition: _, animated: _, highlight: _): + case let .Scroll(subject, anchorIndex, _, _, _, _): + if case .upperBound = anchorIndex, case .upperBound = subject.index { return true - default: + } else { return false + } + default: + return false } } } @@ -427,7 +436,7 @@ private var nextClientId: Int32 = 1 public enum ChatHistoryListSource { case `default` - case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, loadMore: (() -> Void)?) + case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, quote: String?, loadMore: (() -> Void)?) } public final class ChatHistoryListNode: ListView, ChatHistoryNode { @@ -550,7 +559,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? - var scrolledToIndex: ((MessageHistoryAnchorIndex, Bool) -> Void)? + var scrolledToIndex: ((MessageHistoryScrollToSubject, Bool) -> Void)? var scrolledToSomeIndex: (() -> Void)? var beganDragging: (() -> Void)? @@ -660,6 +669,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var adMessagesDisposable: Disposable? private var preloadAdPeerId: PeerId? private let preloadAdPeerDisposable = MetaDisposable() + private var seenAdIds: [Data] = [] private var pendingDynamicAdMessages: [Message] = [] private var pendingDynamicAdMessageInterval: Int? private var remainingDynamicAdMessageInterval: Int? @@ -684,8 +694,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var toLang: String? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource = .default, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNode? = { nil }) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource = .default, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl? = { nil }) { + // MARK: Nicegram self.wantTrButton = usetrButton() + // var tagMask = tagMask if case .pinnedMessages = subject { tagMask = .pinned @@ -833,9 +845,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { initialSearchLocation = .index(MessageIndex.absoluteUpperBound()) } } - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: highlight?.quote), count: historyMessageCount, highlight: highlight != nil), id: 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: historyMessageCount, highlight: true), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: 0) } else { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0) } @@ -1038,7 +1050,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private func beginChatHistoryTransitions( selectedMessages: Signal?, NoError>, - messageTransitionNode: @escaping () -> ChatMessageTransitionNode? + messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl? ) { let context = self.context let chatLocation = self.chatLocation @@ -1099,7 +1111,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let historyViewUpdate: Signal<(ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange?), NoError> var isFirstTime = true var updateAllOnEachVersion = false - if case let .custom(messages, at, _) = self.source { + if case let .custom(messages, at, quote, _) = self.source { updateAllOnEachVersion = true historyViewUpdate = messages |> map { messages, _, hasMore in @@ -1113,7 +1125,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let scrollPosition: ChatHistoryViewScrollPosition? if isFirstTime, let messageIndex = messages.first(where: { $0.id == at })?.index { - scrollPosition = .index(index: .message(messageIndex), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false) + scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: quote), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false) isFirstTime = false } else { scrollPosition = nil @@ -1391,9 +1403,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { initialSearchLocation = .index(.absoluteUpperBound()) } } - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: highlight?.quote), count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue { chatHistoryLocation.id += 1 strongSelf.chatHistoryLocationValue = chatHistoryLocation @@ -1524,18 +1536,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let previousSelectedMessages = previousValueAndVersion?.2 if let previousVersion = previousValueAndVersion?.1 { - if !GlobalExperimentalSettings.isAppStoreBuild { - precondition(update.1 >= previousVersion) - } assert(update.1 >= previousVersion) } if scrollPosition == nil, let originalScrollPosition = originalScrollPosition { switch originalScrollPosition { - case let .index(index, position, _, _, highlight, displayLink): - if case .upperBound = index { + case let .index(subject, position, _, _, highlight, displayLink): + if case .upperBound = subject.index { if let previous = previous, previous.filteredEntries.isEmpty { - updatedScrollPosition = .index(index: index, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink) + updatedScrollPosition = .index(subject: subject, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink) } } default: @@ -1582,7 +1591,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let strongSelf = self, case .default = source { strongSelf.toLang = translateToLanguage if strongSelf.appliedScrollToMessageId == nil, let scrollToMessageId = scrollToMessageId { - updatedScrollPosition = .index(index: .message(scrollToMessageId), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(scrollToMessageId), quote: nil), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true) scrollAnimationCurve = .Spring(duration: 0.4) } else { let wasPlaying = strongSelf.appliedPlayingMessageId != nil @@ -1612,7 +1621,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } }) if currentIsVisible && nextIsVisible && currentlyPlayingVideo { - updatedScrollPosition = .index(index: .message(currentlyPlayingMessageId), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(currentlyPlayingMessageId), quote: nil), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true) scrollAnimationCurve = .Spring(duration: 0.4) } } @@ -1657,7 +1666,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if fillsScreen, let firstNonAdIndex = firstNonAdIndex, previousNumAds == 0, updatedNumAds != 0 { - updatedScrollPosition = .index(index: .message(firstNonAdIndex), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(firstNonAdIndex), quote: nil), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) disableAnimations = true } } @@ -1787,7 +1796,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.forEachItemHeaderNode { itemHeaderNode in if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNode { dateNode.updatePresentationData(chatPresentationData, context: strongSelf.context) - } else if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNode { + } else if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNodeImpl { avatarNode.updatePresentationData(chatPresentationData, context: strongSelf.context) } else if let dateNode = itemHeaderNode as? ListMessageDateHeaderNode { dateNode.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) @@ -2048,7 +2057,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { break } } - self.adMessagesContext?.markAsSeen(opaqueId: opaqueId) + if !self.seenAdIds.contains(opaqueId) { + self.seenAdIds.append(opaqueId) + self.adMessagesContext?.markAsSeen(opaqueId: opaqueId) + } } private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) { @@ -2597,7 +2609,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { currentMessage = messages.first?.0 } if let message = currentMessage, let _ = self.anchorMessageInCurrentHistoryView() { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(message.index), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) } } } @@ -2626,14 +2638,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if let currentMessage = currentMessage { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(currentMessage.index), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(currentMessage.index), quote: nil), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true), id: self.takeNextHistoryLocationId()) } } } public func scrollToStartOfHistory() { self.beganDragging?() - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) } public func scrollToEndOfHistory() { @@ -2642,13 +2654,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { case let .known(value) where value <= CGFloat.ulpOfOne: break default: - let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) + let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) self.chatHistoryLocationValue = locationInput } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, scrollPosition: ListViewScrollPosition = .center(.bottom)) { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: String? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom)) { + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) } public func anchorMessageInCurrentHistoryView() -> Message? { @@ -3592,7 +3604,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let item: ListViewItem switch self.mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) + // MARK: Nicegram, wantTrButton + item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { @@ -3648,7 +3661,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let item: ListViewItem switch self.mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location)) + item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location)) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { diff --git a/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift b/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift index fd150168c31..e1beffa6d4d 100644 --- a/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift @@ -12,6 +12,7 @@ import SearchUI import TelegramUIPreferences import ListMessageItem import ChatControllerInteraction +import ChatMessageItemView private enum ChatHistorySearchEntryStableId: Hashable { case messageId(MessageId) @@ -352,8 +353,6 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { itemNode.updateHiddenMedia() } else if let itemNode = itemNode as? ListMessageNode { itemNode.updateHiddenMedia() - } else if let itemNode = itemNode as? GridMessageItemNode { - itemNode.updateHiddenMedia() } } } @@ -369,10 +368,6 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) { transitionNode = result } - } else if let itemNode = itemNode as? GridMessageItemNode { - if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) { - transitionNode = result - } } } return transitionNode diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 3f6eb1537d4..8d3637a45d9 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -41,9 +41,9 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess if scheduled { var first = true var chatScrollPosition: ChatHistoryViewScrollPosition? - if case let .Scroll(index, _, sourceIndex, position, animated, highlight) = location.content { - let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up - chatScrollPosition = .index(index: index, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) + if case let .Scroll(subject, _, sourceIndex, position, animated, highlight) = location.content { + let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up + chatScrollPosition = .index(subject: subject, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) } return account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in @@ -112,7 +112,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess if tagMask == nil, case let .replyThread(message) = chatLocation, message.isForumPost, view.maxReadIndex == nil { if case let .message(index) = view.anchorIndex { - scrollPosition = .index(index: .message(index), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) + scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(index), quote: nil), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false) } } @@ -174,12 +174,12 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } - case let .InitialSearch(searchLocation, count, highlight): + case let .InitialSearch(searchLocationSubject, count, highlight): var preloaded = false var fadeIn = false let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> - switch searchLocation { + switch searchLocationSubject.location { case let .index(index): signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: .message(index), anchorIndex: .message(index), count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData) case let .id(id): @@ -226,7 +226,8 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } preloaded = true - return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) + + return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } case let .Navigation(index, anchorIndex, count, _): @@ -243,11 +244,11 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } - case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated, highlight): - let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up - let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) + case let .Scroll(subject, anchorIndex, sourceIndex, scrollPosition, animated, highlight): + let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up + let chatScrollPosition = ChatHistoryViewScrollPosition.index(subject: subject, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false) var first = true - return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData) + return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: subject.index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) @@ -359,7 +360,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea case .automatic: if let atMessageId = atMessageId { input = ChatHistoryLocationInput( - content: .InitialSearch(location: .id(atMessageId), count: 40, highlight: true), + content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId), quote: nil), count: 40, highlight: true), id: 0 ) } else { diff --git a/submodules/TelegramUI/Sources/ChatHoleItem.swift b/submodules/TelegramUI/Sources/ChatHoleItem.swift deleted file mode 100644 index 761ec425b40..00000000000 --- a/submodules/TelegramUI/Sources/ChatHoleItem.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Foundation -import UIKit -import Postbox -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramPresentationData - -private let titleFont = UIFont.systemFont(ofSize: 13.0) - -class ChatHoleItem: ListViewItem { - let index: MessageIndex - let presentationData: ChatPresentationData - //let header: ChatMessageDateHeader - - init(index: MessageIndex, presentationData: ChatPresentationData) { - self.index = index - self.presentationData = presentationData - //self.header = ChatMessageDateHeader(timestamp: index.timestamp, theme: theme, strings: strings) - } - - 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 = ChatHoleItemNode() - node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem) - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in }) - }) - } - } - } - - 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 { - completion(ListViewItemNodeLayout(contentSize: node().contentSize, insets: node().insets), { _ in - }) - } - } -} - -class ChatHoleItemNode: ListViewItemNode { - var item: ChatHoleItem? - let backgroundNode: ASImageNode - let labelNode: TextNode - - private let layoutConstants = ChatMessageItemLayoutConstants.default - - init() { - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.labelNode = TextNode() - self.labelNode.isUserInteractionEnabled = false - - super.init(layerBacked: false) - - self.addSubnode(self.backgroundNode) - - self.addSubnode(self.labelNode) - - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - } - - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let item = item as? ChatHoleItem { - let dateAtBottom = false//!chatItemsHaveCommonDateHeader(item, nextItem) - let (layout, apply) = self.asyncLayout()(item, params, dateAtBottom) - apply() - self.contentSize = layout.contentSize - self.insets = layout.insets - } - } - - func asyncLayout() -> (_ item: ChatHoleItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { - let labelLayout = TextNode.asyncLayout(self.labelNode) - let layoutConstants = self.layoutConstants - let currentItem = self.item - return { item, params, dateAtBottom in - var updatedBackground: UIImage? - if item.presentationData.theme !== currentItem?.presentationData.theme { - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) - updatedBackground = graphics.chatServiceBubbleFillImage - } - - let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) - - let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Channel_NotificationLoading, font: titleFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) - - return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 20.0), insets: UIEdgeInsets(top: 4.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] in - if let strongSelf = self { - strongSelf.item = item - - if let updatedBackground = updatedBackground { - strongSelf.backgroundNode.image = updatedBackground - } - - let _ = apply() - - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.backgroundNode.frame.origin.x + 8.0, y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0)), size: size.size) - } - }) - } - } - - override public func animateAdded(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) - } -} diff --git a/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift deleted file mode 100644 index 2ad88268334..00000000000 --- a/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import TelegramPresentationData -import TelegramUIPreferences -import AccountContext -import ChatPresentationInterfaceState -import ChatControllerInteraction - -enum ChatInputContextPanelPlacement { - case overPanels - case overTextInput -} - -class ChatInputContextPanelNode: ASDisplayNode { - let context: AccountContext - var interfaceInteraction: ChatPanelInterfaceInteraction? - var placement: ChatInputContextPanelPlacement = .overPanels - var theme: PresentationTheme - var fontSize: PresentationFontSize - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) { - self.context = context - self.theme = theme - self.fontSize = fontSize - - super.init() - } - - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { - } - - func animateOut(completion: @escaping () -> Void) { - completion() - } - - var topItemFrame: CGRect? { - return nil - } -} diff --git a/submodules/TelegramUI/Sources/ChatInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatInputPanelNode.swift deleted file mode 100644 index c93557a86b2..00000000000 --- a/submodules/TelegramUI/Sources/ChatInputPanelNode.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import Postbox -import TelegramCore -import AccountContext -import ChatPresentationInterfaceState - -protocol ChatInputPanelViewForOverlayContent: UIView { - func maybeDismissContent(point: CGPoint) -} - -class ChatInputPanelNode: ASDisplayNode { - var context: AccountContext? - var interfaceInteraction: ChatPanelInterfaceInteraction? - var prevInputPanelNode: ChatInputPanelNode? - - var viewForOverlayContent: ChatInputPanelViewForOverlayContent? - - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { - } - - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { - return 0.0 - } - - func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 0.0 - } - - func defaultHeight(metrics: LayoutMetrics) -> CGFloat { - if case .regular = metrics.widthClass, case .regular = metrics.heightClass { - return 49.0 - } else { - return 45.0 - } - } - - func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { - return false - } -} diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 98c74398882..f9fd936ea06 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -4,6 +4,7 @@ import TelegramCore import AccountContext import ChatPresentationInterfaceState import ChatControllerInteraction +import ChatInputContextPanelNode private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult) -> (Int, Bool) { switch result { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift index 44a66da9e6a..fcaad73f80a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift @@ -8,6 +8,8 @@ import ChatPresentationInterfaceState import ChatControllerInteraction import ChatInputNode import ChatEntityKeyboardInputNode +import ChatInputPanelNode +import ChatButtonKeyboardInputNode func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? { if let inputPanelNode = inputPanelNode, !(inputPanelNode is ChatTextInputPanelNode) { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index b09e9d5e1af..eac1111677f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -7,8 +7,12 @@ import ChatPresentationInterfaceState import ChatControllerInteraction import AccessoryPanelNode import ForwardAccessoryPanelNode +import ReplyAccessoryPanelNode func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { + if case .standard(previewing: true) = chatPresentationInterfaceState.mode { + return nil + } if let _ = chatPresentationInterfaceState.interfaceState.selectionState { return nil } @@ -17,21 +21,21 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS } switch chatPresentationInterfaceState.subject { - case .pinnedMessages, .forwardedMessages: - return nil - default: - break + case .pinnedMessages, .messageOptions: + return nil + default: + break } if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage { - if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, editMessage.disableUrlPreview != editingUrlPreview.0 { + if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, !editMessage.disableUrlPreviews.contains(editingUrlPreview.url) { if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { previewPanelNode.interfaceInteraction = interfaceInteraction - previewPanelNode.replaceWebpage(url: editingUrlPreview.0, webpage: editingUrlPreview.1) + previewPanelNode.replaceWebpage(url: editingUrlPreview.url, webpage: editingUrlPreview.webPage) previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return previewPanelNode } else { - let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: editingUrlPreview.0, webpage: editingUrlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: editingUrlPreview.url, webpage: editingUrlPreview.webPage, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } @@ -46,14 +50,14 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } - } else if let urlPreview = chatPresentationInterfaceState.urlPreview, chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != urlPreview.0 { + } else if let urlPreview = chatPresentationInterfaceState.urlPreview, !chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) { if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { previewPanelNode.interfaceInteraction = interfaceInteraction - previewPanelNode.replaceWebpage(url: urlPreview.0, webpage: urlPreview.1) + previewPanelNode.replaceWebpage(url: urlPreview.url, webpage: urlPreview.webPage) previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return previewPanelNode } else { - let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: urlPreview.0, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: urlPreview.url, webpage: urlPreview.webPage, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } @@ -67,15 +71,17 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } - } else if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId { - if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId { + } else if let replyMessageSubject = chatPresentationInterfaceState.interfaceState.replyMessageSubject { + if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageSubject.messageId && replyPanelNode.quote == replyMessageSubject.quote { replyPanelNode.interfaceInteraction = interfaceInteraction replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return replyPanelNode - } else { - let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) + } else if let peerId = chatPresentationInterfaceState.chatLocation.peerId { + let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: peerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) panelNode.interfaceInteraction = interfaceInteraction return panelNode + } else { + return nil } } else { return nil diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index b79ff97e0ce..6532b4796e6 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -47,6 +47,9 @@ import SettingsUI import PremiumUI import TextNodeWithEntities import ChatControllerInteraction +import ChatMessageItemCommon +import ChatMessageItemView +import ChatMessageBubbleItemNode // MARK: Nicegram SelectAllMessagesWithAuthor // MARK: Nicegram ReplyPrivately @@ -163,16 +166,19 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo } else if let _ = media as? TelegramMediaPoll { hasUneditableAttributes = true break - } else if let _ = media as? TelegramMediaDice { + } else if let _ = media as? TelegramMediaDice { hasUneditableAttributes = true break - } else if let _ = media as? TelegramMediaGame { + } else if let _ = media as? TelegramMediaGame { hasUneditableAttributes = true break - } else if let _ = media as? TelegramMediaInvoice { + } else if let _ = media as? TelegramMediaInvoice { hasUneditableAttributes = true break - } else if let _ = media as? TelegramMediaStory { + } else if let _ = media as? TelegramMediaStory { + hasUneditableAttributes = true + break + } else if let _ = media as? TelegramMediaGiveaway { hasUneditableAttributes = true break } @@ -309,6 +315,9 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS if case .member = channel.participationStatus { canReply = channel.hasPermission(.sendSomething) } + if case .broadcast = channel.info { + canReply = true + } } else if let group = peer as? TelegramGroup { if case .Member = group.membership { canReply = true @@ -397,7 +406,18 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState, var updated = state for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - updated = updated.updatedEditingUrlPreview((content.url, webpage)) + let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute + var positionBelowText = true + if let leadingPreview = attribute?.leadingPreview { + positionBelowText = !leadingPreview + } + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: content.url, + webPage: webpage, + positionBelowText: positionBelowText, + largeMedia: attribute?.forceLargeMedia + ) + updated = updated.updatedEditingUrlPreview(updatedPreview) } } var isPlaintext = true @@ -650,6 +670,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + if !canSendMessagesToChat(chatPresentationInterfaceState) && (chatPresentationInterfaceState.copyProtectionEnabled || message.isCopyProtected()) { + canReply = false + } + for media in messages[0].media { if let story = media as? TelegramMediaStory { if let story = message.associatedStories[story.storyId], story.data.isEmpty { @@ -724,7 +748,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState readCounters, ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager), context.engine.stickers.availableReactions(), - context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings, SharedDataKeys.loggingSettings]), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings, SharedDataKeys.loggingSettings]) |> take(1), context.engine.peers.notificationSoundList() |> take(1), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) ) @@ -819,7 +843,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let id = Int64.random(in: Int64.min ... Int64.max) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: logPath, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: "CallStats.log")]) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).startStandalone() } @@ -862,7 +886,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).startStandalone() let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip, timeout: nil) + let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip, timeout: nil, customUndoText: nil) controllerInteraction.displayUndo(content) }), false), at: 0) actions.insert(.separator, at: 1) @@ -889,9 +913,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 })) if size > settings.maxSize { - controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil)) + controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil, customUndoText: nil)) } else if Double(duration) > Double(settings.maxDuration) { - controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil)) + controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil, customUndoText: nil)) } else { let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) |> deliverOnMainQueue).startStandalone(completed: { @@ -962,9 +986,11 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if !isPinnedMessages, !isReplyThreadHead, data.canReply { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuReply, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - interfaceInteraction.setupReplyMessage(messages[0].id, { transition in - f(.custom(transition)) + }, action: { c, _ in + interfaceInteraction.setupReplyMessage(messages[0].id, { transition, completed in + c.dismiss(result: .custom(transition), completion: { + completed() + }) }) }))) @@ -1172,7 +1198,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - controllerInteraction.performTextSelectionAction(!isCopyProtected, NSAttributedString(string: messageText), .translate) + controllerInteraction.performTextSelectionAction(message, !isCopyProtected, NSAttributedString(string: messageText), .translate) f(.default) }))) } @@ -1186,7 +1212,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty { text = translation.text } - controllerInteraction.performTextSelectionAction(!isCopyProtected, NSAttributedString(string: text), .speak) + controllerInteraction.performTextSelectionAction(message, !isCopyProtected, NSAttributedString(string: text), .speak) f(.default) }))) } @@ -1245,17 +1271,19 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isMusic { - actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_SaveToFiles, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - controllerInteraction.saveMediaToFiles(message.id) - f(.default) - }))) + if !isCopyProtected { + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isMusic { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_SaveToFiles, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + controllerInteraction.saveMediaToFiles(message.id) + f(.default) + }))) + } + break } - break } } @@ -1616,7 +1644,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState guard let peer = messages[0].peers[messages[0].id.peerId] else { return } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messages[0].id), highlight: true, timecode: nil), useExisting: true)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messages[0].id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), useExisting: true)) }) }))) } @@ -1728,7 +1756,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } - controller.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) + controller.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil, animated: true) }))) } } @@ -1855,7 +1883,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState - controller.setItems(.single(ContextController.Items(content: .list(ngContextItems))), minHeight: nil) + controller.setItems(.single(ContextController.Items(content: .list(ngContextItems))), minHeight: nil, animated: true) }))) // MARK: Nicegram StickerMaker diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index f64e678f8ce..316467d8c89 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -268,6 +268,12 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee let chatPeer = peer let contextBot = context.engine.peers.resolvePeerByName(name: addressName) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> castError(ChatContextQueryError.self) |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { @@ -505,30 +511,34 @@ func searchQuerySuggestionResultStateForChatInterfacePresentationState(_ chatPre private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) -func detectUrl(_ inputText: NSAttributedString?) -> String? { - var detectedUrl: String? +func detectUrls(_ inputText: NSAttributedString?) -> [String] { + var detectedUrls: [String] = [] if let text = inputText, let dataDetector = dataDetector { let utf16 = text.string.utf16 let nsRange = NSRange(location: 0, length: utf16.count) let matches = dataDetector.matches(in: text.string, options: [], range: nsRange) - if let match = matches.first { + for match in matches { let urlText = (text.string as NSString).substring(with: match.range) - detectedUrl = urlText + detectedUrls.append(urlText) } - if detectedUrl == nil { - inputText?.enumerateAttribute(ChatTextInputAttributes.textUrl, in: nsRange, options: [], using: { value, range, stop in - if let value = value as? ChatTextInputTextUrlAttribute { - detectedUrl = value.url + inputText?.enumerateAttribute(ChatTextInputAttributes.textUrl, in: nsRange, options: [], using: { value, range, stop in + if let value = value as? ChatTextInputTextUrlAttribute { + if !detectedUrls.contains(value.url) { + detectedUrls.append(value.url) } - }) - } + } + }) } - return detectedUrl + return detectedUrls } -func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { +struct UrlPreviewState { + var detectedUrl: String +} + +func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: UrlPreviewState?) -> (UrlPreviewState?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { guard let _ = inputText else { if currentQuery != nil { return (nil, .single({ _ in return nil })) @@ -537,10 +547,17 @@ func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: Acco } } if let _ = dataDetector { - let detectedUrl = detectUrl(inputText) - if detectedUrl != currentQuery { + let detectedUrl = detectUrls(inputText).first + if detectedUrl != currentQuery?.detectedUrl { if let detectedUrl = detectedUrl { - return (detectedUrl, webpagePreview(account: context.account, url: detectedUrl) |> map { value in + return (UrlPreviewState(detectedUrl: detectedUrl), webpagePreview(account: context.account, url: detectedUrl) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> map { value in return { _ in return value } }) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 9a514b872a4..c1aee0ddbe2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -6,6 +6,9 @@ import AccountContext import NGWebUtils import NGData import ChatPresentationInterfaceState +import ChatInputPanelNode +import ChatBotStartInputPanelNode +import ChatChannelSubscriberInputPanelNode func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { @@ -22,7 +25,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return (nil, nil) } - if case .forwardedMessages = chatPresentationInterfaceState.subject { + if case .messageOptions = chatPresentationInterfaceState.subject { return (nil, nil) } @@ -191,7 +194,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } - } else if let isGeneralThreadClosed = chatPresentationInterfaceState.isGeneralThreadClosed, isGeneralThreadClosed && chatPresentationInterfaceState.interfaceState.replyMessageId == nil { + } else if let isGeneralThreadClosed = chatPresentationInterfaceState.isGeneralThreadClosed, isGeneralThreadClosed && chatPresentationInterfaceState.interfaceState.replyMessageSubject == nil { if !canManage { if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { return (currentPanel, nil) @@ -262,7 +265,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if channel.flags.contains(.isForum) { /*if let _ = chatPresentationInterfaceState.threadData { } else { - if chatPresentationInterfaceState.interfaceState.replyMessageId == nil { + if chatPresentationInterfaceState.interfaceState.replyMessageSubject == nil { if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { return (currentPanel, nil) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index ebaea614eed..ea75b733643 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -6,30 +6,11 @@ import TelegramCore import TelegramPresentationData import AccountContext import ChatPresentationInterfaceState - -enum ChatNavigationButtonAction: Equatable { - case openChatInfo(expandAvatar: Bool) - case clearHistory - case clearCache - case cancelMessageSelection - case search - case dismiss - case toggleInfoPanel - case spacer -} - -struct ChatNavigationButton: Equatable { - let action: ChatNavigationButtonAction - let buttonItem: UIBarButtonItem - - static func ==(lhs: ChatNavigationButton, rhs: ChatNavigationButton) -> Bool { - return lhs.action == rhs.action && lhs.buttonItem === rhs.buttonItem - } -} +import ChatNavigationButton func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: ChatPresentationInterfaceState, subject: ChatControllerSubject?, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { if let _ = presentationInterfaceState.interfaceState.selectionState { - if case .forwardedMessages = presentationInterfaceState.subject { + if case .messageOptions = presentationInterfaceState.subject { return nil } if let _ = presentationInterfaceState.reportReason { @@ -78,7 +59,7 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?, moreInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { if let _ = presentationInterfaceState.interfaceState.selectionState { - if case .forwardedMessages = presentationInterfaceState.subject { + if case .messageOptions = presentationInterfaceState.subject { return nil } if let currentButton = currentButton, currentButton.action == .cancelMessageSelection { @@ -107,7 +88,7 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch } } - if case .forwardedMessages = presentationInterfaceState.subject { + if case .messageOptions = presentationInterfaceState.subject { return nil } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 2babae74ba4..ea8cfe598a5 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -26,7 +26,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat var inhibitTitlePanelDisplay = false switch chatPresentationInterfaceState.subject { - case .forwardedMessages: + case .messageOptions: return nil case .scheduledMessages, .pinnedMessages: inhibitTitlePanelDisplay = true diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift deleted file mode 100644 index cfbd1db7e23..00000000000 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ /dev/null @@ -1,1328 +0,0 @@ -import Foundation -import UIKit -import Postbox -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import TelegramUIPreferences -import TextFormat -import AccountContext -import UrlEscaping -import PhotoResources -import WebsiteType -import ChatMessageInteractiveMediaBadge -import GalleryData -import TextNodeWithEntities -import AnimationCache -import MultiAnimationRenderer -import ChatControllerInteraction -import ShimmerEffect - -private let buttonFont = Font.semibold(13.0) - -enum ChatMessageAttachedContentActionIcon { - case instant - case link -} - -struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { - var rawValue: Int32 - - init(rawValue: Int32) { - self.rawValue = rawValue - } - - init() { - self.rawValue = 0 - } - - static let preferMediaInline = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 0) - static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1) - static let preferMediaAspectFilled = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 2) - static let titleBeforeMedia = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 3) -} - -final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { - private let textNode: TextNode - private let iconNode: ASImageNode - private let highlightedTextNode: TextNode - private let backgroundNode: ASImageNode - private let shimmerEffectNode: ShimmerEffectForegroundNode - - private var regularImage: UIImage? - private var highlightedImage: UIImage? - private var regularIconImage: UIImage? - private var highlightedIconImage: UIImage? - - var pressed: (() -> Void)? - - private var titleColor: UIColor? - - init() { - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.highlightedTextNode = TextNode() - self.highlightedTextNode.isUserInteractionEnabled = false - - self.shimmerEffectNode = ShimmerEffectForegroundNode() - self.shimmerEffectNode.cornerRadius = 5.0 - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.iconNode = ASImageNode() - self.iconNode.isLayerBacked = true - self.iconNode.displayWithoutProcessing = true - self.iconNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.shimmerEffectNode) - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textNode) - self.addSubnode(self.highlightedTextNode) - self.highlightedTextNode.isHidden = true - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.image = strongSelf.highlightedImage - strongSelf.iconNode.image = strongSelf.highlightedIconImage - strongSelf.textNode.isHidden = true - strongSelf.highlightedTextNode.isHidden = false - - let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width - strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false) - } else { - if let presentationLayer = strongSelf.layer.presentation() { - strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) - } - UIView.transition(with: strongSelf.view, duration: 0.2, options: [.transitionCrossDissolve], animations: { - strongSelf.backgroundNode.image = strongSelf.regularImage - strongSelf.iconNode.image = strongSelf.regularIconImage - strongSelf.textNode.isHidden = false - strongSelf.highlightedTextNode.isHidden = true - }, completion: nil) - } - } - } - - self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - @objc func buttonPressed() { - self.pressed?() - } - - func startShimmering() { - guard let titleColor = self.titleColor else { - return - } - self.shimmerEffectNode.isHidden = false - self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - - let backgroundFrame = self.backgroundNode.frame - self.shimmerEffectNode.frame = backgroundFrame - self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size) - self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: titleColor.withAlphaComponent(0.3), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) - } - - func stopShimmering() { - self.shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in - self?.shimmerEffectNode.isHidden = true - }) - } - - static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage, _ highlightedImage: UIImage, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor, _ inProgress: Bool) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) { - let previousRegularImage = current?.regularImage - let previousHighlightedImage = current?.highlightedImage - let previousRegularIconImage = current?.regularIconImage - let previousHighlightedIconImage = current?.highlightedIconImage - - let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) - let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout) - - return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, cornerIcon, title, titleColor, highlightedTitleColor, inProgress in - let targetNode: ChatMessageAttachedContentButtonNode - if let current = current { - targetNode = current - } else { - targetNode = ChatMessageAttachedContentButtonNode() - } - - let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) - if let maybeMakeTextLayout = maybeMakeTextLayout { - makeTextLayout = maybeMakeTextLayout - } else { - makeTextLayout = TextNode.asyncLayout(targetNode.textNode) - } - - let makeHighlightedTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) - if let maybeMakeHighlightedTextLayout = maybeMakeHighlightedTextLayout { - makeHighlightedTextLayout = maybeMakeHighlightedTextLayout - } else { - makeHighlightedTextLayout = TextNode.asyncLayout(targetNode.highlightedTextNode) - } - - var updatedRegularImage: UIImage? - if regularImage !== previousRegularImage { - updatedRegularImage = regularImage - } - - var updatedHighlightedImage: UIImage? - if highlightedImage !== previousHighlightedImage { - updatedHighlightedImage = highlightedImage - } - - var updatedRegularIconImage: UIImage? - if iconImage !== previousRegularIconImage { - updatedRegularIconImage = iconImage - } - - var updatedHighlightedIconImage: UIImage? - if highlightedIconImage !== previousHighlightedIconImage { - updatedHighlightedIconImage = highlightedIconImage - } - - var iconWidth: CGFloat = 0.0 - if let iconImage = iconImage { - iconWidth = iconImage.size.width + 5.0 - } - - let labelInset: CGFloat = 8.0 - - let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) - - let (_, highlightedTextApply) = makeHighlightedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: highlightedTitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) - - return (textSize.size.width + labelInset * 2.0, { refinedWidth in - return (CGSize(width: refinedWidth, height: 33.0), { - targetNode.accessibilityLabel = title - - targetNode.titleColor = titleColor - - if let updatedRegularImage = updatedRegularImage { - targetNode.regularImage = updatedRegularImage - if !targetNode.textNode.isHidden { - targetNode.backgroundNode.image = updatedRegularImage - } - } - if let updatedHighlightedImage = updatedHighlightedImage { - targetNode.highlightedImage = updatedHighlightedImage - if targetNode.textNode.isHidden { - targetNode.backgroundNode.image = updatedHighlightedImage - } - } - if let updatedRegularIconImage = updatedRegularIconImage { - targetNode.regularIconImage = updatedRegularIconImage - if !targetNode.textNode.isHidden { - targetNode.iconNode.image = updatedRegularIconImage - } - } - if let updatedHighlightedIconImage = updatedHighlightedIconImage { - targetNode.highlightedIconImage = updatedHighlightedIconImage - if targetNode.iconNode.isHidden { - targetNode.iconNode.image = updatedHighlightedIconImage - } - } - - let _ = textApply() - let _ = highlightedTextApply() - - let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: 33.0)) - var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((34.0 - textSize.size.height) / 2.0)), size: textSize.size) - targetNode.backgroundNode.frame = backgroundFrame - if let image = targetNode.iconNode.image { - if cornerIcon { - targetNode.iconNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size) - } else { - textFrame.origin.x += floor(image.size.width / 2.0) - targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + 2.0), size: image.size) - } - if targetNode.iconNode.supernode == nil { - targetNode.addSubnode(targetNode.iconNode) - } - } else if targetNode.iconNode.supernode != nil { - targetNode.iconNode.removeFromSupernode() - } - - targetNode.textNode.frame = textFrame - targetNode.highlightedTextNode.frame = targetNode.textNode.frame - - return targetNode - }) - }) - } - } -} - -final class ChatMessageAttachedContentNode: ASDisplayNode { - private let lineNode: ASImageNode - private let topTitleNode: TextNode - private let textNode: TextNodeWithEntities - private let inlineImageNode: TransformImageNode - private var contentImageNode: ChatMessageInteractiveMediaNode? - private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode? - private var contentFileNode: ChatMessageInteractiveFileNode? - private var buttonNode: ChatMessageAttachedContentButtonNode? - - let statusNode: ChatMessageDateAndStatusNode - private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge? - private var linkHighlightingNode: LinkHighlightingNode? - - private var context: AccountContext? - private var message: Message? - private var media: Media? - private var theme: ChatPresentationThemeData? - - var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)? - var activateAction: (() -> Void)? - var requestUpdateLayout: (() -> Void)? - - var visibility: ListViewItemNodeVisibility = .none { - didSet { - if oldValue != self.visibility { - self.contentImageNode?.visibility = self.visibility != .none - self.contentInstantVideoNode?.visibility = self.visibility != .none - - switch self.visibility { - case .none: - self.textNode.visibilityRect = nil - case let .visible(_, subRect): - var subRect = subRect - subRect.origin.x = 0.0 - subRect.size.width = 10000.0 - self.textNode.visibilityRect = subRect - } - } - } - } - - override init() { - self.lineNode = ASImageNode() - self.lineNode.isLayerBacked = true - self.lineNode.displaysAsynchronously = false - self.lineNode.displayWithoutProcessing = true - - self.topTitleNode = TextNode() - self.topTitleNode.isUserInteractionEnabled = false - self.topTitleNode.displaysAsynchronously = false - self.topTitleNode.contentsScale = UIScreenScale - self.topTitleNode.contentMode = .topLeft - - self.textNode = TextNodeWithEntities() - self.textNode.textNode.isUserInteractionEnabled = false - self.textNode.textNode.displaysAsynchronously = false - self.textNode.textNode.contentsScale = UIScreenScale - self.textNode.textNode.contentMode = .topLeft - - self.inlineImageNode = TransformImageNode() - self.inlineImageNode.contentAnimations = [.subsequentUpdates] - self.inlineImageNode.isLayerBacked = !smartInvertColorsEnabled() - self.inlineImageNode.displaysAsynchronously = false - - self.statusNode = ChatMessageDateAndStatusNode() - - super.init() - - self.addSubnode(self.lineNode) - self.addSubnode(self.topTitleNode) - self.addSubnode(self.textNode.textNode) - - self.addSubnode(self.statusNode) - } - - func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - let topTitleAsyncLayout = TextNode.asyncLayout(self.topTitleNode) - let textAsyncLayout = TextNodeWithEntities.asyncLayout(self.textNode) - let currentImage = self.media as? TelegramMediaImage - let imageLayout = self.inlineImageNode.asyncLayout() - let statusLayout = self.statusNode.asyncLayout() - let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode) - let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode) - let contentInstantVideoLayout = ChatMessageInteractiveInstantVideoNode.asyncLayout(self.contentInstantVideoNode) - - let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) - - let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode - - return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in - let isPreview = presentationData.isPreview - let fontSize: CGFloat - if message.adAttribute != nil { - fontSize = floor(presentationData.fontSize.baseDisplaySize) - } else { - fontSize = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0) - } - - let titleFont = Font.semibold(fontSize) - let textFont = Font.regular(fontSize) - let textBoldFont = Font.semibold(fontSize) - let textItalicFont = Font.italic(fontSize) - let textBoldItalicFont = Font.semiboldItalic(fontSize) - let textFixedFont = Font.regular(fontSize) - let textBlockQuoteFont = Font.regular(fontSize) - - var incoming = message.effectivelyIncoming(context.account.peerId) - if case .forwardedMessages = associatedData.subject { - incoming = false - } - - var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0) - if displayLine { - horizontalInsets.left += 12.0 - } - - var titleBeforeMedia = false - var preferMediaBeforeText = false - var preferMediaAspectFilled = false - if let (_, flags) = mediaAndFlags { - preferMediaBeforeText = flags.contains(.preferMediaBeforeText) - preferMediaAspectFilled = flags.contains(.preferMediaAspectFilled) - titleBeforeMedia = flags.contains(.titleBeforeMedia) - } - - var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit - - var edited = false - if attributes.updatingMedia != nil { - edited = true - } - var viewCount: Int? - var dateReplies = 0 - var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message) - if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) { - dateReactionsAndPeers = ([], []) - } - for attribute in message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - edited = !attribute.isHidden - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - dateReplies = Int(attribute.count) - } - } - } - - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData) - - var webpageGalleryMediaCount: Int? - for media in message.media { - if let media = media as? TelegramMediaWebpage { - if case let .Loaded(content) = media.content, let instantPage = content.instantPage, let image = content.image { - switch instantPageType(of: content) { - case .album: - let count = instantPageGalleryMedia(webpageId: media.webpageId, page: instantPage, galleryMedia: image).count - if count > 1 { - webpageGalleryMediaCount = count - } - default: - break - } - } - } - } - - var textString: NSAttributedString? - var inlineImageDimensions: CGSize? - var inlineImageSize: CGSize? - var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var textCutout = TextNodeCutout() - var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude - var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? - var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)))? - - var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)? - - let topTitleString = NSMutableAttributedString() - - let string = NSMutableAttributedString() - var notEmpty = false - - let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing - - if let title = title, !title.isEmpty { - if titleBeforeMedia { - topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) - } else { - string.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) - notEmpty = true - } - } - - if let subtitle = subtitle, subtitle.length > 0 { - if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) - } - let updatedSubtitle = NSMutableAttributedString() - updatedSubtitle.append(subtitle) - updatedSubtitle.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length)) - updatedSubtitle.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length)) - string.append(updatedSubtitle) - notEmpty = true - } - - if let text = text, !text.isEmpty { - if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) - } - if let entities = entities { - string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil)) - } else { - string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: messageTheme.primaryTextColor)) - } - notEmpty = true - } - - textString = string - if string.length > 1000 { - textString = string.attributedSubstring(from: NSMakeRange(0, 1000)) - } - - var isReplyThread = false - if case .replyThread = chatLocation { - isReplyThread = true - } - - var skipStandardStatus = false - var isImage = false - var isFile = false - - var automaticPlayback = false - - var textStatusType: ChatMessageDateAndStatusType? - var imageStatusType: ChatMessageDateAndStatusType? - var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? - - if let (media, flags) = mediaAndFlags { - if let file = media as? TelegramMediaFile { - if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - isImage = true - } else if file.isInstantVideo { - isImage = true - } else if file.isVideo { - isImage = true - } else if file.isSticker || file.isAnimatedSticker { - isImage = true - } else { - isFile = true - } - } else if let _ = media as? TelegramMediaImage { - if !flags.contains(.preferMediaInline) { - isImage = true - } - } else if let _ = media as? TelegramMediaWebFile { - isImage = true - } else if let _ = media as? WallpaperPreviewMedia { - isImage = true - } else if let _ = media as? TelegramMediaStory { - isImage = true - } - } - - if preferMediaBeforeText, let textString, textString.length != 0 { - isImage = false - } - - var statusInText = !isImage - if let textString { - if textString.length == 0 { - statusInText = false - } - } else { - statusInText = false - } - - switch preparePosition { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - if let count = webpageGalleryMediaCount { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil) - skipStandardStatus = isImage - } else if let mediaBadge = mediaBadge { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil) - } else { - skipStandardStatus = isFile - } - - if !skipStandardStatus { - if incoming { - if isImage { - imageStatusType = .ImageIncoming - } else { - textStatusType = .BubbleIncoming - } - } else { - if message.flags.contains(.Failed) { - if isImage { - imageStatusType = .ImageOutgoing(.Failed) - } else { - textStatusType = .BubbleOutgoing(.Failed) - } - } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { - if isImage { - imageStatusType = .ImageOutgoing(.Sending) - } else { - textStatusType = .BubbleOutgoing(.Sending) - } - } else { - if isImage { - imageStatusType = .ImageOutgoing(.Sent(read: messageRead)) - } else { - textStatusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } - } - } - default: - break - } - - let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in - ChatMessageDateAndStatus( - type: statusType, - edited: edited, - viewCount: viewCount, - dateReactions: dateReactionsAndPeers.reactions, - dateReactionPeers: dateReactionsAndPeers.peers, - dateReplies: dateReplies, - isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - dateText: dateText - ) - } - - if let (media, flags) = mediaAndFlags { - if let file = media as? TelegramMediaFile { - if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if file.isInstantVideo { - let displaySize = CGSize(width: 212.0, height: 212.0) - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, topMessage: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload, 0.0) - initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight - contentInstantVideoSizeAndApply = (videoLayout, apply) - } else if file.isVideo { - var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none - - if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { - automaticDownload = .full - } else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) { - automaticDownload = .prefetch - } - if file.isAnimated { - automaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif - } else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo { - var willDownloadOrLocal = false - if case .full = automaticDownload { - willDownloadOrLocal = true - } else { - willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil - } - if willDownloadOrLocal { - automaticPlayback = true - contentMode = .aspectFill - } - } - - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if file.isSticker || file.isAnimatedSticker { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - - let statusType: ChatMessageDateAndStatusType - if incoming { - statusType = .BubbleIncoming - } else { - if message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { - statusType = .BubbleOutgoing(.Sending) - } else { - statusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } - - let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments( - context: context, - presentationData: presentationData, - message: message, - topMessage: message, - associatedData: associatedData, - chatLocation: chatLocation, - attributes: attributes, - isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - forcedIsEdited: false, - file: file, - automaticDownload: automaticDownload, - incoming: incoming, - isRecentActions: false, - forcedResourceStatus: associatedData.forcedResourceStatus, - dateAndStatusType: statusType, - displayReactions: false, - messageSelection: nil, - layoutConstants: layoutConstants, - constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height), - controllerInteraction: controllerInteraction - )) - refineContentFileLayout = refineLayout - } - } else if let image = media as? TelegramMediaImage { - if !flags.contains(.preferMediaInline) { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { - inlineImageDimensions = dimensions.cgSize - - if image != currentImage { - updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) - } - } - } else if let image = media as? TelegramMediaWebFile { - let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } else if let wallpaper = media as? WallpaperPreviewMedia { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { - skipStandardStatus = true - } - } else if let story = media as? TelegramMediaStory { - var media: Media? - if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue { - media = item.media - } - - var automaticDownload = false - if let media { - automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media) - } - - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) - initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right - refineContentImageLayout = refineLayout - } - } - - if let _ = inlineImageDimensions { - inlineImageSize = CGSize(width: 54.0, height: 54.0) - - if let inlineImageSize = inlineImageSize { - textCutout.topRight = CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0) - } - } - - return (initialWidth, { constrainedSize, position in - var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 5.0, right: horizontalInsets.right) - var lineInsets = insets - switch position { - case .linear(.None, _): - insets.top += 8.0 - lineInsets.top += 8.0 + 8.0 - default: - break - } - - let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - - var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? - if let _ = additionalImageBadgeContent { - updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge() - } - - let upatedTextCutout = textCutout - - - let (topTitleLayout, topTitleApply) = topTitleAsyncLayout(TextNodeLayoutArguments(attributedString: topTitleString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets())) - - var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? - if statusInText, let textStatusType = textStatusType { - let trailingContentWidth: CGFloat - if textLayout.hasRTL { - trailingContentWidth = 10000.0 - } else { - trailingContentWidth = textLayout.trailingLineWidth - } - statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( - context: context, - presentationData: presentationData, - edited: edited, - impressionCount: viewCount, - dateText: dateText, - type: textStatusType, - layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), - constrainedSize: textConstrainedSize, - availableReactions: associatedData.availableReactions, - reactions: dateReactionsAndPeers.reactions, - reactionPeers: dateReactionsAndPeers.peers, - displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser, - replyCount: dateReplies, - isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message), - animationCache: controllerInteraction.presentationContext.animationCache, - animationRenderer: controllerInteraction.presentationContext.animationRenderer - )) - } - let _ = statusSuggestedWidthAndContinue - - var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) - - textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) - - let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(presentationData.theme.theme) - - var boundingSize = textFrame.size - var lineHeight = textLayout.rawTextSize.height - if titleBeforeMedia { - lineHeight += topTitleLayout.size.height + 4.0 - boundingSize.height += topTitleLayout.size.height + 4.0 - boundingSize.width = max(boundingSize.width, topTitleLayout.size.width) - } - if let inlineImageSize = inlineImageSize { - if boundingSize.height < inlineImageSize.height { - boundingSize.height = inlineImageSize.height - } - if lineHeight < inlineImageSize.height { - lineHeight = inlineImageSize.height - } - } - - if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { - boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) - } - - var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))? - if let refineContentImageLayout = refineContentImageLayout { - let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0)) - finalizeContentImageLayout = finalizeImageLayout - - boundingSize.width = max(boundingSize.width, refinedWidth) - } - var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode))? - if let refineContentFileLayout = refineContentFileLayout { - let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize) - finalizeContentFileLayout = finalizeFileLayout - - boundingSize.width = max(boundingSize.width, refinedWidth) - } - - if let (videoLayout, _) = contentInstantVideoSizeAndApply { - boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight) - } - - lineHeight += lineInsets.top + lineInsets.bottom - - var imageApply: (() -> Void)? - if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions { - let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) - let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor) - imageApply = imageLayout(arguments) - } - - var continueActionButtonLayout: ((CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))? - if let actionTitle = actionTitle, !isPreview { - let buttonImage: UIImage - let buttonHighlightedImage: UIImage - var buttonIconImage: UIImage? - var buttonHighlightedIconImage: UIImage? - var cornerIcon = false - let titleColor: UIColor - let titleHighlightedColor: UIColor - if incoming { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme.theme)! - if let actionIcon { - switch actionIcon { - case .instant: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - case .link: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - cornerIcon = true - } - } - titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] - } else { - buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme.theme)! - buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme.theme)! - if let actionIcon { - switch actionIcon { - case .instant: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - case .link: - buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)! - buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! - cornerIcon = true - } - } - titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor - let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) - titleHighlightedColor = bubbleColor.fill[0] - } - let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, cornerIcon, actionTitle, titleColor, titleHighlightedColor, false) - boundingSize.width = max(buttonWidth, boundingSize.width) - continueActionButtonLayout = continueLayout - } - - boundingSize.width += insets.left + insets.right - boundingSize.height += insets.top + insets.bottom - - return (boundingSize.width, { boundingWidth in - var adjustedBoundingSize = boundingSize - var adjustedLineHeight = lineHeight - - var imageFrame: CGRect? - if let inlineImageSize = inlineImageSize { - imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize) - } - - var contentImageSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)? - if let finalizeContentImageLayout = finalizeContentImageLayout { - let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right) - contentImageSizeAndApply = (size, apply) - - var imageHeightAddition = size.height - if textFrame.size.height > CGFloat.ulpOfOne { - imageHeightAddition += 2.0 - } - - adjustedBoundingSize.height += imageHeightAddition + 5.0 - adjustedLineHeight += imageHeightAddition + 4.0 - } - - var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)? - if let finalizeContentFileLayout = finalizeContentFileLayout { - let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right) - contentFileSizeAndApply = (size, apply) - - var imageHeightAddition = size.height + 6.0 - if textFrame.size.height > CGFloat.ulpOfOne { - imageHeightAddition += 6.0 - } else { - imageHeightAddition += 7.0 - } - - adjustedBoundingSize.height += imageHeightAddition + 5.0 - adjustedLineHeight += imageHeightAddition + 4.0 - } - - if let (videoLayout, _) = contentInstantVideoSizeAndApply { - let imageHeightAddition = videoLayout.contentSize.height + 6.0 - if textFrame.size.height > CGFloat.ulpOfOne { - //imageHeightAddition += 2.0 - } - - adjustedBoundingSize.height += imageHeightAddition// + 5.0 - adjustedLineHeight += imageHeightAddition// + 4.0 - } - - var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))? - if let continueActionButtonLayout = continueActionButtonLayout { - let (size, apply) = continueActionButtonLayout(boundingWidth - 12.0 - insets.right) - actionButtonSizeAndApply = (size, apply) - adjustedBoundingSize.width = max(adjustedBoundingSize.width, insets.left + size.width + insets.right) - adjustedBoundingSize.height += 7.0 + size.height - } - - var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)? - if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { - statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right) - } - if let statusSizeAndApply = statusSizeAndApply { - adjustedBoundingSize.height += statusSizeAndApply.0.height - adjustedLineHeight += statusSizeAndApply.0.height - - if let imageFrame = imageFrame, statusSizeAndApply.0.height == 0.0 { - if statusInText { - adjustedBoundingSize.height = max(adjustedBoundingSize.height, imageFrame.maxY + 8.0 + 15.0) - } - } - } - - adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width) - - var contentMediaHeight: CGFloat? - if let (contentImageSize, _) = contentImageSizeAndApply { - contentMediaHeight = contentImageSize.height - } - - if let (contentFileSize, _) = contentFileSizeAndApply { - contentMediaHeight = contentFileSize.height - } - - if let (videoLayout, _) = contentInstantVideoSizeAndApply { - contentMediaHeight = videoLayout.contentSize.height - } - - var textVerticalOffset: CGFloat = 0.0 - if titleBeforeMedia { - textVerticalOffset += topTitleLayout.size.height + 4.0 - } - if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - textVerticalOffset += contentMediaHeight + 7.0 - } - let adjustedTextFrame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) - - var statusFrame: CGRect? - if let statusSizeAndApply = statusSizeAndApply { - var finalStatusFrame = CGRect(origin: CGPoint(x: adjustedTextFrame.minX, y: adjustedTextFrame.maxY), size: statusSizeAndApply.0) - if let imageFrame = imageFrame { - if finalStatusFrame.maxY < imageFrame.maxY + 10.0 { - finalStatusFrame.origin.y = max(finalStatusFrame.minY, imageFrame.maxY + 2.0) - if finalStatusFrame.height == 0.0 { - finalStatusFrame.origin.y += 14.0 - - adjustedBoundingSize.height += 14.0 - adjustedLineHeight += 14.0 - } - } - } - statusFrame = finalStatusFrame - } - - return (adjustedBoundingSize, { [weak self] animation, synchronousLoads, applyInfo in - if let strongSelf = self { - strongSelf.context = context - strongSelf.message = message - strongSelf.media = mediaAndFlags?.0 - strongSelf.theme = presentationData.theme - - strongSelf.lineNode.image = lineImage - animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)), completion: nil) - strongSelf.lineNode.isHidden = !displayLine - - strongSelf.textNode.textNode.displaysAsynchronously = !isPreview - - let _ = topTitleApply() - strongSelf.topTitleNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: insets.top), size: topTitleLayout.size) - - let _ = textApply(TextNodeWithEntities.Arguments( - context: context, - cache: animationCache, - renderer: animationRenderer, - placeholderColor: messageTheme.mediaPlaceholderColor, - attemptSynchronous: synchronousLoads - )) - switch strongSelf.visibility { - case .none: - strongSelf.textNode.visibilityRect = nil - case let .visible(_, subRect): - var subRect = subRect - subRect.origin.x = 0.0 - subRect.size.width = 10000.0 - strongSelf.textNode.visibilityRect = subRect - } - - if let imageFrame = imageFrame { - if let updateImageSignal = updateInlineImageSignal { - strongSelf.inlineImageNode.setSignal(updateImageSignal) - } - animation.animator.updateFrame(layer: strongSelf.inlineImageNode.layer, frame: imageFrame, completion: nil) - if strongSelf.inlineImageNode.supernode == nil { - strongSelf.addSubnode(strongSelf.inlineImageNode) - } - - if let imageApply = imageApply { - imageApply() - } - } else if strongSelf.inlineImageNode.supernode != nil { - strongSelf.inlineImageNode.removeFromSupernode() - } - - if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { - let contentImageNode = contentImageApply(animation, synchronousLoads) - if strongSelf.contentImageNode !== contentImageNode { - strongSelf.contentImageNode = contentImageNode - contentImageNode.activatePinch = { sourceNode in - controllerInteraction.activateMessagePinch(sourceNode) - } - strongSelf.addSubnode(contentImageNode) - contentImageNode.activateLocalContent = { [weak strongSelf] mode in - if let strongSelf = strongSelf { - strongSelf.openMedia?(mode) - } - } - contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in - guard let controllerInteraction = controllerInteraction else { - return - } - controllerInteraction.updateMessageReaction(message, value) - } - contentImageNode.visibility = strongSelf.visibility != .none - } - let _ = contentImageApply(animation, synchronousLoads) - var contentImageFrame: CGRect - if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) - if titleBeforeMedia { - contentImageFrame.origin.y += topTitleLayout.size.height + 4.0 - } - } else { - contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize) - } - - contentImageNode.frame = contentImageFrame - } else if let contentImageNode = strongSelf.contentImageNode { - contentImageNode.visibility = false - contentImageNode.removeFromSupernode() - strongSelf.contentImageNode = nil - } - - if let updatedAdditionalImageBadge = updatedAdditionalImageBadge, let contentImageNode = strongSelf.contentImageNode, let contentImageSize = contentImageSizeAndApply?.0 { - if strongSelf.additionalImageBadgeNode != updatedAdditionalImageBadge { - strongSelf.additionalImageBadgeNode?.removeFromSupernode() - } - strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge - contentImageNode.addSubnode(updatedAdditionalImageBadge) - if mediaBadge != nil { - updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, animated: false) - updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 0.0, height: 0.0)) - } else { - updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, alignment: .right, animated: false) - updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 6.0, y: contentImageSize.height - 18.0 - 6.0), size: CGSize(width: 0.0, height: 0.0)) - } - } else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode { - strongSelf.additionalImageBadgeNode = nil - additionalImageBadgeNode.removeFromSupernode() - } - - if let (contentFileSize, contentFileApply) = contentFileSizeAndApply { - let contentFileNode = contentFileApply(synchronousLoads, animation, applyInfo) - if strongSelf.contentFileNode !== contentFileNode { - strongSelf.contentFileNode = contentFileNode - strongSelf.addSubnode(contentFileNode) - contentFileNode.activateLocalContent = { [weak strongSelf] in - if let strongSelf = strongSelf { - strongSelf.openMedia?(.default) - } - } - contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in - if let strongSelf = strongSelf { - strongSelf.requestUpdateLayout?() - } - } - } - if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize) - } else { - contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 8.0 : 7.0)), size: contentFileSize) - } - } else if let contentFileNode = strongSelf.contentFileNode { - contentFileNode.removeFromSupernode() - strongSelf.contentFileNode = nil - } - - if let (videoLayout, apply) = contentInstantVideoSizeAndApply { - let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation) - if strongSelf.contentInstantVideoNode !== contentInstantVideoNode { - strongSelf.contentInstantVideoNode = contentInstantVideoNode - strongSelf.addSubnode(contentInstantVideoNode) - } - if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: videoLayout.contentSize) - } else { - contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: videoLayout.contentSize) - } - } else if let contentInstantVideoNode = strongSelf.contentInstantVideoNode { - contentInstantVideoNode.removeFromSupernode() - strongSelf.contentInstantVideoNode = nil - } - - strongSelf.textNode.textNode.frame = adjustedTextFrame - if let statusSizeAndApply = statusSizeAndApply, let statusFrame = statusFrame { - if strongSelf.statusNode.supernode == nil { - strongSelf.addSubnode(strongSelf.statusNode) - strongSelf.statusNode.frame = statusFrame - statusSizeAndApply.1(.None) - } else { - animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil) - statusSizeAndApply.1(animation) - } - } else if strongSelf.statusNode.supernode != nil { - strongSelf.statusNode.removeFromSupernode() - } - - if let (size, apply) = actionButtonSizeAndApply { - let buttonNode = apply() - if buttonNode !== strongSelf.buttonNode { - strongSelf.buttonNode?.removeFromSupernode() - strongSelf.buttonNode = buttonNode - strongSelf.addSubnode(buttonNode) - buttonNode.pressed = { - if let strongSelf = self { - strongSelf.activateAction?() - } - } - } - buttonNode.frame = CGRect(origin: CGPoint(x: 12.0, y: adjustedLineHeight - insets.top - insets.bottom - 2.0 + 6.0), size: size) - } else if let buttonNode = strongSelf.buttonNode { - buttonNode.removeFromSupernode() - strongSelf.buttonNode = nil - } - } - }) - }) - }) - } - } - - func updateHiddenMedia(_ media: [Media]?) -> Bool { - if let currentMedia = self.media { - if let media = media { - var found = false - for m in media { - if currentMedia.isEqual(to: m) { - found = true - break - } - } - if let contentImageNode = self.contentImageNode { - contentImageNode.isHidden = found - contentImageNode.updateIsHidden(found) - return found - } - } else if let contentImageNode = self.contentImageNode { - contentImageNode.isHidden = false - contentImageNode.updateIsHidden(false) - } - } - return false - } - - func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { - if let contentImageNode = self.contentImageNode, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) { - return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in - return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) - }) - } else if let contentImageNode = self.contentImageNode, let file = self.media as? TelegramMediaFile, file.isEqual(to: media) { - return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in - return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) - }) - } else if let contentImageNode = self.contentImageNode, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) { - return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in - return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) - }) - } - return nil - } - - func hasActionAtPoint(_ point: CGPoint) -> Bool { - if let buttonNode = self.buttonNode, buttonNode.frame.contains(point) { - return true - } - return false - } - - func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - let textNodeFrame = self.textNode.textNode.frame - if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) - } - return .url(url: url, concealed: concealed) - } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) - } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .textMention(peerName) - } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - return .botCommand(botCommand) - } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) - } else { - return .none - } - } else { - return .none - } - } - - func updateTouchesAtPoint(_ point: CGPoint?) { - if let context = self.context, let message = self.message, let theme = self.theme { - var rects: [CGRect]? - if let point = point { - let textNodeFrame = self.textNode.textNode.frame - if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - let possibleNames: [String] = [ - TelegramTextAttributes.URL, - TelegramTextAttributes.PeerMention, - TelegramTextAttributes.PeerTextMention, - TelegramTextAttributes.BotCommand, - TelegramTextAttributes.Hashtag, - TelegramTextAttributes.BankCard - ] - for name in possibleNames { - if let _ = attributes[NSAttributedString.Key(rawValue: name)] { - rects = self.textNode.textNode.attributeRects(name: name, at: index) - break - } - } - } - } - - if let rects = rects { - let linkHighlightingNode: LinkHighlightingNode - if let current = self.linkHighlightingNode { - linkHighlightingNode = current - } else { - linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor) - self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) - } - linkHighlightingNode.frame = self.textNode.textNode.frame - linkHighlightingNode.updateRects(rects) - } else if let linkHighlightingNode = self.linkHighlightingNode { - self.linkHighlightingNode = nil - linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in - linkHighlightingNode?.removeFromSupernode() - }) - } - } - } - - func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.statusNode.isHidden { - if let result = self.statusNode.reactionView(value: value) { - return result - } - } - if let result = self.contentFileNode?.dateAndStatusNode.reactionView(value: value) { - return result - } - if let result = self.contentImageNode?.dateAndStatusNode.reactionView(value: value) { - return result - } - if let result = self.contentInstantVideoNode?.dateAndStatusNode.reactionView(value: value) { - return result - } - return nil - } - - func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { - return self.contentImageNode?.playMediaWithSound() - } -} diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift deleted file mode 100644 index dc5858ffd24..00000000000 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ /dev/null @@ -1,264 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import Postbox -import TelegramCore -import TelegramUIPreferences -import TelegramPresentationData -import AccountContext -import ChatMessageBackground -import ChatControllerInteraction - -enum ChatMessageBubbleContentBackgroundHiding { - case never - case emptyWallpaper - case always -} - -enum ChatMessageBubbleContentAlignment { - case none - case center -} - -struct ChatMessageBubbleContentProperties { - let hidesSimpleAuthorHeader: Bool - let headerSpacing: CGFloat - let hidesBackground: ChatMessageBubbleContentBackgroundHiding - let forceFullCorners: Bool - let forceAlignment: ChatMessageBubbleContentAlignment - let shareButtonOffset: CGPoint? - let hidesHeaders: Bool - let avatarOffset: CGFloat? - - init( - hidesSimpleAuthorHeader: Bool, - headerSpacing: CGFloat, - hidesBackground: ChatMessageBubbleContentBackgroundHiding, - forceFullCorners: Bool, - forceAlignment: ChatMessageBubbleContentAlignment, - shareButtonOffset: CGPoint? = nil, - hidesHeaders: Bool = false, - avatarOffset: CGFloat? = nil - ) { - self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader - self.headerSpacing = headerSpacing - self.hidesBackground = hidesBackground - self.forceFullCorners = forceFullCorners - self.forceAlignment = forceAlignment - self.shareButtonOffset = shareButtonOffset - self.hidesHeaders = hidesHeaders - self.avatarOffset = avatarOffset - } -} - -enum ChatMessageBubbleNoneMergeStatus { - case Incoming - case Outgoing - case None -} - -enum ChatMessageBubbleMergeStatus { - case None(ChatMessageBubbleNoneMergeStatus) - case Left - case Right - case Both -} - -enum ChatMessageBubbleRelativePosition { - enum NeighbourType { - case media - case freeform - } - - enum NeighbourSpacing { - case `default` - case condensed - case overlap(CGFloat) - } - - case None(ChatMessageBubbleMergeStatus) - case BubbleNeighbour - case Neighbour(Bool, NeighbourType, NeighbourSpacing) -} - -enum ChatMessageBubbleContentMosaicNeighbor { - case merged - case mergedBubble - case none(tail: Bool) -} - -struct ChatMessageBubbleContentMosaicPosition { - let topLeft: ChatMessageBubbleContentMosaicNeighbor - let topRight: ChatMessageBubbleContentMosaicNeighbor - let bottomLeft: ChatMessageBubbleContentMosaicNeighbor - let bottomRight: ChatMessageBubbleContentMosaicNeighbor -} - -enum ChatMessageBubbleContentPosition { - case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) - case mosaic(position: ChatMessageBubbleContentMosaicPosition, wide: Bool) -} - -enum ChatMessageBubblePreparePosition { - case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) - case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) -} - -enum ChatMessageBubbleContentTapAction { - case none - case url(url: String, concealed: Bool) - case textMention(String) - case peerMention(peerId: PeerId, mention: String, openProfile: Bool) - case botCommand(String) - case hashtag(String?, String) - case instantPage - case wallpaper - case theme - case call(peerId: PeerId, isVideo: Bool) - case openMessage - case timecode(Double, String) - case tooltip(String, ASDisplayNode?, CGRect?) - case bankCard(String) - case ignore - case openPollResults(Data) - case copy(String) - case largeEmoji(String, String?, TelegramMediaFile) - case customEmoji(TelegramMediaFile) -} - -final class ChatMessageBubbleContentItem { - let context: AccountContext - let controllerInteraction: ChatControllerInteraction - let message: Message - let topMessage: Message - let read: Bool - let chatLocation: ChatLocation - let presentationData: ChatPresentationData - let associatedData: ChatMessageItemAssociatedData - let attributes: ChatMessageEntryAttributes - let isItemPinned: Bool - let isItemEdited: Bool - - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) { - self.context = context - self.controllerInteraction = controllerInteraction - self.message = message - self.topMessage = topMessage - self.read = read - self.chatLocation = chatLocation - self.presentationData = presentationData - self.associatedData = associatedData - self.attributes = attributes - self.isItemPinned = isItemPinned - self.isItemEdited = isItemEdited - } -} - -class ChatMessageBubbleContentNode: ASDisplayNode { - var supportsMosaic: Bool { - return false - } - - weak var bubbleBackgroundNode: ChatMessageBackground? - weak var bubbleBackdropNode: ChatMessageBubbleBackdrop? - - var visibility: ListViewItemNodeVisibility = .none - - var item: ChatMessageBubbleContentItem? - - var updateIsTextSelectionActive: ((Bool) -> Void)? - - var disablesClipping: Bool { - return false - } - - required override init() { - super.init() - } - - func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - preconditionFailure() - } - - func animateInsertion(_ currentTimestamp: Double, duration: Double) { - } - - func animateAdded(_ currentTimestamp: Double, duration: Double) { - } - - func animateRemoved(_ currentTimestamp: Double, duration: Double) { - } - - func animateInsertionIntoBubble(_ duration: Double) { - } - - func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - completion() - }) - } - - func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { - return nil - } - - func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - return nil - } - - func updateHiddenMedia(_ media: [Media]?) -> Bool { - return false - } - - func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { - } - - func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) { - } - - func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { - return nil - } - - func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - return .none - } - - func updateTouchesAtPoint(_ point: CGPoint?) { - } - - func updateHighlightedState(animated: Bool) -> Bool { - return false - } - - func willUpdateIsExtractedToContextPreview(_ value: Bool) { - } - - func updateIsExtractedToContextPreview(_ value: Bool) { - } - - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - } - - func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { - } - - func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { - } - - func unreadMessageRangeUpdated() { - } - - func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - return nil - } - - func targetForStoryTransition(id: StoryId) -> UIView? { - return nil - } - - func getStatusNode() -> ASDisplayNode? { - return nil - } -} diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index cb99cd36d59..67be7252337 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -5,6 +5,7 @@ import ContextUI import Postbox import TelegramCore import SwiftSignalKit +import ChatMessageItemView final class ChatMessageContextLocationContentSource: ContextLocationContentSource { private let controller: ViewController @@ -20,7 +21,6 @@ final class ChatMessageContextLocationContentSource: ContextLocationContentSourc } } - final class ChatMessageContextExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = false diff --git a/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift index 507373f8a2b..0348668a848 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import AccountContext import AppBundle import ChatPresentationInterfaceState +import ChatInputPanelNode final class ChatMessageReportInputPanelNode: ChatInputPanelNode { private let reportButton: HighlightableButtonNode diff --git a/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift index e438cd25309..6b406dc5487 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import AccountContext import AppBundle import ChatPresentationInterfaceState +import ChatInputPanelNode final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: HighlightableButtonNode diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index c4430e54030..d2ebffd67e2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -12,6 +12,13 @@ import ReactionSelectionNode import ChatControllerInteraction import FeaturedStickersScreen import ChatTextInputMediaRecordingButton +import ReplyAccessoryPanelNode +import ChatMessageItemView +import ChatMessageStickerItemNode +import ChatMessageInstantVideoItemNode +import ChatMessageAnimatedStickerItemNode +import ChatMessageTransitionNode +import ChatMessageBubbleItemNode private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect { if let presentationLayer = fromView.layer.presentation() { @@ -96,7 +103,7 @@ private final class OverlayTransitionContainerController: ViewController, Standa } } -public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransitionProtocol { +public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTransitionNode, ChatMessageTransitionProtocol { static let animationDuration: Double = 0.3 static let verticalAnimationControlPoints: (Float, Float, Float, Float) = (0.19919472913616398, 0.010644531250000006, 0.27920937042459737, 0.91025390625) @@ -231,7 +238,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti case groupedMediaInput(GroupedMediaInput) } - final class DecorationItemNode: ASDisplayNode { + final class DecorationItemNodeImpl: ASDisplayNode, ChatMessageTransitionNode.DecorationItemNode { let itemNode: ChatMessageItemView let contentView: UIView private let getContentAreaInScreenSpace: () -> CGRect @@ -284,7 +291,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti private final class AnimatingItemNode: ASDisplayNode { let itemNode: ChatMessageItemView private let contextSourceNode: ContextExtractedContentContainingNode - private let source: ChatMessageTransitionNode.Source + private let source: ChatMessageTransitionNodeImpl.Source private let getContentAreaInScreenSpace: () -> CGRect private let scrollingContainer: ASDisplayNode @@ -296,7 +303,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti var animationEnded: (() -> Void)? var updateAfterCompletion: Bool = false - init(itemNode: ChatMessageItemView, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNode.Source, getContentAreaInScreenSpace: @escaping () -> CGRect) { + init(itemNode: ChatMessageItemView, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNodeImpl.Source, getContentAreaInScreenSpace: @escaping () -> CGRect) { self.itemNode = itemNode self.getContentAreaInScreenSpace = getContentAreaInScreenSpace @@ -324,7 +331,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti } func beginAnimation() { - let verticalDuration: Double = ChatMessageTransitionNode.animationDuration + let verticalDuration: Double = ChatMessageTransitionNodeImpl.animationDuration let horizontalDuration: Double = verticalDuration let delay: Double = 0.0 @@ -363,7 +370,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti let sourceBackgroundAbsoluteRect = initialTextInput.backgroundView.frame.offsetBy(dx: sourceRect.minX, dy: sourceRect.minY) let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.minX, y: sourceBackgroundAbsoluteRect.maxY - self.contextSourceNode.contentRect.height), size: self.contextSourceNode.contentRect.size) - let textInput = ChatMessageTransitionNode.Source.TextInput(backgroundView: initialTextInput.backgroundView, contentView: initialTextInput.contentView, sourceRect: sourceRect, scrollOffset: initialTextInput.scrollOffset) + let textInput = ChatMessageTransitionNodeImpl.Source.TextInput(backgroundView: initialTextInput.backgroundView, contentView: initialTextInput.contentView, sourceRect: sourceRect, scrollOffset: initialTextInput.scrollOffset) textInput.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height), size: textInput.backgroundView.bounds.size) textInput.contentView.frame = textInput.contentView.frame.offsetBy(dx: 0.0, dy: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height) @@ -386,9 +393,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti self.itemNode.cancelInsertionAnimations() - let horizontalCurve = ChatMessageTransitionNode.horizontalAnimationCurve + let horizontalCurve = ChatMessageTransitionNodeImpl.horizontalAnimationCurve let horizontalTransition: ContainedViewLayoutTransition = .animated(duration: horizontalDuration, curve: horizontalCurve) - let verticalCurve = ChatMessageTransitionNode.verticalAnimationCurve + let verticalCurve = ChatMessageTransitionNodeImpl.verticalAnimationCurve let verticalTransition: ContainedViewLayoutTransition = .animated(duration: verticalDuration, curve: verticalCurve) let combinedTransition = CombinedTransition(horizontal: horizontalTransition, vertical: verticalTransition) @@ -406,19 +413,73 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), verticalCurve, verticalDuration) if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { - itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) + itemNode.animateContentFromTextInputField( + textInput: ChatMessageBubbleItemNode.AnimationTransitionTextInput( + backgroundView: textInput.backgroundView, + contentView: textInput.contentView, + sourceRect: textInput.sourceRect, + scrollOffset: textInput.scrollOffset + ), + transition: combinedTransition + ) if let sourceReplyPanel = sourceReplyPanel { - itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + itemNode.animateReplyPanel( + sourceReplyPanel: ChatMessageBubbleItemNode.AnimationTransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ), + transition: combinedTransition + ) } } else if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { - itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) + itemNode.animateContentFromTextInputField( + textInput: ChatMessageAnimatedStickerItemNode.AnimationTransitionTextInput( + backgroundView: textInput.backgroundView, + contentView: textInput.contentView, + sourceRect: textInput.sourceRect, + scrollOffset: textInput.scrollOffset + ), + transition: combinedTransition + ) if let sourceReplyPanel = sourceReplyPanel { - itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + itemNode.animateReplyPanel( + sourceReplyPanel: ChatMessageAnimatedStickerItemNode.AnimationTransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ), + transition: combinedTransition + ) } } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { - itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) + itemNode.animateContentFromTextInputField( + textInput: ChatMessageStickerItemNode.AnimationTransitionTextInput( + backgroundView: textInput.backgroundView, + contentView: textInput.contentView, + sourceRect: textInput.sourceRect, + scrollOffset: textInput.scrollOffset + ), + transition: combinedTransition + ) if let sourceReplyPanel = sourceReplyPanel { - itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + itemNode.animateReplyPanel( + sourceReplyPanel: ChatMessageStickerItemNode.AnimationTransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ), + transition: combinedTransition + ) } } case let .stickerMediaInput(stickerMediaInput, replyPanel): @@ -457,34 +518,72 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame, relativeTargetRect: replySourceAbsoluteFrame.offsetBy(dx: 0.0, dy: replySourceAbsoluteFrame.height)) } - let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { - itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition) + itemNode.animateContentFromStickerGridItem( + stickerSource: ChatMessageAnimatedStickerItemNode.AnimationTransitionSticker( + imageNode: stickerSource.imageNode, + animationNode: stickerSource.animationNode, + placeholderNode: stickerSource.placeholderNode, + imageLayer: stickerSource.imageLayer, + relativeSourceRect: stickerSource.relativeSourceRect + ), + transition: combinedTransition + ) if let sourceAnimationNode = stickerSource.animationNode { itemNode.animationNode?.setFrameIndex(sourceAnimationNode.currentFrameIndex) } if let sourceReplyPanel = sourceReplyPanel { - itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + itemNode.animateReplyPanel( + sourceReplyPanel: ChatMessageAnimatedStickerItemNode.AnimationTransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ), + transition: combinedTransition + ) } } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { - itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition) + itemNode.animateContentFromStickerGridItem( + stickerSource: ChatMessageStickerItemNode.AnimationTransitionSticker( + imageNode: stickerSource.imageNode, + animationNode: stickerSource.animationNode, + placeholderNode: stickerSource.placeholderNode, + imageLayer: stickerSource.imageLayer, + relativeSourceRect: stickerSource.relativeSourceRect + ), + transition: combinedTransition + ) if let sourceReplyPanel = sourceReplyPanel { - itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + itemNode.animateReplyPanel( + sourceReplyPanel: ChatMessageStickerItemNode.AnimationTransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ), + transition: combinedTransition + ) } } self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) - self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.endAnimation() }) - self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) - self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) - self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true) switch stickerMediaInput { case .inputPanel, .universal: @@ -503,7 +602,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti container.isHidden = true - let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { if let contextContainer = itemNode.animateFromMicInput(micInputNode: snapshotView, transition: combinedTransition) { @@ -513,7 +612,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -contextContainer.contentRect.minX, dy: -contextContainer.contentRect.minY) contextContainer.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) - self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self, weak contextContainer, weak container] _ in + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self, weak contextContainer, weak container] _ in guard let strongSelf = self else { return } @@ -528,13 +627,13 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti strongSelf.endAnimation() }) - self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true) } } } } case let .videoMessage(videoMessage): - let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageInstantVideoItemNode { itemNode.cancelInsertionAnimations() @@ -550,9 +649,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti videoMessage.view.frame = videoMessage.view.frame.offsetBy(dx: targetAbsoluteRect.midX - sourceAbsoluteRect.midX, dy: targetAbsoluteRect.midY - sourceAbsoluteRect.midY) self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) - self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) - self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, completion: { [weak self] _ in + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, completion: { [weak self] _ in guard let strongSelf = self else { return } @@ -577,7 +676,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti let sourceBackgroundAbsoluteRect = snapshotView.frame let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.midX - self.contextSourceNode.contentRect.size.width / 2.0, y: sourceBackgroundAbsoluteRect.midY - self.contextSourceNode.contentRect.size.height / 2.0), size: self.contextSourceNode.contentRect.size) - let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { itemNode.animateContentFromMediaInput(snapshotView: snapshotView, transition: combinedTransition) @@ -590,8 +689,8 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) - self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) - self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return } @@ -606,8 +705,8 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti snapshotView?.removeFromSuperview() }) - self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) - self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration) } } } else { @@ -628,7 +727,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti self.containerNode.addSubnode(self.contextSourceNode.contentNode) - let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) var targetContentRects: [CGRect] = [] if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { @@ -668,8 +767,8 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) - self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) - self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return } @@ -695,8 +794,8 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti index += 1 } - self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) - self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration) } } } @@ -797,7 +896,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti private var currentPendingItems: [Int64: (Source, () -> Void)] = [:] private var animatingItemNodes: [AnimatingItemNode] = [] - private var decorationItemNodes: [DecorationItemNode] = [] + private var decorationItemNodes: [DecorationItemNodeImpl] = [] private var messageReactionContexts: [MessageReactionContext] = [] var hasScheduledTransitions: Bool { @@ -850,8 +949,8 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti self.listNode.setCurrentSendAnimationCorrelationIds(correlationIds) } - func add(decorationView: UIView, itemNode: ChatMessageItemView) -> DecorationItemNode { - let decorationItemNode = DecorationItemNode(itemNode: itemNode, contentView: decorationView, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + public func add(decorationView: UIView, itemNode: ChatMessageItemView) -> DecorationItemNode { + let decorationItemNode = DecorationItemNodeImpl(itemNode: itemNode, contentView: decorationView, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) decorationItemNode.updateLayout(size: self.bounds.size) self.decorationItemNodes.append(decorationItemNode) @@ -866,10 +965,12 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti return decorationItemNode } - func remove(decorationNode: DecorationItemNode) { + public func remove(decorationNode: DecorationItemNode) { self.decorationItemNodes.removeAll(where: { $0 === decorationNode }) decorationNode.removeFromSupernode() - decorationNode.overlayController?.dismiss() + if let decorationNode = decorationNode as? DecorationItemNodeImpl { + decorationNode.overlayController?.dismiss() + } } private func beginAnimation(itemNode: ChatMessageItemView, source: Source) { @@ -920,7 +1021,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransiti animatingItemNode.frame = self.bounds animatingItemNode.beginAnimation() - self.onTransitionEvent(.animated(duration: ChatMessageTransitionNode.animationDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + self.onTransitionEvent(.animated(duration: ChatMessageTransitionNodeImpl.animationDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) } } diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 492b8e057f4..04eda7684bb 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -22,6 +22,7 @@ import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import TranslateUI +import ChatControllerInteraction private enum PinnedMessageAnimation { case slideToTop @@ -858,7 +859,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if url.hasPrefix("tg://") { isConcealed = false } - controllerInteraction.openUrl(url, isConcealed, nil, nil) + controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed)) case .requestMap: controllerInteraction.shareCurrentLocation() case .requestPhone: diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index f71207b47df..9ee9c0168be 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -13,9 +13,10 @@ import AnimationUI import ManagedAnimationNode import ChatPresentationInterfaceState import ChatSendButtonRadialStatusNode +import AudioWaveformNode +import ChatInputPanelNode extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { - } final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 25cad550c17..896d08eb1e2 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -8,6 +8,7 @@ import SwiftSignalKit import TelegramStringFormatting import ChatPresentationInterfaceState import TelegramPresentationData +import ChatInputPanelNode final class ChatRestrictedInputPanelNode: ChatInputPanelNode { private let textNode: ImmediateTextNode diff --git a/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift index 5f8c39dff30..c620d0e2c36 100644 --- a/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift @@ -9,6 +9,7 @@ import TelegramNotices import TelegramPresentationData import ActivityIndicator import ChatPresentationInterfaceState +import ChatInputPanelNode private let labelFont = Font.regular(15.0) diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 7507ddce47e..a14f0dc79ca 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -251,7 +251,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe switch item.content { case let .peer(peerData): if let message = peerData.messages.first { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: true, timecode: nil), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list([]))), gesture: gesture) presentInGlobalOverlay(contextController) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index dbe1ea37752..6dce3e7af77 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -38,6 +38,8 @@ import SolidRoundedButtonNode import TooltipUI import ChatTextInputMediaRecordingButton import ChatContextQuery +import ChatInputTextNode +import ChatInputPanelNode private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -468,9 +470,50 @@ final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayC } } -class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { +private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPresentationInterfaceState) -> ChatInputTextView.Theme { + let lineStyle: ChatInputTextView.Theme.Quote.LineStyle + let authorNameColor: UIColor + + if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info, let nameColor = peer.nameColor { + let colors = context.peerNameColors.get(nameColor) + authorNameColor = colors.main + + if let secondary = colors.secondary, let tertiary = colors.tertiary { + lineStyle = .tripleDashed(mainColor: colors.main, secondaryColor: secondary, tertiaryColor: tertiary) + } else if let secondary = colors.secondary { + lineStyle = .doubleDashed(mainColor: colors.main, secondaryColor: secondary) + } else { + lineStyle = .solid(color: colors.main) + } + } else if let accountPeerColor = interfaceState.accountPeerColor { + authorNameColor = interfaceState.theme.list.itemAccentColor + + switch accountPeerColor.style { + case .solid: + lineStyle = .solid(color: authorNameColor) + case .doubleDashed: + lineStyle = .doubleDashed(mainColor: authorNameColor, secondaryColor: .clear) + case .tripleDashed: + lineStyle = .tripleDashed(mainColor: authorNameColor, secondaryColor: .clear, tertiaryColor: .clear) + } + } else { + lineStyle = .solid(color: interfaceState.theme.list.itemAccentColor) + authorNameColor = interfaceState.theme.list.itemAccentColor + } + + return ChatInputTextView.Theme( + quote: ChatInputTextView.Theme.Quote( + background: authorNameColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1), + foreground: authorNameColor, + lineStyle: lineStyle + ) + ) +} + +class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { // MARK: Nicegram Send with Enter let sendWithKb: Bool + // let clippingNode: ASDisplayNode var textPlaceholderNode: ImmediateTextNode @@ -479,7 +522,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? let textInputContainerBackgroundNode: ASImageNode let textInputContainer: ASDisplayNode - var textInputNode: EditableTextNode? + var textInputNode: ChatInputTextNode? var dustNode: InvisibleInkDustNode? var customEmojiContainerView: CustomEmojiContainerView? @@ -560,7 +603,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var storedInputLanguage: String? var effectiveInputLanguage: String? { if let textInputNode = textInputNode, textInputNode.isFirstResponder() { - return textInputNode.textInputMode.primaryLanguage + return textInputNode.textInputMode?.primaryLanguage } else { return self.storedInputLanguage } @@ -683,7 +726,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) if let presentationInterfaceState = self.presentationInterfaceState { - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) } self.updatingInputState = false @@ -714,12 +757,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor) - self.editableTextNodeDidUpdateText(textInputNode) + self.chatInputTextNodeDidUpdateText() } } } - private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) + private let textInputViewInternalInsets: UIEdgeInsets private let accessoryButtonSpacing: CGFloat = 0.0 private let accessoryButtonInset: CGFloat = 2.0 @@ -737,6 +780,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void, sendWithKb: Bool = false) { self.presentationInterfaceState = presentationInterfaceState self.presentationContext = presentationContext + + self.textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) var hasSpoilers = true if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { @@ -1061,6 +1106,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.statusDisposable.dispose() self.startingBotDisposable.dispose() self.tooltipController?.dismiss() + self.currentEmojiSuggestion?.disposable.dispose() } func loadTextInputNodeIfNeeded() { @@ -1070,13 +1116,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } private func loadTextInputNode() { - let textInputNode = EditableChatTextNode() + let textInputNode = ChatInputTextNode() textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage var textColor: UIColor = .black var tintColor: UIColor = .blue var baseFontSize: CGFloat = 17.0 var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default - if let presentationInterfaceState = self.presentationInterfaceState { + if let context = self.context, let presentationInterfaceState = self.presentationInterfaceState { + textInputNode.textView.theme = makeTextInputTheme(context: context, interfaceState: presentationInterfaceState) + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor tintColor = presentationInterfaceState.theme.list.itemAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) @@ -1090,7 +1138,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { paragraphStyle.maximumLineHeight = 20.0 paragraphStyle.minimumLineHeight = 20.0 - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: paragraphStyle] textInputNode.clipsToBounds = false textInputNode.textView.clipsToBounds = false textInputNode.delegate = self @@ -1102,7 +1150,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true // MARK: Nicegram Send with Enter if self.sendWithKb { - textInputNode.returnKeyType = .send + textInputNode.textView.returnKeyType = .send } // MARK: textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled @@ -1121,7 +1169,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if let presentationInterfaceState = self.presentationInterfaceState { - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } @@ -1129,6 +1177,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textInputFrame = self.textInputContainer.frame textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + textInputNode.updateLayout(size: textInputNode.bounds.size) textInputNode.view.layoutIfNeeded() self.updateSpoiler() } @@ -1202,15 +1251,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } var textFieldMinHeight: CGFloat = 35.0 + var textInputViewRealInsets = UIEdgeInsets() if let presentationInterfaceState = self.presentationInterfaceState { textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics) } + if let presentationInterfaceState = self.presentationInterfaceState { + textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) + } + let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - let measuredHeight = textInputNode.measure(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) - let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight.height)) + let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: textInputViewRealInsets.right) + + let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight)) let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) @@ -1637,7 +1692,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.attributedText = updatedText textInputNode.selectedRange = range } - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor] self.updateSpoiler() } @@ -1649,6 +1704,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.tintColorDidChange() } + if let textInputNode = self.textInputNode, let context = self.context { + textInputNode.textView.theme = makeTextInputTheme(context: context, interfaceState: interfaceState) + } + let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance { if textInputNode.isFirstResponder() && textInputNode.isCurrentlyEmoji() { @@ -1715,7 +1774,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId - let replyMessageUpdated = interfaceState.interfaceState.replyMessageId != previousState?.interfaceState.replyMessageId + let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) { self.initializedPlaceholder = true @@ -1747,7 +1806,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if let keyboardButtonsMessage = interfaceState.keyboardButtonsMessage, interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != keyboardButtonsMessage.id { - if keyboardButtonsMessage.requestsSetupReply && keyboardButtonsMessage.id != interfaceState.interfaceState.replyMessageId { + if keyboardButtonsMessage.requestsSetupReply && keyboardButtonsMessage.id != interfaceState.interfaceState.replyMessageSubject?.messageId { } else { if let placeholderValue = interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder, !placeholderValue.isEmpty { placeholder = placeholderValue @@ -2303,7 +2362,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.textContainerInset = textInputViewRealInsets let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size - transition.updateFrame(node: textInputNode, frame: textFieldFrame) + //transition.updateFrame(node: textInputNode, frame: textFieldFrame) + textInputNode.frame = textFieldFrame + textInputNode.updateLayout(size: textFieldFrame.size) self.updateInputField(textInputFrame: textFieldFrame, transition: Transition(transition)) if shouldUpdateLayout { textInputNode.layout() @@ -2562,24 +2623,28 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return prevInputPanelNode is ChatRecordingPreviewInputPanelNode } - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoiler() let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) - self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode.primaryLanguage }) + self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage }) self.updateTextNodeText(animated: true) self.updateCounterTextNode(transition: .immediate) } } + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidUpdateText() + } + private func updateSpoiler() { guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { return @@ -2588,7 +2653,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor var rects: [CGRect] = [] - var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] + var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = [] let fontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) @@ -2635,7 +2700,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) { let textRects = textInputNode.textView.selectionRects(for: textRange) for textRect in textRects { - customEmojiRects.append((textRect.rect, value)) + var emojiFontSize = fontSize + if let font = attributes[.font] as? UIFont { + emojiFontSize = font.pointSize + } + customEmojiRects.append((textRect.rect, value, emojiFontSize)) break } } @@ -2727,7 +2796,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.textView.isScrollEnabled = false - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) @@ -2800,8 +2869,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var hasTracking = false var hasTrackingView = false - if textInputNode.selectedRange.length == 0 && textInputNode.selectedRange.location > 0 { - let selectedSubstring = textInputNode.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location)) + if textInputNode.selectedRange.length == 0, textInputNode.selectedRange.location > 0, let attributedText = textInputNode.textView.attributedText { + let selectedSubstring = attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location)) if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji { let queryLength = (String(lastCharacter) as NSString).length if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil { @@ -2832,6 +2901,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { beginRequest = true suggestionContext = CurrentEmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition, disposable: MetaDisposable(), value: nil) + + self.currentEmojiSuggestion?.disposable.dispose() self.currentEmojiSuggestion = suggestionContext } suggestionContext.localPosition = trackingPosition @@ -3491,13 +3562,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + func chatInputTextNodeShouldReturn() -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendContainerNode.alpha.isZero { self.sendButtonPressed() } return false } + @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldReturn() + } + private func applyUpdateSendButtonIcon() { if let interfaceState = self.presentationInterfaceState { let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil @@ -3517,7 +3592,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) { if !dueToEditing && !self.updatingInputState { let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) @@ -3529,7 +3604,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoilersRevealed() @@ -3537,7 +3612,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing) + } + + func chatInputTextNodeDidBeginEditing() { guard let interfaceInteraction = self.interfaceInteraction, let presentationInterfaceState = self.presentationInterfaceState else { return } @@ -3558,9 +3637,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.inputMenu.activate() } + @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidBeginEditing() + } + var skipPresentationInterfaceStateUpdate = false - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage + func chatInputTextNodeDidFinishEditing() { + guard let editableTextNode = self.textInputNode else { + return + } + + self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage self.inputMenu.deactivate() self.dismissedEmojiSuggestionPosition = nil @@ -3584,6 +3671,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidFinishEditing() + } + + func chatInputTextNodeBackspaceWhileEmpty() { + } + func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { if action == makeSelectorFromString("_accessibilitySpeak:") { if case .format = self.inputMenu.state { @@ -3617,7 +3711,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return ASEditableTextNodeTargetForAction(target: nil) } } - } else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) { + } else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) || action == #selector(self.formatAttributesQuote(_:)) { if case .format = self.inputMenu.state { if action == #selector(self.formatAttributesSpoiler(_:)), let selectedRange = self.textInputNode?.selectedRange { var intersectsMonospace = false @@ -3631,6 +3725,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { return ASEditableTextNodeTargetForAction(target: nil) } + } else if action == #selector(self.formatAttributesQuote(_:)), let selectedRange = self.textInputNode?.selectedRange { + let _ = selectedRange + return ASEditableTextNodeTargetForAction(target: self) } else if action == #selector(self.formatAttributesMonospace(_:)), let selectedRange = self.textInputNode?.selectedRange { var intersectsSpoiler = false self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in @@ -3656,14 +3753,61 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return nil } - @available(iOS 16.0, *) - func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + var suggestedActionCounter: Int = 0 + + @available(iOS 13.0, *) + func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + guard let editableTextNode = self.textInputNode else { + return UIMenu(children: []) + } + var actions = suggestedActions + if #available(iOS 16.0, *) { + if let index = actions.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.replace;") }), let subMenu = actions[index] as? UIMenu { + var filteredChildren = subMenu.children + if let subIndex = filteredChildren.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.autofill;") }) { + filteredChildren.remove(at: subIndex) + } + actions[index] = UIMenu(title: subMenu.title, subtitle: subMenu.subtitle, image: subMenu.image, identifier: subMenu.identifier, options: subMenu.options, children: filteredChildren) + } + } + if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 { - } else { - var children: [UIAction] = [ + var hasQuoteSelected = false + let selectedRange = editableTextNode.selectedRange + if let attributedText = editableTextNode.attributedText, selectedRange.lowerBound >= 0, selectedRange.lowerBound < attributedText.length, selectedRange.upperBound >= 0, selectedRange.upperBound < attributedText.length { + attributedText.enumerateAttribute(ChatTextInputAttributes.quote, in: selectedRange, using: { value, _, _ in + if value is ChatTextInputTextQuoteAttribute { + hasQuoteSelected = true + } + }) + } + let _ = hasQuoteSelected + + var children: [UIAction] = [] + + children.append(UIAction(title: self.strings?.TextFormat_Quote ?? "Quote", image: nil) { [weak self] (action) in + if let strongSelf = self { + strongSelf.formatAttributesQuote(strongSelf) + } + }) + + var hasSpoilers = true + if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { + hasSpoilers = false + } + + if hasSpoilers { + children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in + if let strongSelf = self { + strongSelf.formatAttributesSpoiler(strongSelf) + } + }) + } + + children.append(contentsOf: [ UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in if let strongSelf = self { strongSelf.formatAttributesBold(strongSelf) @@ -3694,27 +3838,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { strongSelf.formatAttributesUnderline(strongSelf) } } - ] - - var hasSpoilers = true - if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { - hasSpoilers = false - } - - if hasSpoilers { - children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesSpoiler(strongSelf) - } - }) - } + ] as [UIAction]) let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children) - actions.insert(formatMenu, at: 3) + actions.insert(formatMenu, at: 1) } return UIMenu(children: actions) } + @available(iOS 16.0, *) + func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + return chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions) + } + private var currentSpeechHolder: SpeechSynthesizerHolder? @objc func _accessibilitySpeak(_ sender: Any) { var text = "" @@ -3786,6 +3922,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + @objc func formatAttributesQuote(_ sender: Any) { + self.inputMenu.back() + + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.quote), inputMode) + } + } + @objc func formatAttributesSpoiler(_ sender: Any) { self.inputMenu.back() @@ -3805,7 +3949,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.updateSpoilersRevealed(animated: animated) } - @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let editableTextNode = self.textInputNode else { + return false + } + self.updateActivity() // MARK: Nicegram Send with Enter if self.sendWithKb { @@ -3846,7 +3994,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return true } - @objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) + } + + func chatInputTextNodeShouldCopy() -> Bool { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count))) return (current, inputMode) @@ -3854,11 +4006,30 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return false } - @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldCopy() + } + + public func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool { + guard let textInputNode = self.textInputNode else { + return true + } + let _ = textInputNode + + /*if textInputNode.attributedText == nil || textInputNode.attributedText!.length == 0 || textInputNode.selectedRange.length == 0 { + print("action: \(action)") + }*/ + + return true + } + + func chatInputTextNodeShouldPaste() -> Bool { let pasteboard = UIPasteboard.general var attributedString: NSAttributedString? - if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) { + if let data = pasteboard.data(forPasteboardType: "private.telegramtext"), let value = chatInputStateStringFromAppSpecificString(data: data) { + attributedString = value + } else if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) { attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf) } else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") { attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd) @@ -3925,6 +4096,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return true } + @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldPaste() + } + @objc func sendButtonPressed() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) @@ -4195,7 +4370,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return nil } - func makeSnapshotForTransition() -> ChatMessageTransitionNode.Source.TextInput? { + func makeSnapshotForTransition() -> ChatMessageTransitionNodeImpl.Source.TextInput? { guard let backgroundImage = self.transparentTextInputBackgroundImage else { return nil } @@ -4218,7 +4393,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { contentView.frame = textInputNode.frame - return ChatMessageTransitionNode.Source.TextInput( + return ChatMessageTransitionNodeImpl.Source.TextInput( backgroundView: backgroundView, contentView: contentView, sourceRect: self.view.convert(self.bounds, to: nil), diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 31fdad52247..bc794e0aa37 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -955,6 +955,10 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.updateCancelButton() } + deinit { + self.disposable.dispose() + } + private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { self.enqueuedTransitions.append(transition) diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift.orig b/submodules/TelegramUI/Sources/ChatThemeScreen.swift.orig deleted file mode 100644 index e3816ed48b7..00000000000 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift.orig +++ /dev/null @@ -1,1238 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import Postbox -import TelegramCore -import SwiftSignalKit -import AccountContext -import SolidRoundedButtonNode -import TelegramPresentationData -import TelegramUIPreferences -import TelegramNotices -import PresentationDataUtils -import AnimationUI -import MergeLists -import MediaResources -import StickerResources -import WallpaperResources -import TooltipUI -import AnimatedStickerNode -import TelegramAnimatedStickerNode -import ShimmerEffect - -private func closeButtonImage(theme: PresentationTheme) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} - -private struct ThemeSettingsThemeEntry: Comparable, Identifiable { - let index: Int - let emoticon: String? - let emojiFile: TelegramMediaFile? - let themeReference: PresentationThemeReference? - var selected: Bool - let theme: PresentationTheme - let strings: PresentationStrings - let wallpaper: TelegramWallpaper? - - var stableId: Int { - return index - } - - static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { - if lhs.index != rhs.index { - return false - } - if lhs.emoticon != rhs.emoticon { - return false - } - - if lhs.themeReference?.index != rhs.themeReference?.index { - return false - } - if lhs.selected != rhs.selected { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.wallpaper != rhs.wallpaper { - return false - } - return true - } - - static func <(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(context: AccountContext, action: @escaping (String?) -> Void) -> ListViewItem { - return ThemeSettingsThemeIconItem(context: context, emoticon: self.emoticon, emojiFile: self.emojiFile, themeReference: self.themeReference, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) - } -} - - -private class ThemeSettingsThemeIconItem: ListViewItem { - let context: AccountContext - let emoticon: String? - let emojiFile: TelegramMediaFile? - let themeReference: PresentationThemeReference? - let selected: Bool - let theme: PresentationTheme - let strings: PresentationStrings - let wallpaper: TelegramWallpaper? - let action: (String?) -> Void - - public init(context: AccountContext, emoticon: String?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (String?) -> Void) { - self.context = context - self.emoticon = emoticon - self.emojiFile = emojiFile - self.themeReference = themeReference - self.selected = selected - self.theme = theme - self.strings = strings - self.wallpaper = wallpaper - self.action = action - } - - 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 = ThemeSettingsThemeItemIconNode() - let (nodeLayout, apply) = node.asyncLayout()(self, params) - node.insets = nodeLayout.insets - node.contentSize = nodeLayout.contentSize - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in - apply(false) - }) - }) - } - } - } - - 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 { - assert(node() is ThemeSettingsThemeItemIconNode) - if let nodeValue = node() as? ThemeSettingsThemeItemIconNode { - let layout = nodeValue.asyncLayout() - async { - let (nodeLayout, apply) = layout(self, params) - Queue.mainQueue().async { - completion(nodeLayout, { _ in - apply(animation.isAnimated) - }) - } - } - } - } - } - - public var selectable = true - public func selected(listView: ListView) { - self.action(self.emoticon) - } -} - -private struct ThemeSettingsThemeItemNodeTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] - let crossfade: Bool - let entries: [ThemeSettingsThemeEntry] -} - -private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated: Bool) -> Bool { - var resultNode: ThemeSettingsThemeItemIconNode? -<<<<<<< HEAD - var previousNode: ThemeSettingsThemeItemIconNode? - let _ = previousNode -======= -// var previousNode: ThemeSettingsThemeItemIconNode? ->>>>>>> beta - var nextNode: ThemeSettingsThemeItemIconNode? - listNode.forEachItemNode { node in - guard let node = node as? ThemeSettingsThemeItemIconNode else { - return - } - if resultNode == nil { - if node.item?.emoticon == emoticon { - resultNode = node - } else { -// previousNode = node - } - } else if nextNode == nil { - nextNode = node - } - } - if let resultNode = resultNode { - listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 57.0) - return true - } else { - return false - } -} - -private func preparedTransition(context: AccountContext, action: @escaping (String?) -> Void, from fromEntries: [ThemeSettingsThemeEntry], to toEntries: [ThemeSettingsThemeEntry], crossfade: Bool) -> ThemeSettingsThemeItemNodeTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: .Down) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: nil) } - - return ThemeSettingsThemeItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, crossfade: crossfade, entries: toEntries) -} - -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: 18.0, height: 18.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: 3.0 + lineWidth / 2.0, dy: 3.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: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) - } - })?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9) - cachedBorderImages[key] = image - return image - } -} - -private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { - private let containerNode: ASDisplayNode - private let emojiContainerNode: ASDisplayNode - private let imageNode: TransformImageNode - private let overlayNode: ASImageNode - private let textNode: TextNode - private let emojiNode: TextNode - private let emojiImageNode: TransformImageNode - private var animatedStickerNode: AnimatedStickerNode? - private var placeholderNode: StickerShimmerEffectNode - var snapshotView: UIView? - - var item: ThemeSettingsThemeIconItem? - - override var visibility: ListViewItemNodeVisibility { - didSet { - self.visibilityStatus = self.visibility != .none - } - } - - private var visibilityStatus: Bool = false { - didSet { - if self.visibilityStatus != oldValue { - self.animatedStickerNode?.visibility = self.visibilityStatus - } - } - } - - private let stickerFetchedDisposable = MetaDisposable() - - init() { - self.containerNode = ASDisplayNode() - self.emojiContainerNode = ASDisplayNode() - - self.imageNode = TransformImageNode() - self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 82.0, height: 108.0)) - self.imageNode.isLayerBacked = true - self.imageNode.cornerRadius = 8.0 - self.imageNode.clipsToBounds = true - - self.overlayNode = ASImageNode() - self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 84.0, height: 110.0)) - self.overlayNode.isLayerBacked = true - - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - - self.emojiNode = TextNode() - self.emojiNode.isUserInteractionEnabled = false - self.emojiNode.displaysAsynchronously = false - - self.emojiImageNode = TransformImageNode() - - self.placeholderNode = StickerShimmerEffectNode() - - super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.imageNode) - self.containerNode.addSubnode(self.overlayNode) - self.containerNode.addSubnode(self.textNode) - - self.addSubnode(self.emojiContainerNode) - self.emojiContainerNode.addSubnode(self.emojiNode) - self.emojiContainerNode.addSubnode(self.emojiImageNode) - self.emojiContainerNode.addSubnode(self.placeholderNode) - - 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.stickerFetchedDisposable.dispose() - } - - private func removePlaceholder(animated: Bool) { - if !animated { - self.placeholderNode.removeFromSupernode() - } else { - self.placeholderNode.alpha = 0.0 - self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in - self?.placeholderNode.removeFromSupernode() - }) - } - } - - override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0)) - self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) - } - - override func selected() { - let wasSelected = self.item?.selected ?? false - super.selected() - - if let animatedStickerNode = self.animatedStickerNode { - Queue.mainQueue().after(0.1) { - if !wasSelected { - animatedStickerNode.seekTo(.frameIndex(0)) - animatedStickerNode.play() - - let scale: CGFloat = 2.6 - animatedStickerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) - animatedStickerNode.layer.animateSpring(from: 1.0 as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: 0.45) - - animatedStickerNode.completed = { [weak animatedStickerNode, weak self] _ in - guard let item = self?.item, item.selected else { - return - } - animatedStickerNode?.transform = CATransform3DIdentity - animatedStickerNode?.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) - } - } - } - } - - } - - func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { - let makeTextLayout = TextNode.asyncLayout(self.textNode) - let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) - let makeImageLayout = self.imageNode.asyncLayout() - - let currentItem = self.item - - return { [weak self] item, params in - var updatedEmoticon = false - var updatedThemeReference = false - var updatedTheme = false - var updatedWallpaper = false - var updatedSelected = false - - if currentItem?.emoticon != item.emoticon { - updatedEmoticon = true - } - if currentItem?.themeReference != item.themeReference { - updatedThemeReference = true - } - if currentItem?.wallpaper != item.wallpaper { - updatedWallpaper = true - } - if currentItem?.theme !== item.theme { - updatedTheme = true - } - if currentItem?.selected != item.selected { - updatedSelected = true - } - - let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) - 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())) - - let emoticon = item.emoticon - let title = NSAttributedString(string: emoticon != nil ? "" : "❌", font: Font.regular(22.0), textColor: .black) - let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - - let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 120.0, height: 90.0), insets: UIEdgeInsets()) - return (itemLayout, { animated in - if let strongSelf = self { - strongSelf.item = item - - if updatedThemeReference || updatedWallpaper { - if let themeReference = item.themeReference { - strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, emoticon: true)) - strongSelf.imageNode.backgroundColor = nil - } - } - if item.themeReference == nil { - strongSelf.imageNode.backgroundColor = item.theme.actionSheet.opaqueItemBackgroundColor - } - - if updatedTheme || updatedSelected { - strongSelf.overlayNode.image = generateBorderImage(theme: item.theme, bordered: false, selected: item.selected) - } - - if !item.selected && currentItem?.selected == true, let animatedStickerNode = strongSelf.animatedStickerNode { - animatedStickerNode.transform = CATransform3DIdentity - - let initialScale: CGFloat = CGFloat((animatedStickerNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0) - animatedStickerNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) - } - - 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.emoticon != nil - - strongSelf.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) - - strongSelf.emojiContainerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - strongSelf.emojiContainerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) - - let _ = textApply() - let _ = emojiApply() - - let imageSize = CGSize(width: 82.0, height: 108.0) - strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 6.0), size: imageSize) - let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) - applyLayout() - - strongSelf.overlayNode.frame = strongSelf.imageNode.frame.insetBy(dx: -1.0, dy: -1.0) - strongSelf.emojiNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 79.0), size: CGSize(width: 90.0, height: 30.0)) - - let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0)) - if let file = item.emojiFile, updatedEmoticon { - let imageApply = strongSelf.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets())) - imageApply() - strongSelf.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true)) - strongSelf.emojiImageNode.frame = emojiFrame - - let animatedStickerNode: AnimatedStickerNode - if let current = strongSelf.animatedStickerNode { - animatedStickerNode = current - } else { - animatedStickerNode = AnimatedStickerNode() - animatedStickerNode.started = { [weak self] in - self?.emojiImageNode.isHidden = true - } - strongSelf.animatedStickerNode = animatedStickerNode - strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.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 = strongSelf.visibilityStatus - - strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) - - let thumbnailDimensions = PixelDimensions(width: 512, height: 512) - strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) - strongSelf.placeholderNode.frame = emojiFrame - } - - if let animatedStickerNode = strongSelf.animatedStickerNode { - animatedStickerNode.frame = emojiFrame - animatedStickerNode.updateLayout(size: emojiFrame.size) - } - } - }) - } - } - - func crossfade() { - if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.transform = self.containerNode.view.transform - snapshotView.frame = self.containerNode.view.frame - self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) - - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - super.animateRemoved(currentTimestamp, duration: duration) - - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - } - - override func animateAdded(_ currentTimestamp: Double, duration: Double) { - super.animateAdded(currentTimestamp, duration: duration) - - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } -} - -final class ChatThemeScreen: ViewController { - static let themeCrossfadeDuration: Double = 0.3 - static let themeCrossfadeDelay: Double = 0.25 - - private var controllerNode: ChatThemeScreenNode { - return self.displayNode as! ChatThemeScreenNode - } - - private var animatedIn = false - - private let context: AccountContext - private let animatedEmojiStickers: [String: [StickerPackItem]] - private let initiallySelectedEmoticon: String? - private let peerName: String - private let previewTheme: (String?, Bool?) -> Void - private let completion: (String?) -> Void - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - var dismissed: (() -> Void)? - - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { - didSet { - if self.isNodeLoaded { - self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl - } - } - } - - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, peerName: String, previewTheme: @escaping (String?, Bool?) -> Void, completion: @escaping (String?) -> Void) { - self.context = context - self.presentationData = updatedPresentationData.initial - self.animatedEmojiStickers = animatedEmojiStickers - self.initiallySelectedEmoticon = initiallySelectedEmoticon - self.peerName = peerName - self.previewTheme = previewTheme - self.completion = completion - - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore - - self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = (updatedPresentationData.signal - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.presentationData = presentationData - strongSelf.controllerNode.updatePresentationData(presentationData) - } - }) - - self.statusBar.statusBarStyle = .Ignore - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedEmoticon: self.initiallySelectedEmoticon, peerName: self.peerName) - self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl - self.controllerNode.previewTheme = { [weak self] emoticon, dark in - guard let strongSelf = self else { - return - } - strongSelf.previewTheme((emoticon ?? ""), dark) - } - self.controllerNode.present = { [weak self] c in - self?.present(c, in: .current) - } - self.controllerNode.completion = { [weak self] emoticon in - guard let strongSelf = self else { - return - } - strongSelf.dismiss() - if strongSelf.initiallySelectedEmoticon == nil && emoticon == nil { - } else { - strongSelf.completion(emoticon) - } - } - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - self.controllerNode.cancel = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.dismiss() - strongSelf.previewTheme(nil, nil) - } - } - - override public func loadView() { - super.loadView() - - self.view.disablesInteractiveTransitionGestureRecognizer = true - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.forEachController({ controller in - if let controller = controller as? TooltipScreen { - controller.dismiss() - } - return true - }) - - self.controllerNode.animateOut(completion: completion) - - self.dismissed?() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } - - func dimTapped() { - self.controllerNode.dimTapped() - } -} - -private func iconColors(theme: PresentationTheme) -> [String: UIColor] { - let accentColor = theme.actionSheet.controlAccentColor - var colors: [String: UIColor] = [:] - colors["Sunny.Path 14.Path.Stroke 1"] = accentColor - colors["Sunny.Path 15.Path.Stroke 1"] = accentColor - colors["Path.Path.Stroke 1"] = accentColor - colors["Sunny.Path 39.Path.Stroke 1"] = accentColor - colors["Sunny.Path 24.Path.Stroke 1"] = accentColor - colors["Sunny.Path 25.Path.Stroke 1"] = accentColor - colors["Sunny.Path 18.Path.Stroke 1"] = accentColor - colors["Sunny.Path 41.Path.Stroke 1"] = accentColor - colors["Sunny.Path 43.Path.Stroke 1"] = accentColor - colors["Path 10.Path.Fill 1"] = accentColor - colors["Path 11.Path.Fill 1"] = accentColor - return colors -} - -private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { - private let context: AccountContext - private var presentationData: PresentationData - private weak var controller: ChatThemeScreen? - - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let topContentContainerNode: SparseNode - private let effectNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private let titleNode: ASTextNode - private let textNode: ImmediateTextNode - private let cancelButton: HighlightableButtonNode - private let switchThemeButton: HighlightTrackingButtonNode - private let animationContainerNode: ASDisplayNode - private var animationNode: AnimationNode - private let doneButton: SolidRoundedButtonNode - - private let listNode: ListView - private var entries: [ThemeSettingsThemeEntry]? - private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] - private var initialized = false - - private let peerName: String - - private let initiallySelectedEmoticon: String? - private var selectedEmoticon: String? { - didSet { - self.selectedEmoticonPromise.set(self.selectedEmoticon) - } - } - private var selectedEmoticonPromise: ValuePromise - - private var isDarkAppearancePromise: ValuePromise - private var isDarkAppearance: Bool = false { - didSet { - self.isDarkAppearancePromise.set(self.isDarkAppearance) - } - } - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - private let disposable = MetaDisposable() - - var present: ((ViewController) -> Void)? - var previewTheme: ((String?, Bool?) -> Void)? - var completion: ((String?) -> Void)? - var dismiss: (() -> Void)? - var cancel: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData, controller: ChatThemeScreen, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, peerName: String) { - self.context = context - self.controller = controller - self.initiallySelectedEmoticon = initiallySelectedEmoticon - self.peerName = peerName - self.selectedEmoticon = initiallySelectedEmoticon - self.selectedEmoticonPromise = ValuePromise(initiallySelectedEmoticon) - self.presentationData = presentationData - - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.canCancelContentTouches = true - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = .clear - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false - - self.topContentContainerNode = SparseNode() - self.topContentContainerNode.isOpaque = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 - - self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance - self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) - - let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - let textColor = self.presentationData.theme.actionSheet.primaryTextColor - let secondaryTextColor = self.presentationData.theme.actionSheet.secondaryTextColor - let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark - - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor - - self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_Theme_Title, font: Font.semibold(16.0), textColor: textColor) - - self.textNode = ImmediateTextNode() - self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_Theme_Subtitle(peerName).string, font: Font.regular(12.0), textColor: secondaryTextColor) - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - - self.switchThemeButton = HighlightTrackingButtonNode() - self.animationContainerNode = ASDisplayNode() - self.animationContainerNode.isUserInteractionEnabled = false - - self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) - self.animationNode.isUserInteractionEnabled = false - - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false) - self.doneButton.title = initiallySelectedEmoticon == nil ? self.presentationData.strings.Conversation_Theme_DontSetTheme : self.presentationData.strings.Conversation_Theme_Apply - - self.listNode = ListView() - self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - self.wrappingScrollNode.addSubnode(self.topContentContainerNode) - - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.contentContainerNode.addSubnode(self.textNode) - self.contentContainerNode.addSubnode(self.doneButton) - - self.topContentContainerNode.addSubnode(self.animationContainerNode) - self.animationContainerNode.addSubnode(self.animationNode) - self.topContentContainerNode.addSubnode(self.switchThemeButton) - self.topContentContainerNode.addSubnode(self.listNode) - self.topContentContainerNode.addSubnode(self.cancelButton) - - self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.doneButton.pressed = { [weak self] in - if let strongSelf = self { - strongSelf.doneButton.isUserInteractionEnabled = false - strongSelf.completion?(strongSelf.selectedEmoticon) - } - } - - self.disposable.set(combineLatest(queue: Queue.mainQueue(), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), self.selectedEmoticonPromise.get(), self.isDarkAppearancePromise.get()).start(next: { [weak self] themes, selectedEmoticon, isDarkAppearance in - guard let strongSelf = self else { - return - } - - let isFirstTime = strongSelf.entries == nil - let presentationData = strongSelf.presentationData - - var entries: [ThemeSettingsThemeEntry] = [] - entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: nil, emojiFile: nil, themeReference: nil, selected: selectedEmoticon == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) - for theme in themes { - let emoticon = theme.emoji - entries.append(ThemeSettingsThemeEntry(index: entries.count, emoticon: theme.emoji, emojiFile: animatedEmojiStickers[emoticon]?.first?.file, themeReference: .cloud(PresentationCloudTheme(theme: isDarkAppearance ? theme.darkTheme : theme.theme, resolvedWallpaper: nil, creatorAccountId: nil)), selected: selectedEmoticon == theme.emoji, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) - } - - let action: (String?) -> Void = { [weak self] emoticon in - if let strongSelf = self, strongSelf.selectedEmoticon != emoticon { - strongSelf.animateCrossfade(animateIcon: true) - - strongSelf.previewTheme?(emoticon, strongSelf.isDarkAppearance) - strongSelf.selectedEmoticon = emoticon - let _ = ensureThemeVisible(listNode: strongSelf.listNode, emoticon: emoticon, animated: true) - - let doneButtonTitle: String - if emoticon == nil { - doneButtonTitle = strongSelf.initiallySelectedEmoticon == nil ? strongSelf.presentationData.strings.Conversation_Theme_DontSetTheme : strongSelf.presentationData.strings.Conversation_Theme_Reset - } else { - doneButtonTitle = strongSelf.presentationData.strings.Conversation_Theme_Apply - } - strongSelf.doneButton.title = doneButtonTitle - - strongSelf.themeSelectionsCount += 1 - if strongSelf.themeSelectionsCount == 2 { - strongSelf.maybePresentPreviewTooltip() - } - } - } - let previousEntries = strongSelf.entries ?? [] - let crossfade = previousEntries.count != entries.count - let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) - strongSelf.enqueueTransition(transition) - - strongSelf.entries = entries - - if isFirstTime { - for theme in themes { - if let wallpaper = theme.theme.settings?.wallpaper, case let .file(file) = wallpaper { - let account = strongSelf.context.account - let accountManager = strongSelf.context.sharedContext.accountManager - let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) - if !FileManager.default.fileExists(atPath: path) { - let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in - let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) - - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) - let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = accountResource.start(next: { next in - subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) - - if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { - accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) - } - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - fetchedFullSizeDisposable.dispose() - fullSizeDisposable.dispose() - } - } - let _ = accountFullSizeData.start() - } - } - } - } - })) - - self.switchThemeButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.animationNode.layer.removeAnimation(forKey: "opacity") - strongSelf.animationNode.alpha = 0.4 - } else { - strongSelf.animationNode.alpha = 1.0 - strongSelf.animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - } - - private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { - self.enqueuedTransitions.append(transition) - - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - - private func dequeueTransition() { - guard let transition = self.enqueuedTransitions.first else { - return - } - self.enqueuedTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if self.initialized && transition.crossfade { - options.insert(.AnimateCrossfade) - } - options.insert(.Synchronous) - - var scrollToItem: ListViewScrollToItem? - if !self.initialized { - if let index = transition.entries.firstIndex(where: { entry in - return entry.emoticon == self.initiallySelectedEmoticon - }) { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) - self.initialized = true - } - } - - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - - func updatePresentationData(_ presentationData: PresentationData) { - guard !self.animatedOut else { - return - } - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.semibold(16.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(12.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor) - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) - - if self.animationNode.isPlaying { - if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.2) { - let previousAnimationNode = self.animationNode - self.animationNode = animationNode - - animationNode.completion = { [weak previousAnimationNode] in - previousAnimationNode?.removeFromSupernode() - } - animationNode.isUserInteractionEnabled = false - animationNode.frame = previousAnimationNode.frame - previousAnimationNode.supernode?.insertSubnode(animationNode, belowSubnode: previousAnimationNode) - previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, removeOnCompletion: false) - animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } else { - self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) - } - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true - } - - @objc func cancelButtonPressed() { - self.cancel?() - } - - func dimTapped() { - if self.selectedEmoticon == self.initiallySelectedEmoticon { - self.cancelButtonPressed() - } else { - let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in - if let strongSelf = self { - strongSelf.completion?(strongSelf.selectedEmoticon) - } - })], actionLayout: .horizontal, dismissOnOutsideTap: true) - self.present?(alertController) - } - } - - @objc func switchThemePressed() { - self.switchThemeButton.isUserInteractionEnabled = false - Queue.mainQueue().after(0.5) { - self.switchThemeButton.isUserInteractionEnabled = true - } - - self.animateCrossfade(animateIcon: false) - self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) - self.animationNode.playOnce() - - let isDarkAppearance = !self.isDarkAppearance - self.previewTheme?(self.selectedEmoticon, isDarkAppearance) - self.isDarkAppearance = isDarkAppearance - - if isDarkAppearance { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() - } else { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() - } - } - - private func animateCrossfade(animateIcon: Bool) { - if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.animationNode.frame - self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - - Queue.mainQueue().after(ChatThemeScreen.themeCrossfadeDelay) { - if let effectView = self.effectNode.view as? UIVisualEffectView { - UIView.animate(withDuration: ChatThemeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { - effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } completion: { _ in - } - } - - let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatThemeScreen.themeCrossfadeDuration) - } - - if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.contentContainerNode.frame - self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - - self.listNode.forEachVisibleItemNode { node in - if let node = node as? ThemeSettingsThemeItemIconNode { - node.crossfade() - } - } - } - - private var animatedOut = false - func animateIn() { - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - let targetBounds = self.bounds - self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) - self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) - transition.animateView({ - self.bounds = targetBounds - self.dimNode.position = dimPosition - }) - } - - private var themeSelectionsCount = 0 - private var displayedPreviewTooltip = false - private func maybePresentPreviewTooltip() { - guard !self.displayedPreviewTooltip, !self.animatedOut else { - return - } - - let frame = self.switchThemeButton.view.convert(self.switchThemeButton.bounds, to: self.view) - let currentTimestamp = Int32(Date().timeIntervalSince1970) - - let isDark = self.presentationData.theme.overallDarkAppearance - - let signal: Signal<(Int32, Int32), NoError> - if isDark { - signal = ApplicationSpecificNotice.getChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager) - } else { - signal = ApplicationSpecificNotice.getChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager) - } - - let _ = (signal - |> deliverOnMainQueue).start(next: { [weak self] count, timestamp in - if let strongSelf = self, count < 2 && currentTimestamp > timestamp + 24 * 60 * 60 { - strongSelf.displayedPreviewTooltip = true - - strongSelf.present?(TooltipScreen(account: strongSelf.context.account, text: isDark ? strongSelf.presentationData.strings.Conversation_Theme_PreviewLight(strongSelf.peerName).string : strongSelf.presentationData.strings.Conversation_Theme_PreviewDark(strongSelf.peerName).string, style: .default, icon: nil, location: .point(frame.offsetBy(dx: 3.0, dy: 6.0), .bottom), displayDuration: .custom(3.0), inset: 3.0, shouldDismissOnTouch: { _ in - return .dismiss(consume: false) - })) - - if isDark { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).start() - } else { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).start() - } - } - }) - } - - func animateOut(completion: (() -> Void)? = nil) { - self.animatedOut = true - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.dismiss?() - completion?() - } - }) - } - - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - var presentingAlertController = false - self.controller?.forEachController({ c in - if c is AlertController { - presentingAlertController = true - } - return true - }) - - if !presentingAlertController && self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - if let result = self.passthroughHitTestImpl?(point) { - return result - } else { - return nil - } - } - } - return super.hitTest(point, with: event) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - let titleHeight: CGFloat = 54.0 - let contentHeight = titleHeight + bottomInset + 188.0 - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) - - let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 11.0 + UIScreenPixel), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let textSize = self.textNode.updateLayout(CGSize(width: width - 90.0, height: titleHeight)) - let textFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - textSize.width) / 2.0), y: 31.0), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - let switchThemeSize = CGSize(width: 44.0, height: 44.0) - let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) - transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) - transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) - transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) - - let cancelSize = CGSize(width: 44.0, height: 44.0) - let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) - - let buttonInset: CGFloat = 16.0 - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight)) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) - - var listInsets = UIEdgeInsets() - listInsets.top += layout.safeInsets.left + 12.0 - listInsets.bottom += layout.safeInsets.right + 12.0 - - let contentSize = CGSize(width: contentFrame.width, height: 120.0) - - self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) - self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } -} diff --git a/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift index 8d6fd461a2e..354a82df607 100644 --- a/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift @@ -7,6 +7,7 @@ import Postbox import SwiftSignalKit import TelegramPresentationData import ChatPresentationInterfaceState +import ChatInputPanelNode final class ChatUnblockInputPanelNode: ChatInputPanelNode { private let button: HighlightableButtonNode diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index adfad2e4b5f..1e5fd7f7f6b 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -12,6 +12,7 @@ import ChatPresentationInterfaceState import ChatControllerInteraction import ItemListUI import ChatContextQuery +import ChatInputContextPanelNode private struct CommandChatInputContextPanelEntryStableId: Hashable { let command: PeerCommand diff --git a/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift index 0e0a6b9d254..a41da1c7491 100644 --- a/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift @@ -12,6 +12,7 @@ import AccountContext import ChatPresentationInterfaceState import ChatControllerInteraction import ChatContextQuery +import ChatInputContextPanelNode private struct CommandMenuChatInputContextPanelEntryStableId: Hashable { let command: PeerCommand diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 3b2d204a86c..66c579b8d68 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -13,6 +13,7 @@ import AlertUI import PresentationDataUtils import ContactListUI import CounterContollerTitleView +import EditableTokenListNode private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { if peer.id == accountPeerId { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index fdfddc9fabc..725d1259e5e 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -11,6 +11,7 @@ import ContactListUI import ChatListUI import AnimationCache import MultiAnimationRenderer +import EditableTokenListNode private struct SearchResultEntry: Identifiable { let index: Int diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index d52f0cfc658..8fee5a2f58b 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -37,6 +37,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private let displayDeviceContacts: Bool private let displayCallIcons: Bool private let multipleSelection: Bool + private let requirePhoneNumbers: Bool private var _ready = Promise() override var ready: Promise { @@ -91,6 +92,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.displayCallIcons = params.displayCallIcons self.confirmation = params.confirmation self.multipleSelection = params.multipleSelection + self.requirePhoneNumbers = params.requirePhoneNumbers self.presentationData = params.updatedPresentationData?.initial ?? params.context.sharedContext.currentPresentationData.with { $0 } @@ -148,6 +150,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController deinit { self.createActionDisposable.dispose() self.presentationDataDisposable?.dispose() + self.confirmationDisposable.dispose() } @objc private func beginSearch() { @@ -177,7 +180,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } override func loadDisplayNode() { - self.displayNode = ContactSelectionControllerNode(context: self.context, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection) + self.displayNode = ContactSelectionControllerNode(context: self.context, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 33586776687..ad60e31dd4e 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -23,6 +23,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { private let displayDeviceContacts: Bool private let displayCallIcons: Bool + private let filters: [ContactListFilter] let contactListNode: ContactListNode private let dimNode: ASDisplayNode @@ -53,14 +54,20 @@ final class ContactSelectionControllerNode: ASDisplayNode { var searchContainerNode: ContactsSearchContainerNode? - init(context: AccountContext, presentationData: PresentationData, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool) { + init(context: AccountContext, presentationData: PresentationData, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool) { self.context = context self.presentationData = presentationData self.displayDeviceContacts = displayDeviceContacts self.displayCallIcons = displayCallIcons + var filters: [ContactListFilter] = [.excludeSelf] + if requirePhoneNumbers { + filters.append(.excludeWithoutPhoneNumbers) + } + 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)), 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)), filters: filters, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in contextActionImpl?(peer, node, gesture, nil) } : nil, multipleSelection: multipleSelection) @@ -186,7 +193,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { categories.insert(.global) } - let searchContainerNode = ContactsSearchContainerNode(context: self.context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), onlyWriteable: false, categories: categories, addContact: nil, openPeer: { [weak self] peer in + let searchContainerNode = ContactsSearchContainerNode(context: self.context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), onlyWriteable: false, categories: categories, filters: self.filters, addContact: nil, openPeer: { [weak self] peer in if let strongSelf = self { var updated = false strongSelf.contactListNode.updateSelectionState { state -> ContactListNodeGroupSelectionState? in diff --git a/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift b/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift index f5fbf51104e..4a456d3ee64 100644 --- a/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift @@ -6,6 +6,7 @@ import TelegramCore import Postbox import SwiftSignalKit import ChatPresentationInterfaceState +import ChatInputPanelNode final class DeleteChatInputPanelNode: ChatInputPanelNode { private let button: HighlightableButtonNode diff --git a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift index ff4f17d0b79..69025bf1fec 100644 --- a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift @@ -9,6 +9,7 @@ import TelegramUIPreferences import AccountContext import ChatPresentationInterfaceState import ChatControllerInteraction +import ChatInputContextPanelNode final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { private let containerNode: ASDisplayNode diff --git a/submodules/TelegramUI/Sources/EmojiResources.swift b/submodules/TelegramUI/Sources/EmojiResources.swift deleted file mode 100644 index 228f5ea1c9d..00000000000 --- a/submodules/TelegramUI/Sources/EmojiResources.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import UIKit -import Postbox -import TelegramCore -import Emoji - -func messageIsElligibleForLargeEmoji(_ message: Message) -> Bool { - if !message.text.isEmpty && message.text.containsOnlyEmoji { - if !(message.textEntitiesAttribute?.entities.isEmpty ?? true) { - return false - } - return true - } else { - return false - } -} - -func messageIsElligibleForLargeCustomEmoji(_ message: Message) -> Bool { - let text = message.text.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: " ", with: "") - guard !text.isEmpty && text.containsOnlyEmoji else { - return false - } - let entities = message.textEntitiesAttribute?.entities ?? [] - guard entities.count > 0 else { - return false - } - for entity in entities { - if case let .CustomEmoji(_, fileId) = entity.type { - if let _ = message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { - - } else { - return false - } - } else { - return false - } - } - return true -} diff --git a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift index cd8f5216be1..80c2ecb74dd 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift @@ -21,6 +21,7 @@ import StickerPeekUI import UndoUI import Pasteboard import ChatContextQuery +import ChatInputContextPanelNode private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable { case symbol(String) diff --git a/submodules/TelegramUI/Sources/GridHoleItem.swift b/submodules/TelegramUI/Sources/GridHoleItem.swift deleted file mode 100644 index b603f10cb18..00000000000 --- a/submodules/TelegramUI/Sources/GridHoleItem.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit - -final class GridHoleItem: GridItem { - let section: GridSection? = nil - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - return GridHoleItemNode() - } - - func update(node: GridItemNode) { - } -} - -class GridHoleItemNode: GridItemNode { - private let activityIndicatorView: UIActivityIndicatorView - - override init() { - self.activityIndicatorView = UIActivityIndicatorView(style: .gray) - - super.init() - - self.view.addSubview(self.activityIndicatorView) - self.activityIndicatorView.startAnimating() - } - - override func layout() { - super.layout() - - let size = self.bounds.size - let activityIndicatorSize = self.activityIndicatorView.bounds.size - self.activityIndicatorView.frame = CGRect(origin: CGPoint(x: floor((size.width - activityIndicatorSize.width) / 2.0), y: floor((size.height - activityIndicatorSize.height) / 2.0)), size: activityIndicatorSize) - } -} diff --git a/submodules/TelegramUI/Sources/GridMessageItem.swift b/submodules/TelegramUI/Sources/GridMessageItem.swift deleted file mode 100644 index 5a05b2dc7ab..00000000000 --- a/submodules/TelegramUI/Sources/GridMessageItem.swift +++ /dev/null @@ -1,471 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import TelegramCore -import Postbox -import SwiftSignalKit -import TelegramPresentationData -import TelegramUIPreferences -import TelegramStringFormatting -import AccountContext -import RadialStatusNode -import PhotoResources -import GridMessageSelectionNode -import ContextUI -import ChatMessageInteractiveMediaBadge -import ChatControllerInteraction - -private func mediaForMessage(_ message: Message) -> Media? { - for media in message.media { - if let media = media as? TelegramMediaImage { - return media - } else if let file = media as? TelegramMediaFile { - if file.mimeType.hasPrefix("audio/") { - return nil - } else if !file.isVideo && file.mimeType.hasPrefix("video/") { - return file - } else { - return file - } - } - } - return nil -} - -private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) -private let mediaBadgeTextColor = UIColor.white - -final class GridMessageItemSection: GridSection { - let height: CGFloat = 36.0 - - fileprivate let theme: PresentationTheme - private let strings: PresentationStrings - private let fontSize: PresentationFontSize - - private let roundedTimestamp: Int32 - private let month: Int32 - private let year: Int32 - - var hashValue: Int { - return self.roundedTimestamp.hashValue - } - - init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { - self.theme = theme - self.strings = strings - self.fontSize = fontSize - - var now = time_t(timestamp) - var timeinfoNow: tm = tm() - localtime_r(&now, &timeinfoNow) - - self.roundedTimestamp = timeinfoNow.tm_year * 100 + timeinfoNow.tm_mon - self.month = timeinfoNow.tm_mon - self.year = timeinfoNow.tm_year - } - - func isEqual(to: GridSection) -> Bool { - if let to = to as? GridMessageItemSection { - return self.roundedTimestamp == to.roundedTimestamp && theme === to.theme - } else { - return false - } - } - - func node() -> ASDisplayNode { - return GridMessageItemSectionNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) - } -} - -final class GridMessageItemSectionNode: ASDisplayNode { - var theme: PresentationTheme - var strings: PresentationStrings - var fontSize: PresentationFontSize - let titleNode: ASTextNode - - init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) { - self.theme = theme - self.strings = strings - self.fontSize = fontSize - - self.titleNode = ASTextNode() - self.titleNode.isUserInteractionEnabled = false - - super.init() - - self.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9) - - let sectionTitleFont = Font.regular(floor(fontSize.baseDisplaySize * 14.0 / 17.0)) - - let dateText = stringForMonth(strings: strings, month: month, ofYear: year) - self.addSubnode(self.titleNode) - self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor) - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.truncationMode = .byTruncatingTail - } - - override func layout() { - super.layout() - - let bounds = self.bounds - - let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) - self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((bounds.size.height - titleSize.height) / 2.0)), size: titleSize) - } -} - -final class GridMessageItem: GridItem { - fileprivate let theme: PresentationTheme - private let strings: PresentationStrings - private let context: AccountContext - fileprivate let message: Message - private let controllerInteraction: ChatControllerInteraction - let section: GridSection? - - init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, context: AccountContext, message: Message, controllerInteraction: ChatControllerInteraction) { - self.theme = theme - self.strings = strings - self.context = context - self.message = message - self.controllerInteraction = controllerInteraction - self.section = GridMessageItemSection(timestamp: message.timestamp, theme: theme, strings: strings, fontSize: fontSize) - } - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - let node = GridMessageItemNode() - if let media = mediaForMessage(self.message) { - node.setup(context: self.context, item: self, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction, synchronousLoad: synchronousLoad) - } - return node - } - - func update(node: GridItemNode) { - guard let node = node as? GridMessageItemNode else { - assertionFailure() - return - } - if let media = mediaForMessage(self.message) { - node.setup(context: self.context, item: self, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction, synchronousLoad: false) - } - } -} - -final class GridMessageItemNode: GridItemNode { - private var currentState: (AccountContext, Media, CGSize)? - private let containerNode: ContextControllerSourceNode - private let imageNode: TransformImageNode - private(set) var messageId: MessageId? - private var item: GridMessageItem? - private var controllerInteraction: ChatControllerInteraction? - private var statusNode: RadialStatusNode - private let mediaBadgeNode: ChatMessageInteractiveMediaBadge - - private var selectionNode: GridMessageSelectionNode? - - private let fetchStatusDisposable = MetaDisposable() - private let fetchDisposable = MetaDisposable() - private var resourceStatus: MediaResourceStatus? - - override init() { - self.containerNode = ContextControllerSourceNode() - self.imageNode = TransformImageNode() - self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) - let progressDiameter: CGFloat = 40.0 - self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter) - self.statusNode.isUserInteractionEnabled = false - - self.mediaBadgeNode = ChatMessageInteractiveMediaBadge() - self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 50.0, height: 50.0)) - - super.init() - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.imageNode) - self.containerNode.addSubnode(self.mediaBadgeNode) - - self.containerNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self, let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction else { - gesture.cancel() - return - } - controllerInteraction.openMessageContextActions(item.message, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) - } - } - - deinit { - self.fetchStatusDisposable.dispose() - self.fetchDisposable.dispose() - } - - override func didLoad() { - super.didLoad() - - self.mediaBadgeNode.pressed = { [weak self] in - self?.progressPressed() - } - - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { _ in - return .waitForSingleTap - } - self.imageNode.view.addGestureRecognizer(recognizer) - } - - func setup(context: AccountContext, item: GridMessageItem, media: Media, messageId: MessageId, controllerInteraction: ChatControllerInteraction, synchronousLoad: Bool) { - self.item = item - - if self.currentState == nil || self.currentState!.0 !== context || !self.currentState!.1.isEqual(to: media) { - var mediaDimensions: CGSize? - if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { - mediaDimensions = largestSize.cgSize - - self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) - - self.fetchStatusDisposable.set(nil) - self.statusNode.transitionToState(.none, completion: { [weak self] in - self?.statusNode.isHidden = true - }) - self.mediaBadgeNode.isHidden = true - self.resourceStatus = nil - } else if let file = media as? TelegramMediaFile, file.isVideo { - mediaDimensions = file.dimensions?.cgSize - self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) - - self.mediaBadgeNode.isHidden = false - - self.resourceStatus = nil - self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: messageId, file: file) |> deliverOnMainQueue).startStrict(next: { [weak self] status in - if let strongSelf = self, let item = strongSelf.item { - strongSelf.resourceStatus = status - - let isStreamable = isMediaStreamable(message: item.message, media: file) - - let statusState: RadialStatusNodeState - if isStreamable { - statusState = .none - } else { - switch status { - case let .Fetching(_, progress): - let adjustedProgress = max(progress, 0.027) - statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true) - case .Local: - statusState = .none - case .Remote, .Paused: - statusState = .download(.white) - } - } - - switch statusState { - case .none: - break - default: - strongSelf.statusNode.isHidden = false - } - - strongSelf.statusNode.transitionToState(statusState, animated: true, completion: { - if let strongSelf = self { - if case .none = statusState { - strongSelf.statusNode.isHidden = true - } - } - }) - - if let duration = file.duration { - let durationString = stringForDuration(Int32(duration)) - - var badgeContent: ChatMessageInteractiveMediaBadgeContent? - var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? - - if isStreamable { - switch status { - case let .Fetching(_, progress): - let progressString = String(format: "%d%%", Int(progress * 100.0)) - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString), iconName: nil) - mediaDownloadState = .compactFetching(progress: 0.0) - case .Local: - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) - case .Remote, .Paused: - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) - mediaDownloadState = .compactRemote - } - } else { - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) - } - - strongSelf.mediaBadgeNode.update(theme: item.theme, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false) - } - } - })) - if self.statusNode.supernode == nil { - self.imageNode.addSubnode(self.statusNode) - } - } else { - self.mediaBadgeNode.isHidden = true - } - - if let mediaDimensions = mediaDimensions { - self.currentState = (context, media, mediaDimensions) - self.setNeedsLayout() - } - } - - self.messageId = messageId - self.controllerInteraction = controllerInteraction - - self.updateSelectionState(animated: false) - self.updateHiddenMedia() - } - - override func layout() { - super.layout() - - let imageFrame = self.bounds - - self.containerNode.frame = imageFrame - - self.imageNode.frame = imageFrame - - if let item = self.item, let (_, _, mediaDimensions) = self.currentState { - let imageSize = mediaDimensions.aspectFilled(imageFrame.size) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))() - } - - self.selectionNode?.frame = CGRect(origin: CGPoint(), size: self.bounds.size) - let progressDiameter: CGFloat = 40.0 - self.statusNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - progressDiameter) / 2.0), y: floor((imageFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) - - self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: imageFrame.width - 3.0, y: imageFrame.height - 18.0 - 3.0), size: CGSize(width: 50.0, height: 50.0)) - } - - func updateSelectionState(animated: Bool) { - if let messageId = self.messageId, let controllerInteraction = self.controllerInteraction { - if let selectionState = controllerInteraction.selectionState { - guard let item = self.item else { - return - } - - let selected = selectionState.selectedIds.contains(messageId) - - if let selectionNode = self.selectionNode { - selectionNode.updateSelected(selected, animated: animated) - selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) - } else { - let selectionNode = GridMessageSelectionNode(theme: item.theme, toggle: { [weak self] value in - if let strongSelf = self, let messageId = strongSelf.messageId { - strongSelf.controllerInteraction?.toggleMessagesSelection([messageId], value) - } - }) - - selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) - self.containerNode.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() - } - } - } - } - } - - func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { - if self.messageId == id { - let imageNode = self.imageNode - return (self.imageNode, self.imageNode.bounds, { [weak self, weak imageNode] in - var statusNodeHidden = false - var accessoryHidden = false - if let strongSelf = self { - statusNodeHidden = strongSelf.statusNode.isHidden - accessoryHidden = strongSelf.mediaBadgeNode.isHidden - strongSelf.statusNode.isHidden = true - strongSelf.mediaBadgeNode.isHidden = true - } - let view = imageNode?.view.snapshotContentTree(unhide: true) - if let strongSelf = self { - strongSelf.statusNode.isHidden = statusNodeHidden - strongSelf.mediaBadgeNode.isHidden = accessoryHidden - } - return (view, nil) - }) - } else { - return nil - } - } - - func updateHiddenMedia() { - if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, controllerInteraction.hiddenMedia[messageId] != nil { - self.imageNode.isHidden = true - self.mediaBadgeNode.alpha = 0.0 - self.statusNode.alpha = 0.0 - } else { - self.imageNode.isHidden = false - if self.statusNode.alpha < 1.0 { - self.statusNode.alpha = 1.0 - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - if self.mediaBadgeNode.alpha < 1.0 { - self.mediaBadgeNode.alpha = 1.0 - self.mediaBadgeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - } - - private func progressPressed() { - guard let controllerInteraction = self.controllerInteraction, let message = self.item?.message else { - return - } - - if let (context, media, _) = self.currentState, let resourceStatus = self.resourceStatus, let file = media as? TelegramMediaFile { - switch resourceStatus { - case .Fetching: - messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) - case .Local: - let _ = controllerInteraction.openMessage(message, .default) - case .Remote, .Paused: - self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: true).startStrict()) - } - } - } - - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - guard let controllerInteraction = self.controllerInteraction, let message = self.item?.message else { - return - } - - switch recognizer.state { - case .ended: - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - if let (_, media, _) = self.currentState, let file = media as? TelegramMediaFile { - if isMediaStreamable(message: message, media: file) { - let _ = controllerInteraction.openMessage(message, .default) - } else { - self.progressPressed() - } - } else { - let _ = controllerInteraction.openMessage(message, .default) - } - case .longTap: - break - default: - break - } - } - default: - break - } - } -} diff --git a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift index 237398e4094..523b598ef44 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift @@ -13,6 +13,7 @@ import ItemListUI import ChatPresentationInterfaceState import ChatControllerInteraction import ChatContextQuery +import ChatInputContextPanelNode private struct HashtagChatInputContextPanelEntryStableId: Hashable { let text: String diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index c2daa7b013b..0099449916d 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -16,6 +16,8 @@ import ChatPresentationInterfaceState import UndoUI import PremiumUI import ChatControllerInteraction +import ChatContextResultPeekContent +import ChatInputContextPanelNode private struct ChatContextResultStableId: Hashable { let result: ChatContextResult diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index b64eb6a485e..8d934b11f3f 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -16,6 +16,7 @@ import ChatPresentationInterfaceState import PremiumUI import UndoUI import ChatControllerInteraction +import ChatInputContextPanelNode final class HorizontalStickersChatContextPanelInteraction { var previewedStickerItem: TelegramMediaFile? diff --git a/submodules/TelegramUI/Sources/InChatPrefetchManager.swift b/submodules/TelegramUI/Sources/InChatPrefetchManager.swift index 07ccd575dfe..eb649810a1f 100644 --- a/submodules/TelegramUI/Sources/InChatPrefetchManager.swift +++ b/submodules/TelegramUI/Sources/InChatPrefetchManager.swift @@ -6,6 +6,7 @@ import TelegramUIPreferences import AccountContext import PhotoResources import UniversalMediaPlayer +import ChatMessageInteractiveMediaNode private final class PrefetchMediaContext { let fetchDisposable = MetaDisposable() diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 22f646c30c9..4ace9c8922a 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -15,6 +15,7 @@ import ChatPresentationInterfaceState import PremiumUI import UndoUI import ChatControllerInteraction +import ChatInputContextPanelNode private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollViewDelegate { private final class DisplayItem { diff --git a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift new file mode 100644 index 00000000000..0fc439dd797 --- /dev/null +++ b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift @@ -0,0 +1,56 @@ +import Foundation +import SwiftSignalKit +import TelegramCore +import Postbox +import AccountContext +import BuildConfig +import TelegramPresentationData + +private var sharedTempContext: SharedAccountContextImpl? + +public func makeTempContext( + sharedContainerPath: String, + rootPath: String, + appGroupPath: String, + accountManager: AccountManager, + appLockContext: AppLockContext, + encryptionParameters: ValueBoxEncryptionParameters, + applicationBindings: TelegramApplicationBindings, + initialPresentationDataAndSettings: InitialPresentationDataAndSettings, + networkArguments: NetworkInitializationArguments, + buildConfig: BuildConfig +) -> Signal { + let sharedContext = sharedTempContext ?? SharedAccountContextImpl( + mainWindow: nil, + sharedContainerPath: sharedContainerPath, + basePath: rootPath, + encryptionParameters: encryptionParameters, + accountManager: accountManager, + appLockContext: appLockContext, + applicationBindings: applicationBindings, + initialPresentationDataAndSettings: initialPresentationDataAndSettings, + networkArguments: networkArguments, + hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, + rootPath: rootPath, + legacyBasePath: appGroupPath, + apsNotificationToken: .single(nil), + voipNotificationToken: .single(nil), + firebaseSecretStream: .never(), + setNotificationCall: { _ in + }, + navigateToChat: { _, _, _ in + }, displayUpgradeProgress: { _ in + }, + appDelegate: nil + ) + sharedTempContext = sharedContext + + return sharedContext.activeAccountContexts + |> take(1) + |> mapToSignal { accounts -> Signal in + guard let context = accounts.primary else { + return .complete() + } + return .single(context) + } +} diff --git a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift index c532856a60a..a740df5b28b 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift @@ -17,6 +17,7 @@ import ItemListUI import ChatPresentationInterfaceState import ChatControllerInteraction import ChatContextQuery +import ChatInputContextPanelNode private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { let index: Int diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index ec3577498c0..7782fe6a07f 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -17,6 +17,7 @@ import LegacyInstantVideoController import StoryContainerScreen import CameraScreen import MediaEditorScreen +import ChatControllerInteraction public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) { if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) { @@ -72,11 +73,11 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.updateTextInputState(updateTextInputState) } var popAndComplete = true - if let subject = params.subject, case let .message(messageSubject, _, timecode) = subject { + if let subject = params.subject, case let .message(messageSubject, highlight, timecode) = subject { if case let .id(messageId) = messageSubject { let navigationController = params.navigationController let animated = params.animated - controller.navigateToMessage(messageLocation: .id(messageId, timecode), animated: isFirst, completion: { [weak navigationController, weak controller] in + controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: highlight?.quote)), animated: isFirst, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) } @@ -316,7 +317,7 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee context: context, chatLocation: .replyThread(result.message), chatLocationContextHolder: result.contextHolder, - subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }, + subject: messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) }, activateInput: actualActivateInput, keepStack: keepStack ) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 3471adcb02f..ea1bffe859d 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -168,7 +168,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() let controllerParams = LocationViewParams(sendLiveLocation: { location in - let outMessage: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let outMessage: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) params.enqueueMessage(outMessage) }, stopLiveLocation: { messageId in params.context.liveLocationManager?.cancelLiveLocation(peerId: messageId?.peerId ?? params.message.id.peerId) @@ -366,7 +366,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return false } -func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { +func openChatInstantPageImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { if let (webpage, anchor) = instantPageAndAnchor(message: message) { let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel) @@ -375,7 +375,7 @@ func openChatInstantPage(context: AccountContext, message: Message, sourcePeerTy } } -func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { +func openChatWallpaperImpl(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { let _ = (context.sharedContext.resolveUrl(context: context, peerId: nil, url: content.url, skipUrlAuth: true) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 6401288d2be..5c9c0df71bd 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -187,7 +187,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur dismissInput() navigationController?.pushViewController(controller) case let .channelMessage(peer, messageId, timecode): - openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: true, timecode: timecode), peekData: nil)) + openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode), peekData: nil)) case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = navigationController { let _ = ChatControllerImpl.openMessageReplies(context: context, navigationController: navigationController, present: { c, a in @@ -318,6 +318,12 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur if let to = to { if to.hasPrefix("@") { let _ = (context.engine.peers.resolvePeerByName(name: String(to[to.index(to.startIndex, offsetBy: 1)...])) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> deliverOnMainQueue).startStandalone(next: { peer in if let peer = peer { context.sharedContext.applicationBindings.dismissNativeController() @@ -595,7 +601,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } case let .startAttach(peerId, payload, choose): let presentError: (String) -> Void = { errorText in - present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText, timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) } @@ -835,122 +841,118 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur }), nil) } }) - case let .boost(peerId, status, canApplyStatus): + case let .boost(peerId, status, myBoostStatus): + var isCurrent = false + if case let .chat(chatPeerId, _) = urlContext, chatPeerId == peerId { + isCurrent = true + } var forceDark = false if let updatedPresentationData, updatedPresentationData.initial.theme.overallDarkAppearance { forceDark = true } - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let peer, let status else { - return - } - - var isBoosted = false - if case let .error(error) = canApplyStatus, case .peerBoostAlreadyActive = error { - isBoosted = true - } - - var isCurrent = false - if case let .chat(chatPeerId, _) = urlContext, chatPeerId == peerId { - isCurrent = true - } - - var currentLevel = Int32(status.level) - var currentLevelBoosts = Int32(status.currentLevelBoosts) - var nextLevelBoosts = status.nextLevelBoosts.flatMap(Int32.init) - - if isBoosted && status.boosts == currentLevelBoosts { - currentLevel = max(0, currentLevel - 1) - nextLevelBoosts = currentLevelBoosts - currentLevelBoosts = max(0, currentLevelBoosts - 1) + + var dismissedImpl: (() -> Void)? + if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { + let updateExternalController = storyProgressPauseContext.update + dismissedImpl = { + updateExternalController(nil) } - - let subject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: currentLevel, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: nil, boosted: isBoosted) - let nextSubject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: currentLevel, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: nil, boosted: true) - let nextCount = Int32(status.boosts + 1) - - var updateImpl: (() -> Void)? - var dismissImpl: (() -> Void)? - let controller = PremiumLimitScreen(context: context, subject: subject, count: Int32(status.boosts), forceDark: forceDark, action: { - if isBoosted { - return true - } - var dismiss = false - switch canApplyStatus { - case .ok: - updateImpl?() - case let .replace(previousPeer): - let text = presentationData.strings.ChannelBoost_ReplaceBoost(previousPeer.compactDisplayTitle, peer.compactDisplayTitle).string - let controller = replaceBoostConfirmationController(context: context, fromPeer: previousPeer, toPeer: peer, text: text, commit: { - updateImpl?() - }) - present(controller, nil) - case let .error(error): - let title: String? - let text: String - - var actions: [TextAlertAction] = [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) - ] - - switch error { - case .generic: - title = nil - text = presentationData.strings.Login_UnknownError - case let .floodWait(timeout): - title = presentationData.strings.ChannelBoost_Error_BoostTooOftenTitle - let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime, preferLowerValue: false) - text = presentationData.strings.ChannelBoost_Error_BoostTooOftenText(valueText).string - dismiss = true - case .premiumRequired: - title = presentationData.strings.ChannelBoost_Error_PremiumNeededTitle - text = presentationData.strings.ChannelBoost_Error_PremiumNeededText - actions = [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { - dismissImpl?() - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: false, dismissed: nil) - navigationController?.pushViewController(controller) - }) - ] - case .giftedPremiumNotAllowed: - title = presentationData.strings.ChannelBoost_Error_GiftedPremiumNotAllowedTitle - text = presentationData.strings.ChannelBoost_Error_GiftedPremiumNotAllowedText - dismiss = true - case .peerBoostAlreadyActive: - return true - } - - let controller = textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, title: title, text: text, actions: actions, parseMarkdown: true) - present(controller, nil) - } - return dismiss - }, + } + + PremiumBoostScreen( + context: context, + contentContext: contentContext, + peerId: peerId, + isCurrent: isCurrent, + status: status, + myBoostStatus: myBoostStatus, + forceDark: forceDark, openPeer: { peer in openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) - }) - navigationController?.pushViewController(controller) - - if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { - storyProgressPauseContext.update(controller) + }, + presentController: { [weak navigationController] c in + (navigationController?.viewControllers.last as? ViewController)?.present(c, in: .window(.root)) + }, + pushController: { [weak navigationController] c in + navigationController?.pushViewController(c) - let updateExternalController = storyProgressPauseContext.update - controller.disposed = { - updateExternalController(nil) + if c is PremiumLimitScreen { + if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { + storyProgressPauseContext.update(c) + } } + }, dismissed: { + dismissedImpl?() } - - updateImpl = { [weak controller] in - if let _ = status.nextLevelBoosts { - let _ = context.engine.peers.applyChannelBoost(peerId: peerId).startStandalone() - controller?.updateSubject(nextSubject, count: nextCount) - } else { - dismissImpl?() + ) + case let .premiumGiftCode(slug): + var forceDark = false + if let updatedPresentationData, updatedPresentationData.initial.theme.overallDarkAppearance { + forceDark = true + } + let _ = (context.engine.payments.checkPremiumGiftCode(slug: slug) + |> 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)) + } + }, + 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)) + }) + }, + 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) + } + ) + dismissImpl = { [weak controller] in + controller?.dismissAnimated() } - } - dismissImpl = { [weak controller] in - controller?.dismiss() + 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 78cc7bde7b3..6a0ef980926 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -75,13 +75,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu return false } }, openPeer: { _, _, _, _ in - }, openPeerMention: { _ in + }, openPeerMention: { _, _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _ in + }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { @@ -98,7 +98,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in - }, openUrl: { _, _, _, _ in + }, openUrl: { _ in }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in @@ -123,6 +123,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -135,7 +137,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in @@ -166,11 +168,14 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, activateAdAction: { _ in }, openRequestedPeerSelection: { _, _, _ in }, saveMediaToFiles: { _ in + }, openNoAdsDemo: { + }, displayGiveawayParticipationStatus: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() @@ -203,14 +208,14 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu let source: ChatHistoryListSource if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, at, loadMore) = playlistLocation { - source = .custom(messages: messages, messageId: at, loadMore: loadMore) + source = .custom(messages: messages, messageId: at, quote: nil, loadMore: loadMore) self.isGlobalSearch = true } else { source = .default self.isGlobalSearch = false } - self.historyNode = ChatHistoryListNode(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: .id(initialMessageId), highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) + self.historyNode = ChatHistoryListNode(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) self.historyNode.clipsToBounds = true super.init() @@ -552,7 +557,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } let chatLocationContextHolder = Atomic(value: nil) - let historyNode = ChatHistoryListNode(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: .id(messageId), highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) + let historyNode = ChatHistoryListNode(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) historyNode.clipsToBounds = true historyNode.preloadPages = true historyNode.stackFromBottom = true diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift index 76dd958791e..92ec3281105 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import UniversalMediaPlayer import AccountContext import AppBundle +import InstantVideoRadialStatusNode private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayInstantVideoShadow")?.precomposed() diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift index c129a42b4c5..828a840b8e5 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift @@ -138,7 +138,7 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { transition.updateFrame(node: self.iconNode, frame: iconFrame) } else if let iconSignal = item.iconSignal { self.iconDisposable.set((iconSignal - |> deliverOnMainQueue).start(next: { [weak self] image in + |> deliverOnMainQueue).startStrict(next: { [weak self] image in if let strongSelf = self, let image { strongSelf.iconNode.image = image let iconFrame = CGRect(origin: CGPoint(x: iconInset, y: floorToScreenPixels((height - image.size.height) / 2.0)), size: image.size) diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift index 0b4a4de9c43..760f6f41d1a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift @@ -8,12 +8,13 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { case none case text(String) case badge(String, UIColor) + case semitransparentBadge(String, UIColor) var text: String { switch self { case .none: return "" - case let .text(text), let .badge(text, _): + case let .text(text), let .badge(text, _), let .semitransparentBadge(text, _): return text } } @@ -22,7 +23,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { switch self { case .none, .text: return nil - case let .badge(_, color): + case let .badge(_, color), let .semitransparentBadge(_, color): return color } } @@ -60,7 +61,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { private let bottomSeparatorNode: ASDisplayNode private let activateArea: AccessibilityAreaNode - private var iconDisposable: Disposable? + private var iconDisposable = MetaDisposable() private var item: PeerInfoScreenDisclosureItem? @@ -115,7 +116,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { } deinit { - self.iconDisposable?.dispose() + self.iconDisposable.dispose() } override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -135,11 +136,14 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - + let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor let labelColorValue: UIColor let labelFont: UIFont - if case .badge = item.label { + if case let .semitransparentBadge(_, color) = item.label { + labelColorValue = color + labelFont = Font.semibold(14.0) + } else if case .badge = item.label { labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor labelFont = Font.regular(15.0) } else { @@ -152,7 +156,11 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue) let textSize = self.textNode.updateLayout(CGSize(width: width - (leftInset + rightInset), height: .greatestFiniteMagnitude)) - let labelSize = self.labelNode.updateLayout(CGSize(width: width - textSize.width - (leftInset + rightInset), height: .greatestFiniteMagnitude)) + var labelConstrainWidth = width - textSize.width - (leftInset + rightInset) + if case .semitransparentBadge = item.label { + labelConstrainWidth -= 16.0 + } + let labelSize = self.labelNode.updateLayout(CGSize(width: labelConstrainWidth, height: .greatestFiniteMagnitude)) let textFrame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: textSize) @@ -169,12 +177,12 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { } else if let iconSignal = item.iconSignal { if previousItem?.text != item.text { self.iconNode.image = nil - self.iconDisposable = (iconSignal - |> deliverOnMainQueue).start(next: { [weak self] icon in + self.iconDisposable.set((iconSignal + |> deliverOnMainQueue).startStrict(next: { [weak self] icon in if let self { self.iconNode.image = icon } - }) + })) } iconSize = CGSize(width: 29.0, height: 29.0) } else { @@ -193,8 +201,16 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { transition.updateFrame(node: self.arrowNode, frame: arrowFrame) } - let badgeDiameter: CGFloat = 20.0 - if case let .badge(text, badgeColor) = item.label, !text.isEmpty { + var badgeDiameter: CGFloat = 20.0 + if case let .semitransparentBadge(text, badgeColor) = item.label, !text.isEmpty { + badgeDiameter = 24.0 + if previousItem?.label.badgeColor != badgeColor { + self.labelBadgeNode.image = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor.withAlphaComponent(0.1)) + } + if self.labelBadgeNode.supernode == nil { + self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode) + } + } else if case let .badge(text, badgeColor) = item.label, !text.isEmpty { if previousItem?.label.badgeColor != badgeColor { self.labelBadgeNode.image = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } @@ -205,15 +221,20 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.labelBadgeNode.removeFromSupernode() } - let badgeWidth = max(badgeDiameter, labelSize.width + 10.0) + var badgeWidth = max(badgeDiameter, labelSize.width + 10.0) + if case .semitransparentBadge = item.label { + badgeWidth += 2.0 + } let labelFrame: CGRect - if case .badge = item.label { + if case .semitransparentBadge = item.label { + labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) + } else if case .badge = item.label { labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) } else { labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize) } - let labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) + let labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) self.activateArea.accessibilityLabel = item.text self.activateArea.accessibilityValue = item.label.text diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift index 414ea0ddf7c..436a667ed99 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift @@ -19,7 +19,7 @@ enum PeerInfoScreenMemberItemAction { final class PeerInfoScreenMemberItem: PeerInfoScreenItem { let id: AnyHashable - let context: AccountContext + let context: ItemListPeerItem.Context let enclosingPeer: Peer? let member: PeerInfoMember let badge: String? @@ -30,7 +30,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem { init( id: AnyHashable, - context: AccountContext, + context: ItemListPeerItem.Context, enclosingPeer: Peer?, member: PeerInfoMember, badge: String? = nil, @@ -149,7 +149,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { } } - let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.account.peerId, peer: item.enclosingPeer, member: item.member) + let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member) var options: [ItemListPeerItemRevealOption] = [] if actions.contains(.promote) && item.enclosingPeer is TelegramChannel { @@ -272,7 +272,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { } var highlight = point != nil if case .account = item.member { - } else if item.context.account.peerId == item.member.id { + } else if item.context.accountPeerId == item.member.id { highlight = false } if let point, let itemNode = self.itemNode, let value = itemNode.view.hitTest(self.view.convert(point, to: itemNode.view), with: nil), value is UIControl { diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index a310357e99b..7033542b75b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -118,7 +118,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { self.addSubnode(self.listNode) self.disposable = (groupsInCommonContext.state - |> deliverOnMainQueue).start(next: { [weak self] state in + |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift index bc6ceffaf85..b91dd7792ca 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -17,6 +17,7 @@ import UndoUI import ChatPresentationInterfaceState import ChatControllerInteraction import PeerInfoVisualMediaPaneNode +import ChatMessageItemView final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext @@ -127,7 +128,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { return .single(nil) } } - |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in + |> deliverOnMainQueue).startStrict(next: { [weak self] playlistStateAndType in guard let strongSelf = self else { return } @@ -275,7 +276,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { }) return rate } - |> deliverOnMainQueue).start(next: { baseRate in + |> deliverOnMainQueue).startStandalone(next: { baseRate in guard let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType else { return } @@ -366,7 +367,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation { if type == .music { - let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) + let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic(value: nil), tagMask: MessageTags.music) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -392,7 +393,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { progressDisposable.dispose() } } - |> deliverOnMainQueue).start(next: { index in + |> deliverOnMainQueue).startStandalone(next: { index in guard let strongSelf = self else { return } @@ -409,7 +410,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { } else if index.1 { if !progressStarted { progressStarted = true - progressDisposable.set(progressSignal.start()) + progressDisposable.set(progressSignal.startStandalone()) } } }, completed: { diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift index 09b9a01d1f2..3583fa23312 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -201,7 +201,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { self.presentationDataPromise.get(), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) ) - |> deliverOnMainQueue).start(next: { [weak self] state, presentationData, enclosingPeer in + |> deliverOnMainQueue).startStrict(next: { [weak self] state, presentationData, enclosingPeer in guard let strongSelf = self, let enclosingPeer = enclosingPeer else { return } @@ -222,6 +222,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { } deinit { + self.disposable?.dispose() } func ensureMessageIsVisible(id: MessageId) { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 96f1459d127..40c5eca7d67 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2855,7 +2855,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { return .complete() } - }).start(next: { fileAndPackTitle in + }).startStrict(next: { fileAndPackTitle in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift index a784f89437e..2654f9840f9 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift @@ -188,7 +188,7 @@ private final class PeerInfoMembersContextImpl { self.channelMembersControl = control self.peerDisposable.set((context.account.postbox.peerView(id: peerId) - |> deliverOn(self.queue)).start(next: { [weak self] view in + |> deliverOn(self.queue)).startStrict(next: { [weak self] view in guard let strongSelf = self else { return } @@ -223,7 +223,7 @@ private final class PeerInfoMembersContextImpl { })) } else if peerId.namespace == Namespaces.Peer.CloudGroup { self.disposable.set((context.account.postbox.peerView(id: peerId) - |> deliverOn(self.queue)).start(next: { [weak self] view in + |> deliverOn(self.queue)).startStrict(next: { [weak self] view in guard let strongSelf = self, let cachedData = view.cachedData as? CachedGroupData, let participantsData = cachedData.participants else { return } @@ -316,7 +316,7 @@ private final class PeerInfoMembersContextImpl { self.pushState() disposable.set((signal - |> deliverOn(self.queue)).start(completed: { + |> deliverOn(self.queue)).startStrict(completed: { completed() })) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 125ed2e2090..a375e520c7b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -421,7 +421,7 @@ private final class PeerInfoPendingPane { self.pane = PeerInfoPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in self?.isReady = true hasBecomeReady(key) }) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 0ddf294ee43..5c6f92ca129 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -104,6 +104,8 @@ import ChatAvatarNavigationNode import PeerReportScreen import WebUI import ShareWithPeersScreen +import ItemListPeerItem +import PeerNameColorScreen enum PeerInfoAvatarEditingMode { case generic @@ -322,6 +324,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, forwardMessages: { _ in }, updateForwardOptionsState: { _ in }, presentForwardOptions: { _ in + }, presentReplyOptions: { _ in + }, presentLinkOptions: { _ in }, shareSelectedMessages: { shareMessages() }, updateTextInputStateAndMode: { _ in @@ -431,7 +435,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: self.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) + let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: self.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics, isMediaInputExpanded: false) transition.updateFrame(node: self.selectionPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeight))) @@ -540,6 +544,7 @@ private final class PeerInfoInteraction { let openAddBotToGroup: () -> Void let performBotCommand: (PeerInfoBotCommand) -> Void let editingOpenPublicLinkSetup: () -> Void + let editingOpenNameColorSetup: () -> Void let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void let editingToggleMessageSignatures: (Bool) -> Void @@ -593,6 +598,7 @@ private final class PeerInfoInteraction { openAddBotToGroup: @escaping () -> Void, performBotCommand: @escaping (PeerInfoBotCommand) -> Void, editingOpenPublicLinkSetup: @escaping () -> Void, + editingOpenNameColorSetup: @escaping () -> Void, editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, editingToggleMessageSignatures: @escaping (Bool) -> Void, @@ -645,6 +651,7 @@ private final class PeerInfoInteraction { self.openAddBotToGroup = openAddBotToGroup self.performBotCommand = performBotCommand self.editingOpenPublicLinkSetup = editingOpenPublicLinkSetup + self.editingOpenNameColorSetup = editingOpenNameColorSetup self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup self.editingToggleMessageSignatures = editingToggleMessageSignatures @@ -780,7 +787,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } })) items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).string, action: { - let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePhoneNumber).start() + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePhoneNumber).startStandalone() })) items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_ChangePhoneNumber, action: { interaction.openSettings(.phoneNumber) @@ -789,7 +796,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: presentationData.strings.Settings_CheckPasswordTitle, text: .markdown(presentationData.strings.Settings_CheckPasswordText), linkAction: { _ in })) items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: presentationData.strings.Settings_KeepPassword, action: { - let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePassword).start() + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePassword).startStandalone() })) items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_TryEnterPassword, action: { interaction.openSettings(.rememberPassword) @@ -804,8 +811,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p if !settings.accountsAndPeers.isEmpty { for (peerAccountContext, peer, badgeCount) in settings.accountsAndPeers { + let mappedContext = ItemListPeerItem.Context.custom(ItemListPeerItem.Context.Custom( + accountPeerId: peerAccountContext.account.peerId, + postbox: peerAccountContext.account.postbox, + network: peerAccountContext.account.network, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + isPremiumDisabled: false, + resolveInlineStickers: { fileIds in + return context.engine.stickers.resolveInlineStickers(fileIds: fileIds) + } + )) let member: PeerInfoMember = .account(peer: RenderedPeer(peer: peer._asPeer())) - items[.accounts]!.append(PeerInfoScreenMemberItem(id: member.id, context: context.sharedContext.makeTempAccountContext(account: peerAccountContext.account), enclosingPeer: nil, member: member, badge: badgeCount > 0 ? "\(compactNumericCountString(Int(badgeCount), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))" : nil, isAccount: true, action: { action in + items[.accounts]!.append(PeerInfoScreenMemberItem(id: member.id, context: mappedContext, enclosingPeer: nil, member: member, badge: badgeCount > 0 ? "\(compactNumericCountString(Int(badgeCount), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))" : nil, isAccount: true, action: { action in switch action { case .open: interaction.switchToAccount(peerAccountContext.account.id) @@ -877,11 +895,11 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: .zero)) return context?.generateImage() } - let _ = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).start() + let _ = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).startStandalone() } else { iconSignal = .single(UIImage(bundleImageName: "Settings/Menu/Websites")!) } - items[.apps]!.append(PeerInfoScreenDisclosureItem(id: bot.peer.id.id._internalGetInt64Value(), text: bot.peer.compactDisplayTitle, icon: nil, iconSignal: iconSignal, action: { + items[.apps]!.append(PeerInfoScreenDisclosureItem(id: bot.peer.id.id._internalGetInt64Value(), text: bot.shortName, icon: nil, iconSignal: iconSignal, action: { interaction.openBotApp(bot) })) appIndex += 1 @@ -1483,7 +1501,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese for member in memberList { let isAccountPeer = member.id == context.account.peerId - items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: context, enclosingPeer: peer, member: member, isAccount: false, action: isAccountPeer ? nil : { action in + items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer, member: member, isAccount: false, action: isAccountPeer ? nil : { action in switch action { case .open: interaction.openPeerInfo(member.peer, true) @@ -1654,18 +1672,18 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL switch channel.info { case .broadcast: let ItemUsername = 1 - let ItemInviteLinks = 2 - let ItemDiscussionGroup = 3 - let ItemSignMessages = 4 - let ItemSignMessagesHelp = 5 - let ItemDeleteChannel = 6 - let ItemReactions = 7 - - let ItemAdmins = 8 - let ItemMembers = 9 - let ItemMemberRequests = 10 - let ItemBanned = 11 - let ItemRecentActions = 12 + 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 ItemBanned = 12 + let ItemRecentActions = 13 let isCreator = channel.flags.contains(.isCreator) @@ -1680,7 +1698,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.editingOpenPublicLinkSetup() })) } - + if (isCreator && (channel.addressName?.isEmpty ?? true)) || (!channel.flags.contains(.isCreator) && channel.adminRights?.rights.contains(.canInviteUsers) == true) { let invitesText: String if let count = data.invitations?.count, count > 0 { @@ -1733,6 +1751,13 @@ 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: "Channel Color", icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { + interaction.editingOpenNameColorSetup() + })) + } + if isCreator || (channel.adminRights != nil && channel.hasPermission(.sendSomething)) { let messagesShouldHaveSignatures: Bool switch channel.info { @@ -2396,6 +2421,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenPublicLinkSetup: { [weak self] in self?.editingOpenPublicLinkSetup() }, + editingOpenNameColorSetup: { [weak self] in + self?.editingOpenNameColorSetup() + }, editingOpenInviteLinksSetup: { [weak self] in self?.editingOpenInviteLinksSetup() }, @@ -2519,7 +2547,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return strongSelf.openMessage(id: message.id) }, openPeer: { [weak self] peer, navigation, _, _ in self?.openPeer(peerId: peer.id, navigation: navigation) - }, openPeerMention: { _ in + }, openPeerMention: { _, _ in }, openMessageContextMenu: { [weak self] message, _, node, frame, anyRecognizer, _ in guard let strongSelf = self, let node = node as? ContextExtractedContentContainingNode else { return @@ -2542,7 +2570,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let gesture: ContextGesture? = anyRecognizer as? ContextGesture let _ = (chatAvailableMessageActionsImpl(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id]) - |> deliverOnMainQueue).start(next: { actions in + |> deliverOnMainQueue).startStandalone(next: { actions in guard let strongSelf = self else { return } @@ -2553,7 +2581,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro c.dismiss(completion: { if let strongSelf = self, let currentPeer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController { if let channel = currentPeer as? TelegramChannel, channel.flags.contains(.isForum), let threadId = message.threadId { - let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, keepStack: .default).start() + let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, keepStack: .default).startStandalone() } else { let targetLocation: NavigateToChatControllerParams.Location if case let .replyThread(message) = strongSelf.chatLocation { @@ -2563,7 +2591,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let currentPeerId = strongSelf.peerId - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { var viewControllers = navigationController.viewControllers var indexesToRemove = Set() var keptCurrentChatController = false @@ -2643,7 +2671,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } }) }))) @@ -2662,7 +2690,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).startStandalone() } }) }))) @@ -2670,7 +2698,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } return ContextController.Items(content: .list(items)) - }, minHeight: nil) + }, minHeight: nil, animated: true) }))) } if strongSelf.searchDisplayController == nil { @@ -2699,7 +2727,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let _ = (chatMediaListPreviewControllerData(context: strongSelf.context, chatLocation: .peer(id: message.id.peerId), chatLocationContextHolder: Atomic(value: nil), message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.controller?.navigationController as? NavigationController) - |> deliverOnMainQueue).start(next: { previewData in + |> deliverOnMainQueue).startStandalone(next: { previewData in guard let strongSelf = self else { gesture?.cancel() return @@ -2715,7 +2743,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro c.dismiss(completion: { if let strongSelf = self, let currentPeer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController { if let channel = currentPeer as? TelegramChannel, channel.flags.contains(.isForum), let threadId = message.threadId { - let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, keepStack: .default).start() + let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, keepStack: .default).startStandalone() } else { let targetLocation: NavigateToChatControllerParams.Location if case let .replyThread(message) = strongSelf.chatLocation { @@ -2725,7 +2753,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let currentPeerId = strongSelf.peerId - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { var viewControllers = navigationController.viewControllers var indexesToRemove = Set() var keptCurrentChatController = false @@ -2793,7 +2821,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } }) }))) @@ -2812,7 +2840,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).startStandalone() } }) }))) @@ -2820,7 +2848,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } return ContextController.Items(content: .list(items)) - }, minHeight: nil) + }, minHeight: nil, animated: true) }))) } @@ -2849,7 +2877,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } }) - }, navigateToMessage: { fromId, id in + }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { @@ -2886,11 +2914,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in - }, openUrl: { [weak self] url, concealed, external, _ in + }, openUrl: { [weak self] url in guard let strongSelf = self else { return } - strongSelf.openUrl(url: url, concealed: concealed, external: external ?? false) + strongSelf.openUrl(url: url.url, concealed: url.concealed, external: url.external ?? false) }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in @@ -2910,7 +2938,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if let foundGalleryMessage = foundGalleryMessage { - openChatInstantPage(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) } }, openWallpaper: { _ in }, openTheme: { _ in @@ -2981,6 +3009,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -2993,7 +3023,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in @@ -3024,14 +3054,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, activateAdAction: { _ in }, openRequestedPeerSelection: { _, _, _ in }, saveMediaToFiles: { _ in + }, openNoAdsDemo: { + }, displayGiveawayParticipationStatus: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) - self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in guard let strongSelf = self else { return } @@ -3275,7 +3308,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = self?.headerNode.avatarListNode.listContainerNode.deleteItem(item) } } - strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in + strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).startStrict(next: { entry in self?.headerNode.updateAvatarIsHidden(entry: entry) })) strongSelf.view.endEditing(true) @@ -3360,13 +3393,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let context = strongSelf.context controller.completion = { [weak controller] title, fileId, _, isHidden in let _ = (context.engine.peers.editForumChannelTopic(id: peerId, threadId: threadId, title: title, iconFileId: fileId) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStandalone(completed: { controller?.dismiss() }) if let isHidden = isHidden { let _ = (context.engine.peers.setForumChannelTopicHidden(id: peerId, threadId: threadId, isHidden: isHidden) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStandalone(completed: { controller?.dismiss() }) } @@ -3458,7 +3491,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.present(statusController, in: .window(.root)) } strongSelf.activeActionDisposable.set((combineLatest(updateNameSignal, updateBioSignal) |> deliverOnMainQueue - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStrict(completed: { dismissStatus?() guard let strongSelf = self else { @@ -3515,7 +3548,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.present(statusController, in: .window(.root)) } strongSelf.activeActionDisposable.set((combineLatest(updateNameSignal, updateBioSignal) |> deliverOnMainQueue - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStrict(completed: { dismissStatus?() guard let strongSelf = self else { @@ -3546,7 +3579,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.present(statusController, in: .window(.root)) strongSelf.activeActionDisposable.set((context.engine.contacts.updateContactName(peerId: peer.id, firstName: firstName, lastName: lastName) - |> deliverOnMainQueue).start(error: { _ in + |> deliverOnMainQueue).startStrict(error: { _ in dismissStatus?() guard let strongSelf = self else { @@ -3580,7 +3613,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return .complete() } } - }).start() + }).startStandalone() strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil) })) } @@ -3630,7 +3663,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.present(statusController, in: .window(.root)) } strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) - |> deliverOnMainQueue).start(error: { _ in + |> deliverOnMainQueue).startStrict(error: { _ in dismissStatus?() guard let strongSelf = self else { @@ -3689,7 +3722,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.present(statusController, in: .window(.root)) } strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) - |> deliverOnMainQueue).start(error: { _ in + |> deliverOnMainQueue).startStrict(error: { _ in dismissStatus?() guard let strongSelf = self else { @@ -3812,7 +3845,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })) } - let contextMenuController = ContextMenuController(actions: actions) + let contextMenuController = makeContextMenuController(actions: actions) strongSelf.controller?.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (node, node.bounds.insetBy(dx: 0.0, dy: -2.0), strongSelf, strongSelf.view.bounds) @@ -3860,9 +3893,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, - isStatusSelection: true, - isReactionSelection: false, - isEmojiSelection: false, + subject: .status, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: false, @@ -3899,7 +3930,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in guard let strongSelf = self else { return } @@ -3919,7 +3950,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let _ = (source - |> deliverOnMainQueue).start(next: { [weak self] source in + |> deliverOnMainQueue).startStandalone(next: { [weak self] source in guard let strongSelf = self else { return } @@ -3980,7 +4011,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let icon = threadData.info.icon, icon != 0 { let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [icon]) - |> deliverOnMainQueue).start(next: { [weak self] files in + |> deliverOnMainQueue).startStandalone(next: { [weak self] files in if let file = files.first?.value { var stickerPackReference: StickerPackReference? for attribute in file.attributes { @@ -3992,7 +4023,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let stickerPackReference = stickerPackReference { let _ = (strongSelf.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false) - |> deliverOnMainQueue).start(next: { [weak self] stickerPack in + |> deliverOnMainQueue).startStandalone(next: { [weak self] stickerPack in if let strongSelf = self, case let .result(info, _, _) = stickerPack { strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: strongSelf.presentationData.strings.PeerInfo_TopicIconInfoText(info.title).string, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self, action == .undo { @@ -4034,7 +4065,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro queue: Queue.mainQueue(), screenData, self.forceIsContactPromise.get() - ).start(next: { [weak self] data, forceIsContact in + ).startStrict(next: { [weak self] data, forceIsContact in guard let strongSelf = self else { return } @@ -4054,7 +4085,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } self.customStatusDisposable = (self.customStatusPromise.get() - |> deliverOnMainQueue).start(next: { [weak self] value in + |> deliverOnMainQueue).startStrict(next: { [weak self] value in guard let strongSelf = self else { return } @@ -4064,11 +4095,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } }) - self.refreshMessageTagStatsDisposable = context.engine.messages.refreshMessageTagStats(peerId: peerId, threadId: chatLocation.threadId, tags: [.video, .photo, .gif, .music, .voiceOrInstantVideo, .webPage, .file]).start() + self.refreshMessageTagStatsDisposable = context.engine.messages.refreshMessageTagStats(peerId: peerId, threadId: chatLocation.threadId, tags: [.video, .photo, .gif, .music, .voiceOrInstantVideo, .webPage, .file]).startStrict() if peerId.namespace == Namespaces.Peer.CloudChannel { self.translationStateDisposable = (chatTranslationState(context: context, peerId: peerId) - |> deliverOnMainQueue).start(next: { [weak self] translationState in + |> deliverOnMainQueue).startStrict(next: { [weak self] translationState in self?.translationState = translationState }) } @@ -4081,7 +4112,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro |> map { value -> Float? in return value[peerId] } - |> distinctUntilChanged).start(next: { [weak self] value in + |> distinctUntilChanged).startStrict(next: { [weak self] value in guard let self else { return } @@ -4099,7 +4130,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), expiringStoryList.state ) - |> deliverOnMainQueue).start(next: { [weak self] peer, state in + |> deliverOnMainQueue).startStrict(next: { [weak self] peer, state in guard let self, let peer else { return } @@ -4168,6 +4199,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.copyProtectionTooltipController?.dismiss() self.expiringStoryListDisposable?.dispose() self.postingAvailabilityDisposable?.dispose() + self.storyUploadProgressDisposable?.dispose() } override func didLoad() { @@ -4204,7 +4236,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let channel = data.peer as? TelegramChannel, channel.flags.contains(.isForum), self.chatLocation.threadId == nil { if self.forumTopicNotificationExceptionsDisposable == nil { self.forumTopicNotificationExceptionsDisposable = (self.context.engine.peers.forumChannelTopicNotificationExceptions(id: channel.id) - |> deliverOnMainQueue).start(next: { [weak self] list in + |> deliverOnMainQueue).startStrict(next: { [weak self] list in guard let strongSelf = self else { return } @@ -4347,7 +4379,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: self.peerId, singlePeer: true) let _ = (storyContent.state |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] storyContentState in + |> deliverOnMainQueue).startStandalone(next: { [weak self] storyContentState in guard let self else { return } @@ -4543,14 +4575,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let timestamp = timestamp { storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: AudioPlaybackRate(playbackRate)) } - let _ = updateMediaPlaybackStoredStateInteractively(engine: strongSelf.context.engine, messageId: messageId, state: storedState).start() + let _ = updateMediaPlaybackStoredStateInteractively(engine: strongSelf.context.engine, messageId: messageId, state: storedState).startStandalone() }, editMedia: { [weak self] messageId, snapshots, transitionCompletion in guard let strongSelf = self else { return } let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) - |> deliverOnMainQueue).start(next: { [weak self] message in + |> deliverOnMainQueue).startStandalone(next: { [weak self] message in guard let strongSelf = self, let message = message else { return } @@ -4572,9 +4604,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, sendMessagesWithSignals: { [weak self] signals, _, _ in if let strongSelf = self { strongSelf.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: strongSelf.context, account: strongSelf.context.account, signals: signals!) - |> deliverOnMainQueue).start(next: { [weak self] messages in + |> deliverOnMainQueue).startStrict(next: { [weak self] messages in if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: messages.map { $0.message }).start() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: messages.map { $0.message }).startStandalone() } })) } @@ -4630,7 +4662,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) { - openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in + let _ = openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { [weak self] tempResolved in guard let strongSelf = self else { @@ -4668,7 +4700,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openPeer(peerId: PeerId, navigation: ChatControllerInteractionNavigateToPeer) { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let self, let peer = peer else { return } @@ -4713,6 +4745,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.resolvePeerByNameDisposable = disposable } var resolveSignal = self.context.engine.peers.resolvePeerByName(name: name, ageLimit: 10) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } var cancelImpl: (() -> Void)? let presentationData = self.presentationData @@ -4769,6 +4807,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var resolveSignal: Signal if let peerName = peerName { resolveSignal = self.context.engine.peers.resolvePeerByName(name: peerName) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> mapToSignal { peer -> Signal in return .single(peer?._asPeer()) } @@ -4840,7 +4884,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro text = presentationData.strings.WebApp_ShortcutsAdded(bot.peer.compactDisplayTitle).string } controller.present( - UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: 5.0), elevatedLayout: false, position: .top, action: { _ in return false }), + UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: 5.0, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return false }), in: .current ) }) @@ -4853,12 +4897,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } if bot.flags.contains(.showInSettingsDisclaimer) { - let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).start() + let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone() } if bot.flags.contains(.notActivated) { let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite) - |> deliverOnMainQueue).start(error: { _ in - + |> deliverOnMainQueue).startStandalone(error: { _ in }, completed: { proceed(true) }) @@ -4895,7 +4938,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .discussion: if let cachedData = self.data?.cachedData as? CachedChannelData, case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: linkedDiscussionPeerId)) - |> deliverOnMainQueue).start(next: { [weak self] linkedDiscussionPeer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] linkedDiscussionPeer in guard let self, let linkedDiscussionPeer else { return } @@ -4925,7 +4968,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let peerId = self.data?.peer?.id ?? self.peerId if !displayCustomNotificationSettings { - let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: self.chatLocation.threadId, muteInterval: 0).start() + let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: self.chatLocation.threadId, muteInterval: 0).startStandalone() let iconColor: UIColor = .white self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ @@ -4971,7 +5014,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, action: { _, f in f(.default) - let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: strongSelf.chatLocation.threadId, muteInterval: value).start() + let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: strongSelf.chatLocation.threadId, muteInterval: value).startStandalone() strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_mute_for", scale: 0.066, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipMutedFor(mutedForTimeIntervalString(strings: strongSelf.presentationData.strings, value: value)).string, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) @@ -5011,7 +5054,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: strongSelf.chatLocation.threadId, sound: .default).start() + let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: strongSelf.chatLocation.threadId, sound: .default).startStandalone() strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_sound_on", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipSoundEnabled, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) @@ -5024,7 +5067,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: strongSelf.chatLocation.threadId, sound: .none).start() + let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: strongSelf.chatLocation.threadId, sound: .none).startStandalone() strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_sound_off", scale: 0.056, colors: [:], title: nil, text: strongSelf.presentationData.strings.PeerInfo_TooltipSoundDisabled, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }))) @@ -5040,7 +5083,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.NotificationSettings.Global() ) - |> deliverOnMainQueue).start(next: { globalSettings in + |> deliverOnMainQueue).startStandalone(next: { globalSettings in guard let strongSelf = self, let peer = strongSelf.data?.peer else { return } @@ -5100,11 +5143,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: EnginePeer(peer), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: globalSettings.privateChats.storySettings.sound, edit: true, updatePeerSound: { peerId, sound in let _ = (updatePeerSound(peer.id, sound) - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).startStandalone(next: { _ in }) }, updatePeerNotificationInterval: { peerId, muteInterval in let _ = (updatePeerNotificationInterval(peerId, muteInterval) - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).startStandalone(next: { _ in guard let strongSelf = self else { return } @@ -5121,18 +5164,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) }, updatePeerDisplayPreviews: { peerId, displayPreviews in let _ = (updatePeerDisplayPreviews(peerId, displayPreviews) - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).startStandalone(next: { _ in }) }, updatePeerStoriesMuted: { peerId, mute in let _ = (updatePeerStoriesMuted(peerId, mute) - |> deliverOnMainQueue).start() + |> deliverOnMainQueue).startStandalone() }, updatePeerStoriesHideSender: { peerId, hideSender in let _ = (updatePeerStoriesHideSender(peerId, hideSender) - |> deliverOnMainQueue).start() + |> deliverOnMainQueue).startStandalone() }, updatePeerStorySound: { peerId, sound in let _ = (updatePeerStorySound(peer.id, sound) - |> deliverOnMainQueue).start() + |> deliverOnMainQueue).startStandalone() }, removePeerFromExceptions: { }, modifiedPeer: { }) @@ -5151,7 +5194,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: self.chatLocation.threadId, muteInterval: 0).start() + let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: self.chatLocation.threadId, muteInterval: 0).startStandalone() let iconColor: UIColor = .white self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ @@ -5172,7 +5215,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: strongSelf.chatLocation.threadId, muteInterval: Int32.max).start() + let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: strongSelf.chatLocation.threadId, muteInterval: Int32.max).startStandalone() let iconColor: UIColor = .white strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ @@ -5388,7 +5431,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) ) ) - |> deliverOnMainQueue).start(next: { [weak self] peerList in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in guard let strongSelf = self else { return } @@ -5474,11 +5517,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self { let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, { current in return current?.withIsEnabled(true) - }).start() + }).startStandalone() Queue.mainQueue().after(0.2, { let _ = (strongSelf.context.engine.messages.togglePeerMessagesTranslationHidden(peerId: strongSelf.peerId, hidden: false) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in self?.openChatForTranslation() }) }) @@ -5611,7 +5654,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } else if let channel = peer as? TelegramChannel { if let cachedData = strongSelf.data?.cachedData as? CachedChannelData { - if channel.hasPermission(.editStories) { + if case .broadcast = channel.info, channel.hasPermission(.editStories) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_Channel_ArchivedStories, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -5638,11 +5681,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self { let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, { current in return current?.withIsEnabled(true) - }).start() + }).startStandalone() Queue.mainQueue().after(0.2, { let _ = (strongSelf.context.engine.messages.togglePeerMessagesTranslationHidden(peerId: strongSelf.peerId, hidden: false) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in self?.openChatForTranslation() }) }) @@ -5664,7 +5707,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, action: { [weak self] c, f in self?.openReport(type: .default, contextController: c, backAction: { c in if let mainItemsImpl = mainItemsImpl { - c.setItems(mainItemsImpl() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) + c.setItems(mainItemsImpl() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) } }) }))) @@ -5895,11 +5938,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self { let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: strongSelf.peerId, { current in return current?.withIsEnabled(true) - }).start() + }).startStandalone() Queue.mainQueue().after(0.2, { let _ = (strongSelf.context.engine.messages.togglePeerMessagesTranslationHidden(peerId: strongSelf.peerId, hidden: false) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in self?.openChatForTranslation() }) }) @@ -6006,7 +6049,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openChatForReporting(_ reason: ReportReason) { if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) { if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { - let _ = self.context.engine.peers.reportPeer(peerId: peer.id, reason: reason, message: "").start() + let _ = self.context.engine.peers.reportPeer(peerId: peer.id, reason: reason, message: "").startStandalone() self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .emoji(name: "PoliceCar", text: self.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) } else { @@ -6037,7 +6080,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let _ = (strongSelf.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: strongSelf.peerId, timeout: value == 0 ? nil : value) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStandalone(completed: { guard let strongSelf = self else { return } @@ -6065,9 +6108,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } if value <= 0 { - let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peer.id, threadId: strongSelf.chatLocation.threadId, muteInterval: nil).start() + let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peer.id, threadId: strongSelf.chatLocation.threadId, muteInterval: nil).startStandalone() } else { - let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peer.id, threadId: strongSelf.chatLocation.threadId, muteInterval: value).start() + let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: peer.id, threadId: strongSelf.chatLocation.threadId, muteInterval: value).startStandalone() let timeString = stringForPreciseRelativeTimestamp(strings: strongSelf.presentationData.strings, relativeTimestamp: Int32(Date().timeIntervalSince1970) + value, relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: strongSelf.presentationData.dateTimeFormat) @@ -6080,7 +6123,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func setAutoremove(timeInterval: Int32?) { let _ = (self.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: self.peerId, timeout: timeInterval) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in guard let strongSelf = self else { return } @@ -6105,7 +6148,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)), self.context.engine.peers.mostRecentSecretChat(id: self.peerId) ) - |> deliverOnMainQueue).start(next: { [weak self] peer, currentPeerId in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, currentPeerId in guard let strongSelf = self else { return } @@ -6271,7 +6314,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) ) ) - |> deliverOnMainQueue).start(next: { [weak self] peerList in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in guard let strongSelf = self else { return } @@ -6480,7 +6523,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = (combineLatest( getUserPeer(engine: self.context.engine, peerId: self.peerId), getUserPeer(engine: self.context.engine, peerId: self.context.account.peerId) - ) |> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer in + ) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, accountPeer in guard let strongSelf = self else { return } @@ -6587,7 +6630,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro ), self.context.engine.peers.notificationSoundList() ) - |> deliverOnMainQueue).start(next: { [weak self] settings, notificationSoundList in + |> deliverOnMainQueue).startStandalone(next: { [weak self] settings, notificationSoundList in guard let strongSelf = self else { return } @@ -6602,7 +6645,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, sound: sound).start() + let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, sound: sound).startStandalone() }) soundController.navigationPresentation = .modal strongSelf.controller?.push(soundController) @@ -6610,7 +6653,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, muteInterval: value).start() + let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, muteInterval: value).startStandalone() }) strongSelf.view.endEditing(true) strongSelf.controller?.present(muteSettingsController, in: .window(.root)) @@ -6622,7 +6665,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: self.peerId), TelegramEngine.EngineData.Item.NotificationSettings.Global() ) - |> deliverOnMainQueue).start(next: { [weak self] peerSettings, globalSettings in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerSettings, globalSettings in guard let strongSelf = self else { return } @@ -6631,7 +6674,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, sound: sound).start() + let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, threadId: strongSelf.chatLocation.threadId, sound: sound).startStandalone() }) strongSelf.controller?.push(soundController) }) @@ -6639,11 +6682,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func editingToggleShowMessageText(value: Bool) { let _ = (getUserPeer(engine: self.context.engine, peerId: self.peerId) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self, let peer = peer else { return } - let _ = strongSelf.context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peer.id, threadId: strongSelf.chatLocation.threadId, displayPreviews: value ? .show : .hide).start() + let _ = strongSelf.context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peer.id, threadId: strongSelf.chatLocation.threadId, displayPreviews: value ? .show : .hide).startStandalone() }) } @@ -6660,7 +6703,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } let _ = (getUserPeer(engine: strongSelf.context.engine, peerId: strongSelf.peerId) - |> deliverOnMainQueue).start(next: { peer in + |> deliverOnMainQueue).startStandalone(next: { peer in guard let peer = peer, let strongSelf = self else { return } @@ -6699,10 +6742,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.activeActionDisposable.set((deleteSignal - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStrict(completed: { [weak self] in if let strongSelf = self, let peer = strongSelf.data?.peer { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) + let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) controller.keepOnParentDismissal = true strongSelf.controller?.present(controller, in: .window(.root)) @@ -6761,7 +6804,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openAddContact() { let _ = (getUserPeer(engine: self.context.engine, peerId: self.peerId) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self, let peer = peer else { return } @@ -6780,16 +6823,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func updateBlocked(block: Bool) { let _ = (getUserPeer(engine: self.context.engine, peerId: self.peerId) |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self, let peer = peer else { return } let presentationData = strongSelf.presentationData if case let .user(peer) = peer, let _ = peer.botInfo { - strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block).start()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block).startStrict()) if !block { - let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() if let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)))) } @@ -6812,12 +6855,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).start()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).startStrict()) if deleteChat { - let _ = strongSelf.context.engine.peers.removePeerChat(peerId: strongSelf.peerId, reportChatSpam: reportSpam).start() + let _ = strongSelf.context.engine.peers.removePeerChat(peerId: strongSelf.peerId, reportChatSpam: reportSpam).startStandalone() (strongSelf.controller?.navigationController as? NavigationController)?.popToRoot(animated: true) } else if reportSpam { - let _ = strongSelf.context.engine.peers.reportPeer(peerId: strongSelf.peerId, reason: .spam, message: "").start() + let _ = strongSelf.context.engine.peers.reportPeer(peerId: strongSelf.peerId, reason: .spam, message: "").startStandalone() } deleteSendMessageIntents(peerId: strongSelf.peerId) @@ -6838,7 +6881,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block).start()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block).startStrict()) })]), in: .window(.root)) } } @@ -6885,7 +6928,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var result = currentAccountPeer result.append(contentsOf: availablePeers) return result - }).start(next: { [weak self] peers in + }).startStandalone(next: { [weak self] peers in guard let strongSelf = self else { return } @@ -6907,7 +6950,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in - let _ = context.engine.calls.updateGroupCallJoinAsPeer(peerId: peerId, joinAs: joinAsPeerId).start() + let _ = context.engine.calls.updateGroupCallJoinAsPeer(peerId: peerId, joinAs: joinAsPeerId).startStandalone() self?.openVoiceChatOptions(defaultJoinAsPeerId: joinAsPeerId, gesture: nil, contextController: c) }, gesture: gesture, contextController: c, result: f, backAction: { [weak self] c in self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: nil, contextController: c) @@ -6966,7 +7009,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if let contextController = contextController { - contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil, animated: true) } else { strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) if let (layout, navigationHeight) = strongSelf.validLayout { @@ -7000,7 +7043,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var result = currentAccountPeer result.append(contentsOf: availablePeers) return result - }).start(next: { [weak self] peers in + }).startStandalone(next: { [weak self] peers in guard let strongSelf = self else { return } @@ -7062,7 +7105,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if let contextController = contextController { - contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil, animated: true) } else { strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) if let (layout, navigationHeight) = strongSelf.validLayout { @@ -7095,7 +7138,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro switch type { case let .reaction(sourceMessageId): let _ = (self.context.engine.peers.reportPeerReaction(authorId: self.peerId, messageId: sourceMessageId) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in guard let strongSelf = self else { return } @@ -7128,7 +7171,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openShareBot() { let _ = (getUserPeer(engine: self.context.engine, peerId: self.peerId) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self else { return } @@ -7143,7 +7186,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) ) ) - |> deliverOnMainQueue).start(next: { [weak self] peerList in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in guard let strongSelf = self else { return } @@ -7205,7 +7248,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func performBotCommand(command: PeerInfoBotCommand) { let _ = (self.context.account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self else { return } @@ -7218,7 +7261,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .help: text = "/help" } - let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() if let peer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)))) @@ -7246,6 +7289,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } + private func editingOpenNameColorSetup() { + let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .channel(self.peerId)) + self.controller?.push(controller) + } + private func editingOpenInviteLinksSetup() { self.controller?.push(inviteLinkListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, admin: nil)) } @@ -7294,7 +7342,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } |> deliverOnMainQueue - let _ = signal.start(next: { [weak self] resultPeerId in + let _ = signal.startStandalone(next: { [weak self] resultPeerId in guard let self else { return } @@ -7303,7 +7351,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let _ = (self.context.engine.peers.setChannelForumMode(id: resultPeerId, isForum: isEnabled) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in guard let self, let controller = self.controller else { return } @@ -7315,12 +7363,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } } else { - let _ = self.context.engine.peers.setChannelForumMode(id: self.peerId, isForum: isEnabled).start() + let _ = self.context.engine.peers.setChannelForumMode(id: self.peerId, isForum: isEnabled).startStandalone() } } private func editingToggleMessageSignatures(value: Bool) { - self.toggleShouldChannelMessagesSignaturesDisposable.set(self.context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: self.peerId, enabled: value).start()) + self.toggleShouldChannelMessagesSignaturesDisposable.set(self.context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: self.peerId, enabled: value).startStrict()) } private func openParticipantsSection(section: PeerInfoParticipantsSection) { @@ -7441,7 +7489,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro |> mapToSignal { address -> Signal in return updateChannelGeoLocation(postbox: context.account.postbox, network: context.account.network, channelId: peer.id, coordinate: (location.latitude, location.longitude), address: address) } - |> deliverOnMainQueue).start() + |> deliverOnMainQueue).startStandalone() }) self.controller?.push(controller) } @@ -7592,7 +7640,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] sharedData in + |> deliverOnMainQueue).startStandalone(next: { [weak self] sharedData in let translationSettings: TranslationSettings if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { translationSettings = current @@ -7622,7 +7670,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })) } - let contextMenuController = ContextMenuController(actions: actions) + let contextMenuController = makeContextMenuController(actions: actions) controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in if let controller = self?.controller, let sourceNode = sourceNode { var rect = sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0) @@ -7637,7 +7685,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } case let .phone(phone): - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in + let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in UIPasteboard.general.string = phone let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -7673,7 +7721,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro content = .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied) } - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in + let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in UIPasteboard.general.string = text let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -7691,7 +7739,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } })) case let .ngId(peerId): - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = peerId })]) controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in @@ -7702,7 +7750,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } })) case let .regDate(registeredString): - let contextMenuController = ContextMenuController(actions: [ + let contextMenuController = makeContextMenuController(actions: [ ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = registeredString }), @@ -7735,7 +7783,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openDeletePeer() { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self, let peer = peer else { return } @@ -7780,7 +7828,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func openLeavePeer(delete: Bool) { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self, let peer = peer else { return } @@ -7935,7 +7983,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } self.updateAvatarDisposable.set((signal - |> deliverOnMainQueue).start(next: { [weak self] result in + |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let strongSelf = self else { return } @@ -7953,7 +8001,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro dismissStatus?() let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in if let strongSelf = self, let peer { switch mode { case .fallback: @@ -7961,7 +8009,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .custom: strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) - let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start() + let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() case .suggest: if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in @@ -8171,7 +8219,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } } - |> deliverOnMainQueue).start(next: { [weak self] result in + |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let strongSelf = self else { return } @@ -8189,7 +8237,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro dismissStatus?() let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in if let strongSelf = self, let peer { switch mode { case .fallback: @@ -8197,7 +8245,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .custom: strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) - let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start() + let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() case .suggest: if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in @@ -8245,7 +8293,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Configuration.SearchBots() ) - |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, searchBotsConfiguration in guard let strongSelf = self, let peer = peer else { return } @@ -8449,7 +8497,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } strongSelf.updateAvatarDisposable.set((signal - |> deliverOnMainQueue).start(next: { result in + |> deliverOnMainQueue).startStrict(next: { result in guard let strongSelf = self else { return } @@ -8521,7 +8569,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro #endif self.postingAvailabilityDisposable = (canPostStatus - |> deliverOnMainQueue).start(next: { [weak self] status in + |> deliverOnMainQueue).startStrict(next: { [weak self] status in guard let self else { return } @@ -8543,28 +8591,24 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .channelBoostRequired: self.postingAvailabilityDisposable?.dispose() + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + self.postingAvailabilityDisposable = combineLatest( queue: Queue.mainQueue(), self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)), self.context.engine.peers.getChannelBoostStatus(peerId: self.peerId) - ).start(next: { [weak self] peer, status in + ).startStrict(next: { [weak self] peer, status in guard let self, let peer, let status else { return } - let link: String - if let addressName = peer.addressName, !addressName.isEmpty { - link = "t.me/\(peer.addressName ?? "")?boost" - } else { - link = "t.me/c/\(peer.id.id._internalGetInt64Value())?boost" - } - + let link = status.url if let navigationController = self.controller?.navigationController as? NavigationController { if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen { previousController.dismiss() } - let controller = PremiumLimitScreen(context: self.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, boosted: false), count: Int32(status.boosts), action: { [weak self] in - UIPasteboard.general.string = "https://\(link)" + let controller = PremiumLimitScreen(context: self.context, subject: .storiesChannelBoost(peer: peer, boostSubject: .stories, 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: { [weak self] in + UIPasteboard.general.string = link if let self { self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .linkCopied(text: self.presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) @@ -8574,7 +8618,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let self { self.openStats(boosts: true, boostStatus: status) } - }) + }, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { [weak self] in + if let self { + let controller = createGiveawayController(context: self.context, peerId: self.peerId, subject: .generic) + self.controller?.push(controller) + } + } : nil) navigationController.pushViewController(controller) } @@ -8637,7 +8686,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro 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).start(next: { [weak self] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let self, let peer = peer else { return } @@ -8650,7 +8699,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .devices: let _ = (self.activeSessionsContextAndCount.get() |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] activeSessionsContextAndCount in + |> 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)) @@ -8666,7 +8715,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let settings = self.data?.globalSettings { let _ = (combineLatest(self.blockedPeers.get(), self.hasTwoStepAuth.get()) |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] blockedPeersContext, hasTwoStepAuth in + |> deliverOnMainQueue).startStandalone(next: { [weak self] blockedPeersContext, hasTwoStepAuth in if let strongSelf = self { let loginEmailPattern = strongSelf.twoStepAuthData.get() |> map { data -> String? in return data?.loginEmailPattern @@ -8704,7 +8753,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start() + let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).startStandalone() }) let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context) @@ -8742,7 +8791,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro 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 - self?.supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in + guard let self else { + return + } + self.supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peerId in if let strongSelf = self, let peerId = peerId { push(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) } @@ -8767,7 +8819,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = (activeAccountsAndPeers(context: context) |> take(1) |> deliverOnMainQueue - ).start(next: { [weak self] accountAndPeer, accountsAndPeers in + ).startStandalone(next: { [weak self] accountAndPeer, accountsAndPeers in guard let strongSelf = self else { return } @@ -8832,7 +8884,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro 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).start() + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePassword).startStandalone() } push(controller) case .emojiStatus: @@ -8895,7 +8947,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let context = self.context let navigationController = self.controller?.navigationController as? NavigationController - self.tipsPeerDisposable.set((self.context.engine.peers.resolvePeerByName(name: self.presentationData.strings.Settings_TipsUsername) |> deliverOnMainQueue).start(next: { [weak controller] peer in + self.tipsPeerDisposable.set((self.context.engine.peers.resolvePeerByName(name: self.presentationData.strings.Settings_TipsUsername) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).startStrict(next: { [weak controller] peer in controller?.dismiss() if let peer = peer, let navigationController = navigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) @@ -8919,7 +8978,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro items.append(ActionSheetButtonItem(title: self.presentationData.strings.Settings_Logout, color: .destructive, action: { [weak self] in dismissAction() if let strongSelf = self { - let _ = logoutFromAccount(id: id, accountManager: strongSelf.context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).start() + let _ = logoutFromAccount(id: id, accountManager: strongSelf.context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).startStandalone() } })) controller.setItemGroups([ @@ -8938,7 +8997,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if !unreadChatListPeerIds.isEmpty { items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAllAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in let _ = (context.engine.messages.markAllChatsAsReadInteractively(items: [(groupId: .root, filterPredicate: nil)]) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStandalone(completed: { f(.default) }) }))) @@ -8957,7 +9016,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var selectedAccount: Account? let _ = (self.accountsAndPeers.get() |> take(1) - |> deliverOnMainQueue).start(next: { accountsAndPeers in + |> deliverOnMainQueue).startStandalone(next: { accountsAndPeers in for (account, _, _) in accountsAndPeers { if account.account.id == id { selectedAccount = account.account @@ -8988,7 +9047,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func deleteMessages(messageIds: Set?) { if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(engine: self.context.engine, accountPeerId: self.context.account.peerId, messageIds: messageIds) - |> deliverOnMainQueue).start(next: { [weak self] actions in + |> deliverOnMainQueue).startStrict(next: { [weak self] actions in if let strongSelf = self, let peer = strongSelf.data?.peer, !actions.options.isEmpty { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] @@ -9013,7 +9072,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } })) } @@ -9030,7 +9089,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).startStandalone() } })) } @@ -9065,7 +9124,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } } } @@ -9080,7 +9139,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var displayPeers: [EnginePeer] = [] for peer in peers { let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: result) - |> deliverOnMainQueue).start(next: { messageIds in + |> deliverOnMainQueue).startStandalone(next: { messageIds in if let strongSelf = self { let signals: [Signal] = messageIds.compactMap({ id -> Signal? in guard let id = id else { @@ -9100,7 +9159,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.shareStatusDisposable = MetaDisposable() } strongSelf.shareStatusDisposable?.set((combineLatest(signals) - |> deliverOnMainQueue).start()) + |> deliverOnMainQueue).startStrict()) } }) @@ -9158,7 +9217,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in return .forward(source: id, threadId: nil, grouping: .auto, attributes: [], correlationId: nil) }) - |> deliverOnMainQueue).start(next: { [weak self] messageIds in + |> deliverOnMainQueue).startStandalone(next: { [weak self] messageIds in if let strongSelf = self { let signals: [Signal] = messageIds.compactMap({ id -> Signal? in guard let id = id else { @@ -9175,7 +9234,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro |> take(1) }) strongSelf.activeActionDisposable.set((combineLatest(signals) - |> deliverOnMainQueue).start()) + |> deliverOnMainQueue).startStrict()) } }) if let peerSelectionController = peerSelectionController { @@ -9185,7 +9244,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in return currentState.withUpdatedForwardMessageIds(Array(messageIds)) }) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).startStandalone(completed: { if let strongSelf = self { let proceed: (ChatController) -> Void = { chatController in strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil) @@ -9202,7 +9261,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.activeActionDisposable.set((chatController.ready.get() |> filter { $0 } |> take(1) - |> deliverOnMainQueue).start(next: { [weak navigationController] _ in + |> deliverOnMainQueue).startStrict(next: { [weak navigationController] _ in viewControllers.removeAll(where: { $0 is PeerSelectionController }) navigationController?.setViewControllers(viewControllers, animated: true) })) @@ -9211,7 +9270,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let threadId = threadId { let _ = (strongSelf.context.sharedContext.chatControllerForForumThread(context: strongSelf.context, peerId: peerId, threadId: threadId) - |> deliverOnMainQueue).start(next: { chatController in + |> deliverOnMainQueue).startStandalone(next: { chatController in proceed(chatController) }) } else { @@ -9345,7 +9404,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .photo), TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: self.chatLocation.threadId, tag: .video) ])) - |> deliverOnMainQueue).start(next: { [weak self] messageCounts in + |> deliverOnMainQueue).startStandalone(next: { [weak self] messageCounts in guard let strongSelf = self else { return } @@ -9571,7 +9630,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro chatController: nil, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), - subject: .message(id: .id(index.id), highlight: false, timecode: nil), + subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, updateTextInputState: nil, keepStack: .never, @@ -9592,7 +9651,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro )) }))) - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) @@ -9627,7 +9686,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case let .remove(positionInList): strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string : presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: false, animateInAsReplacement: false, action: { action in if case .undo = action { - let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).startStandalone() } return true })) @@ -9660,7 +9719,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in mapResourceToAvatarSizes(postbox: strongSelf.context.account.postbox, resource: resource, representations: representations) }) - |> deliverOnMainQueue).start(next: { [weak self] _ in + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in guard let strongSelf = self else { return } @@ -9888,7 +9947,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = (strongSelf.context.engine.data.get(EngineDataMap( messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) )) - |> deliverOnMainQueue).start(next: { messageMap in + |> deliverOnMainQueue).startStandalone(next: { messageMap in let messages = messageMap.values.compactMap { $0 } if let strongSelf = self, !messages.isEmpty { @@ -9921,7 +9980,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = (strongSelf.context.engine.data.get(EngineDataMap( messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) )) - |> deliverOnMainQueue).start(next: { [weak self] messageMap in + |> deliverOnMainQueue).startStandalone(next: { [weak self] messageMap in guard let strongSelf = self else { return } @@ -10460,7 +10519,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.accountsAndPeers.set(activeAccountsAndPeers(context: context)) self.accountsAndPeersDisposable = (self.accountsAndPeers.get() - |> deliverOnMainQueue).start(next: { [weak self] value in + |> deliverOnMainQueue).startStrict(next: { [weak self] value in self?.accountsAndPeersValue = value }) @@ -10522,7 +10581,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc context.clear(CGRect(origin: CGPoint(), size: size)) context.translateBy(x: inset, y: inset) - drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: displayLetters, peerId: primary.1.id) + drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: displayLetters, peerId: primary.1.id, nameColor: primary.1.nameColor) })?.withRenderingMode(.alwaysOriginal) if let image = image { subscriber.putNext((image, image)) @@ -10596,7 +10655,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning || phoneNumberWarning || passwordWarning ? "!" : otherAccountsBadge, accountTabBarAvatar != nil, presentationData.reduceMotion) } - self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] title, image, selectedImage, badgeValue, isAvatar, reduceMotion in + self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).startStrict(next: { [weak self] title, image, selectedImage, badgeValue, isAvatar, reduceMotion in if let strongSelf = self { strongSelf.tabBarItem.title = title strongSelf.tabBarItem.image = image @@ -10692,7 +10751,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } self.presentationDataDisposable = (presentationDataSignal - |> deliverOnMainQueue).start(next: { [weak self] presentationData in + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings @@ -10825,7 +10884,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc peerMap, threadDataMap ) - |> deliverOnMainQueue).start(next: { [weak parentController, weak backButtonView, weak navigationController] peerMap, threadDataMap in + |> deliverOnMainQueue).startStandalone(next: { [weak parentController, weak backButtonView, weak navigationController] peerMap, threadDataMap in guard let parentController, let backButtonView else { return } @@ -11512,7 +11571,7 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in let _ = (members.get() |> take(1) - |> deliverOnMainQueue).start(next: { [weak parentController] recentIds in + |> deliverOnMainQueue).startStandalone(next: { [weak parentController] recentIds in var createInviteLinkImpl: (() -> Void)? var confirmationImpl: ((PeerId) -> Signal)? let _ = confirmationImpl @@ -11846,7 +11905,7 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in } if !mappedPeerIds.isEmpty { let _ = (context.engine.data.get(EngineDataMap(mappedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))) - |> deliverOnMainQueue).start(next: { maybePeers in + |> deliverOnMainQueue).startStandalone(next: { maybePeers in let presentationData = context.sharedContext.currentPresentationData.with { $0 } let peers = maybePeers.compactMap { $0.value } @@ -11866,7 +11925,7 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in return item.0 }.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ) - |> deliverOnMainQueue).start(next: { peerItems in + |> deliverOnMainQueue).startStandalone(next: { peerItems in let peers = peerItems.compactMap { $0 } if !peers.isEmpty, let contactsController, let navigationController = contactsController.navigationController as? NavigationController { var viewControllers = navigationController.viewControllers @@ -11888,7 +11947,7 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in switch peers[0] { case let .peer(peerId): let _ = (context.account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in + |> deliverOnMainQueue).startStandalone(next: { peer in parentController?.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) default: diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index 32e0c379fcd..ebbe1b6164d 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -7,6 +7,7 @@ import ItemListUI import Display import ItemListPeerItem import ItemListPeerActionItem +import TextFormat private let collapsedResultCount: Int = 10 private let collapsedInitialLimit: Int = 10 @@ -250,7 +251,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po displayCount = Int(voterCount) } for peerIndex in 0 ..< displayCount { - let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) + let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil) let peer = EngineRenderedPeer(peer: EnginePeer(fakeUser)) entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0)) } @@ -425,4 +426,3 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess return controller } - diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index c7b5829e0c6..0fd47eb4482 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -7,6 +7,8 @@ import PhotoResources import StickerResources import Emoji import UniversalMediaPlayer +import ChatMessageInteractiveMediaNode +import ChatMessageAnimatedStickerItemNode private final class PrefetchMediaContext { let fetchDisposable = MetaDisposable() diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index d516efebf7b..21e2e19c5c6 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -6,8 +6,10 @@ import Display import MergeLists import AccountContext import ChatControllerInteraction +import ChatHistoryEntry +import ChatMessageBubbleItemNode -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, scrollAnimationCurve: ListViewAnimationCurve?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool, updatedMessageSelection: Bool, messageTransitionNode: ChatMessageTransitionNode?, allUpdated: Bool) -> ChatHistoryViewTransition { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, scrollAnimationCurve: ListViewAnimationCurve?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool, updatedMessageSelection: Bool, messageTransitionNode: ChatMessageTransitionNodeImpl?, allUpdated: Bool) -> ChatHistoryViewTransition { var mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)]) let allUpdated = allUpdated || (fromView?.associatedData != toView.associatedData) if reverse { @@ -134,7 +136,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie adjustedUpdateItems.append(ChatHistoryViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint)) } - var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToIndex: MessageHistoryScrollToSubject? var scrolledToSomeIndex = false let curve: ListViewAnimationCurve = scrollAnimationCurve ?? .Default(duration: nil) @@ -192,13 +194,25 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie index += 1 } } - case let .index(scrollIndex, position, directionHint, animated, highlight, displayLink): + case let .index(scrollSubject, position, directionHint, animated, highlight, displayLink): + let scrollIndex = scrollSubject + var position = position if case .center = position, highlight { - scrolledToIndex = scrollIndex + scrolledToIndex = scrollSubject + } + if case .center = position, let quote = scrollSubject.quote { + position = .center(.custom({ itemNode in + if let itemNode = itemNode as? ChatMessageBubbleItemNode { + if let quoteRect = itemNode.getQuoteRect(quote: quote) { + return quoteRect.midY + } + } + return 0.0 + })) } var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { - if scrollIndex.isLessOrEqual(to: entry.index) { + if scrollIndex.index.isLessOrEqual(to: entry.index) { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint, displayLink: displayLink) break } @@ -208,7 +222,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie if scrollToItem == nil { var index = 0 for entry in toView.filteredEntries.reversed() { - if !scrollIndex.isLess(than: entry.index) { + if !scrollIndex.index.isLess(than: entry.index) { scrolledToSomeIndex = true scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint) break diff --git a/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift b/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift index 0dcdecc3766..400e6bd8e37 100644 --- a/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift @@ -7,6 +7,7 @@ import Postbox import SwiftSignalKit import LocalizedPeerData import ChatPresentationInterfaceState +import ChatInputPanelNode final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { private let button: HighlightableButtonNode diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index c2065363682..4534f19b2ce 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -43,6 +43,11 @@ import PeerInfoStoryGridScreen import TelegramAccountAuxiliaryMethods import PeerSelectionController import LegacyMessageInputPanel +import StatisticsUI +import ChatHistoryEntry +import ChatMessageItem +import ChatMessageItemImpl +import ChatRecentActionsController import NGCore import NGData @@ -220,7 +225,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let energyUsageAutomaticDisposable = MetaDisposable() // MARK: Nicegram DB Changes, openDoubleBottomFlow added - init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, openDoubleBottomFlow: @escaping (AccountContext) -> Void, appDelegate: AppDelegate?) { + init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, openDoubleBottomFlow: @escaping (AccountContext) -> Void = { _ in }, appDelegate: AppDelegate?) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -1507,7 +1512,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { if !found { let controllerParams = LocationViewParams(sendLiveLocation: { location in - //let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil) + //let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil) // params.enqueueMessage(outMessage) }, stopLiveLocation: { messageId in if let messageId = messageId { @@ -1531,6 +1536,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .complete() + case let .result(value): + return .single(value) + } + } + } + + public func resolveUrlWithProgress(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { + return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) } public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { @@ -1609,9 +1626,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _, _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in - }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { message in tapMessage?(message) @@ -1619,7 +1636,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { clickThroughMessage?() }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false - }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { @@ -1629,6 +1646,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -1641,7 +1660,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in @@ -1672,11 +1691,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, activateAdAction: { _ in }, openRequestedPeerSelection: { _, _, _ in }, saveMediaToFiles: { _ in + }, openNoAdsDemo: { + }, displayGiveawayParticipationStatus: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) @@ -1693,7 +1715,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { chatLocation = .peer(id: messages.first!.id.peerId) } - return ChatMessageItem(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, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false, accountPeer: nil, forceInlineReactions: true), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) + return ChatMessageItemImpl(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, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false, accountPeer: nil, forceInlineReactions: true), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { @@ -1732,6 +1754,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } + public func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { + openChatInstantPageImpl(context: context, message: message, sourcePeerType: sourcePeerType, navigationController: navigationController) + } + + public func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { + openChatWallpaperImpl(context: context, message: message, present: present) + } + public func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController { return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: context.engine.privacy.webSessions(), websitesOnly: false) } @@ -1916,8 +1946,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .storiesWeekly case .storiesMonthly: mappedSubject = .storiesMonthly - case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, boosted): - mappedSubject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: level, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: link, boosted: boosted) + case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, myBoostCount, canBoostAgain): + mappedSubject = .storiesChannelBoost(peer: peer, boostSubject: .stories, isCurrent: isCurrent, level: level, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: link, myBoostCount: myBoostCount, canBoostAgain: canBoostAgain) } return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } @@ -1941,6 +1971,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController { return installedStickerPacksController(context: context, mode: mode, forceTheme: forceTheme) } + + public func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?, statsDatacenterId: Int32) -> ViewController { + return channelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, section: boosts ? .boosts : .stats, boostStatus: nil, statsDatacenterId: statsDatacenterId) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 1fab0fa87b9..a2d9cd69419 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -51,7 +51,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh init(context: AccountContext) { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: true) self.emptyNode = ChatEmptyNode(context: context, interaction: nil) @@ -64,7 +64,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.presentationInterfaceState.limitsConfiguration, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.presentationInterfaceState.accountPeerId, mode: .standard(previewing: false), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.presentationInterfaceState.limitsConfiguration, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.presentationInterfaceState.accountPeerId, mode: .standard(previewing: false), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.wallpaperBackgroundNode.update(wallpaper: presentationData.chatWallpaper) } @@ -451,7 +451,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon var showDraftTooltipImpl: (() -> Void)? let cameraController = CameraScreen( context: context, - mode: .story, transitionIn: transitionIn.flatMap { if let sourceView = $0.sourceView { return CameraScreen.TransitionIn( @@ -624,7 +623,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let _ = self.chatListController as? ChatListControllerImpl { switch mediaResult { case let .image(image, dimensions): - if let imageData = compressImageToJPEG(image, quality: 0.7) { + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) { let entities = generateChatInputTextEntities(caption) Logger.shared.log("MediaEditor", "Calling uploadStory for image, randomId \(randomId)") let _ = (context.engine.messages.uploadStory(target: target, media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId) @@ -650,7 +653,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case let .asset(localIdentifier): resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } - let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) } let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in let file = TempBox.shared.tempFile(fileName: "image.jpg") if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 557ea1b5787..4cf6aa14ddd 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -69,7 +69,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n openResolvedPeerImpl(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) case let .channelMessage(peer, messageId, timecode): if let navigationController = controller.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: true, timecode: timecode))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode))) } case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = controller.navigationController as? NavigationController { @@ -90,9 +90,11 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in openResolvedPeerImpl(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) }, parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) - case .boost: + 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: { _, _ in }, sendFile: nil, sendSticker: nil, joinVoiceChat: nil, present: { c, a in }, dismissInput: {}, contentContext: nil) + openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, 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) } default: break @@ -102,7 +104,14 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n } let openPeerMentionImpl: (String) -> Void = { mention in - navigateDisposable.set((context.engine.peers.resolvePeerByName(name: mention, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peer in + navigateDisposable.set((context.engine.peers.resolvePeerByName(name: mention, ageLimit: 10) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { peer in openResolvedPeerImpl(peer, .default) })) } diff --git a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift index e9cf655b0fe..2d0679a6384 100644 --- a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift +++ b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift @@ -147,7 +147,11 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me if data.complete { if let smallest = smallestImageRepresentation(image.representations), smallest.dimensions.width > 100 || smallest.dimensions.height > 100 { let smallestSize = smallest.dimensions.cgSize.fitted(CGSize(width: 320.0, height: 320.0)) - if let fullImage = UIImage(contentsOfFile: data.path), let smallestImage = generateScaledImage(image: fullImage, size: smallestSize, scale: 1.0), let smallestData = compressImageToJPEG(smallestImage, quality: 0.7) { + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + if let fullImage = UIImage(contentsOfFile: data.path), let smallestImage = generateScaledImage(image: fullImage, size: smallestSize, scale: 1.0), let smallestData = compressImageToJPEG(smallestImage, quality: 0.7, tempFilePath: tempFile.path) { var representations = image.representations let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift index 9e8a8f9da66..d26978cc54c 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift @@ -10,6 +10,7 @@ import AccountContext import SwiftSignalKit import ChatPresentationInterfaceState import ChatControllerInteraction +import ChatInputContextPanelNode private enum VerticalChatContextResultsEntryStableId: Hashable { case action diff --git a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift index ba7fb6ee99d..630b6b118a6 100644 --- a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift @@ -9,6 +9,7 @@ import AccountContext import TelegramStringFormatting import ChatPresentationInterfaceState import AccessoryPanelNode +import AppBundle final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { private let webpageDisposable = MetaDisposable() @@ -18,7 +19,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { let closeButton: HighlightableButtonNode let lineNode: ASImageNode - let iconNode: ASImageNode + let iconView: UIImageView let titleNode: TextNode private var titleString: NSAttributedString? @@ -46,10 +47,9 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.lineNode.displaysAsynchronously = false self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) - self.iconNode = ASImageNode() - self.iconNode.displayWithoutProcessing = false - self.iconNode.displaysAsynchronously = false - self.iconNode.image = PresentationResourcesChat.chatInputPanelWebpageIconImage(theme) + self.iconView = UIImageView() + self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/LinkSettingsIcon")?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor self.titleNode = TextNode() self.titleNode.displaysAsynchronously = false @@ -63,7 +63,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.closeButton) self.addSubnode(self.lineNode) - self.addSubnode(self.iconNode) + self.view.addSubview(self.iconView) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) @@ -75,15 +75,25 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { } override func animateIn() { - self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } override func animateOut() { - self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + self.iconView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + } + + override public func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - if self.theme !== theme || self.strings !== strings { + self.updateThemeAndStrings(theme: theme, strings: strings, force: false) + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, force: Bool) { + if self.theme !== theme || self.strings !== strings || force { self.strings = strings if self.theme !== theme { @@ -91,7 +101,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) - self.iconNode.image = PresentationResourcesChat.chatInputPanelWebpageIconImage(theme) + self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor } if let text = self.titleString?.string { @@ -184,8 +194,8 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) - if let icon = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) + if let icon = self.iconView.image { + self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) } let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -209,4 +219,21 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { dismiss() } } + + private var previousTapTimestamp: Double? + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() + self.interfaceInteraction?.presentLinkOptions(self) + Queue.mainQueue().after(1.5) { + self.updateThemeAndStrings(theme: self.theme, strings: self.strings, force: true) + } + + //let _ = ApplicationSpecificNotice.incrementChatReplyOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start() + } + } } diff --git a/submodules/TelegramUI/Sources/module.private.modulemap b/submodules/TelegramUI/Sources/module.private.modulemap deleted file mode 100644 index 0c4e7fe618b..00000000000 --- a/submodules/TelegramUI/Sources/module.private.modulemap +++ /dev/null @@ -1,3 +0,0 @@ -module TelegramUIPrivate { - export * -} diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 8537571aed9..c1bc10c8d06 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -53,6 +53,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var storiesExperiment: Bool public var storiesJpegExperiment: Bool public var crashOnMemoryPressure: Bool + public var unidirectionalSwipeToReply: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -84,7 +85,8 @@ public struct ExperimentalUISettings: Codable, Equatable { logLanguageRecognition: false, storiesExperiment: false, storiesJpegExperiment: false, - crashOnMemoryPressure: false + crashOnMemoryPressure: false, + unidirectionalSwipeToReply: false ) } @@ -116,7 +118,8 @@ public struct ExperimentalUISettings: Codable, Equatable { logLanguageRecognition: Bool, storiesExperiment: Bool, storiesJpegExperiment: Bool, - crashOnMemoryPressure: Bool + crashOnMemoryPressure: Bool, + unidirectionalSwipeToReply: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -146,6 +149,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesExperiment = storiesExperiment self.storiesJpegExperiment = storiesJpegExperiment self.crashOnMemoryPressure = crashOnMemoryPressure + self.unidirectionalSwipeToReply = unidirectionalSwipeToReply } public init(from decoder: Decoder) throws { @@ -179,6 +183,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesExperiment") ?? false 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 } public func encode(to encoder: Encoder) throws { @@ -212,6 +217,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.storiesExperiment, forKey: "storiesExperiment") try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment") try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure") + try container.encode(self.unidirectionalSwipeToReply, forKey: "unidirectionalSwipeToReply") } } diff --git a/submodules/TelegramVoip/Package.swift b/submodules/TelegramVoip/Package.swift index 1eeb60ca4bf..47993729386 100644 --- a/submodules/TelegramVoip/Package.swift +++ b/submodules/TelegramVoip/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "TelegramVoip", - platforms: [.macOS(.v10_12)], + 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/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 3a43be9c1b3..3569cf77085 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -46,7 +46,7 @@ final class NetworkBroadcastPartSource: BroadcastPartSource { private var dataSource: AudioBroadcastDataSource? #if DEBUG - private let debugDumpDirectory = EngineTempBox.shared.tempDirectory() + private let debugDumpDirectory: EngineTempBox.Directory? #endif init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, isExternalStream: Bool) { @@ -55,6 +55,10 @@ final class NetworkBroadcastPartSource: BroadcastPartSource { self.callId = callId self.accessHash = accessHash self.isExternalStream = isExternalStream + + #if DEBUG && true + self.debugDumpDirectory = EngineTempBox.shared.tempDirectory() + #endif } func requestTime(completion: @escaping (Int64) -> Void) -> Disposable { @@ -143,9 +147,10 @@ final class NetworkBroadcastPartSource: BroadcastPartSource { } |> deliverOn(self.queue) - /*#if DEBUG + #if DEBUG let debugDumpDirectory = self.debugDumpDirectory - #endif*/ + #endif + return signal.start(next: { result in guard let result = result else { completion(OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: Double(timestampIdMilliseconds), status: .notReady, oggData: Data())) @@ -154,11 +159,13 @@ final class NetworkBroadcastPartSource: BroadcastPartSource { let part: OngoingGroupCallBroadcastPart switch result.status { case let .data(dataValue): - /*#if DEBUG - let tempFilePath = debugDumpDirectory.path + "/\(timestampMilliseconds).mp4" - let _ = try? dataValue.subdata(in: 32 ..< dataValue.count).write(to: URL(fileURLWithPath: tempFilePath)) - print("Dump stream part: \(tempFilePath)") - #endif*/ + #if DEBUG + if let debugDumpDirectory = debugDumpDirectory { + let tempFilePath = debugDumpDirectory.path + "/\(timestampMilliseconds).mp4" + let _ = try? dataValue.subdata(in: 32 ..< dataValue.count).write(to: URL(fileURLWithPath: tempFilePath)) + print("Dump stream part: \(tempFilePath)") + } + #endif part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .success, oggData: dataValue) case .notReady: part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .notReady, oggData: Data()) diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 78ec00f3eb5..5ffcb239864 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -163,7 +163,7 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { self.profileDataPreloadContexts[peerId] = context if let customData = customData { - disposable.add(customData.start()) + disposable.add(customData.startStrict()) } /*disposable.set(signal.start(next: { [weak context] value in @@ -195,7 +195,7 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { current.subscribers.remove(index) if current.subscribers.isEmpty { if current.emptyTimer == nil { - let timer = SwiftSignalKit.Timer(timeout: 60.0, repeat: false, completion: { [weak context] in + let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak context] in if let current = strongSelf.profileDataPreloadContexts[peerId], let context = context, current === context { if current.subscribers.isEmpty { strongSelf.profileDataPreloadContexts.removeValue(forKey: peerId) diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 494c70e4704..ba9a8fbd571 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -19,11 +19,22 @@ public struct ChatTextInputAttributes { public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl") public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler") public static let customEmoji = NSAttributedString.Key(rawValue: "Attribute__CustomEmoji") + public static let code = NSAttributedString.Key(rawValue: "Attribute__Code") + public static let quote = NSAttributedString.Key(rawValue: "Attribute__Blockquote") - public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji] + public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji, ChatTextInputAttributes.code, ChatTextInputAttributes.quote] } public let originalTextAttributeKey = NSAttributedString.Key(rawValue: "Attribute__OriginalText") +public final class OriginalTextAttribute: NSObject { + public let id: Int + public let string: String + + public init(id: Int, string: String) { + self.id = id + self.string = string + } +} public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString { let sourceString = NSMutableAttributedString(attributedString: text) @@ -55,17 +66,40 @@ public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttrib return result } -public struct ChatTextFontAttributes: OptionSet { - public var rawValue: Int32 = 0 +public struct ChatTextFontAttributes: OptionSet, Hashable, Sequence { + public var rawValue: UInt32 = 0 - public init(rawValue: Int32) { + public init(rawValue: UInt32) { self.rawValue = rawValue } + public init() { + self.rawValue = 0 + } + public static let bold = ChatTextFontAttributes(rawValue: 1 << 0) public static let italic = ChatTextFontAttributes(rawValue: 1 << 1) public static let monospace = ChatTextFontAttributes(rawValue: 1 << 2) public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) + + public func makeIterator() -> AnyIterator { + var index = 0 + return AnyIterator { () -> ChatTextFontAttributes? in + while index < 31 { + let currentTags = self.rawValue >> UInt32(index) + let tag = ChatTextFontAttributes(rawValue: 1 << UInt32(index)) + index += 1 + if currentTags == 0 { + break + } + + if (currentTags & 1) != 0 { + return tag + } + } + return nil + } + } } public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) -> NSAttributedString { @@ -115,11 +149,25 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo } else if key == ChatTextInputAttributes.customEmoji { result.addAttribute(key, value: value, range: range) result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } else if key == ChatTextInputAttributes.quote { + fontAttributes.insert(.blockQuote) + result.addAttribute(key, value: value, range: range) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 8.0 + paragraphStyle.headIndent = 8.0 + //paragraphStyle.paragraphSpacing = 8.0 + //paragraphStyle.paragraphSpacingBefore = 8.0 + //result.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) } } if !fontAttributes.isEmpty { var font: UIFont? + var fontSize = fontSize + if fontAttributes.contains(.blockQuote) { + fontAttributes.remove(.blockQuote) + fontSize = round(fontSize * 0.8235294117647058) + } if fontAttributes == [.bold, .italic, .monospace] { font = Font.semiboldItalicMonospace(fontSize) } else if fontAttributes == [.bold, .monospace] { @@ -134,6 +182,8 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo font = Font.italic(fontSize) } else if fontAttributes == [.monospace] { font = Font.monospace(fontSize) + } else { + font = Font.regular(fontSize) } if let font = font { @@ -193,6 +243,22 @@ public final class ChatTextInputTextUrlAttribute: NSObject { } } +public final class ChatTextInputTextQuoteAttribute: NSObject { + override public init() { + super.init() + } + + override public func isEqual(_ object: Any?) -> Bool { + guard let other = object as? ChatTextInputTextQuoteAttribute else { + return false + } + + let _ = other + + return true + } +} + public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { private enum CodingKeys: String, CodingKey { case interactivelySelectedFromPackId @@ -506,8 +572,82 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed } } -public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { - refreshChatTextInputAttributes(textView: textNode.textView, primaryTextColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, baseFontSize: baseFontSize, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) +private func quoteRangesEqual(_ lhs: [(NSRange, ChatTextInputTextQuoteAttribute)], _ rhs: [(NSRange, ChatTextInputTextQuoteAttribute)]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if lhs[i].0 != rhs[i].0 || !lhs[i].1.isEqual(rhs[i].1) { + return false + } + } + return true +} + +private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttributedString, attributedText: NSMutableAttributedString, fullRange: NSRange) { + var quoteRanges: [(NSRange, ChatTextInputTextQuoteAttribute)] = [] + initialAttributedText.enumerateAttributes(in: fullRange, using: { dict, range, _ in + if let value = dict[ChatTextInputAttributes.quote] as? ChatTextInputTextQuoteAttribute { + quoteRanges.append((range, value)) + } + }) + quoteRanges.sort(by: { $0.0.location < $1.0.location }) + let initialQuoteRanges = quoteRanges + quoteRanges = quoteRanges.filter({ $0.0.length > 0 }) + + for i in 0 ..< quoteRanges.count { + var backIndex = quoteRanges[i].0.lowerBound + innerBack: while backIndex >= 0 { + let character = text.character(at: backIndex) + if character == 0x0a { + backIndex += 1 + break innerBack + } + backIndex -= 1 + } + backIndex = max(backIndex, 0) + + if backIndex < quoteRanges[i].0.lowerBound { + quoteRanges[i].0 = NSRange(location: backIndex, length: quoteRanges[i].0.upperBound - backIndex) + } + + var forwardIndex = quoteRanges[i].0.upperBound + innerForward: while forwardIndex < text.length { + let character = text.character(at: forwardIndex) + if character == 0x0a { + forwardIndex -= 1 + break innerForward + } + forwardIndex += 1 + } + forwardIndex = min(forwardIndex, text.length - 1) + + if forwardIndex > quoteRanges[i].0.upperBound - 1 { + quoteRanges[i].0 = NSRange(location: quoteRanges[i].0.lowerBound, length: forwardIndex + 1 - quoteRanges[i].0.lowerBound) + } + } + + for i in (0 ..< quoteRanges.count).reversed() { + inner: for mergeIndex in (i + 1 ..< quoteRanges.count).reversed() { + if quoteRanges[mergeIndex].1 === quoteRanges[i].1 || quoteRanges[mergeIndex].0.intersection(quoteRanges[i].0) != nil { + quoteRanges[i].0 = NSRange(location: quoteRanges[i].0.location, length: quoteRanges[mergeIndex].0.location + quoteRanges[mergeIndex].0.length - quoteRanges[i].0.location) + quoteRanges.removeSubrange((i + 1) ..< (mergeIndex + 1)) + break inner + } + } + } + + if !quoteRangesEqual(quoteRanges, initialQuoteRanges) { + attributedText.removeAttribute(ChatTextInputAttributes.quote, range: fullRange) + for (range, attribute) in quoteRanges { + let _ = attribute + attributedText.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range) + } + } +} + +public func refreshChatTextInputAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { + refreshChatTextInputAttributes(textView: textView, primaryTextColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, baseFontSize: baseFontSize, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) } public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColor: UIColor, accentTextColor: UIColor, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { @@ -515,6 +655,8 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo return } + textView.textStorage.beginEditing() + var writingDirection: NSWritingDirection? if let style = initialAttributedText.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { writingDirection = style.baseWritingDirection @@ -534,6 +676,13 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + text = resultAttributedText.string as NSString + fullRange = NSRange(location: 0, length: text.length) + attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) + refreshBlockQuotes(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) + + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + if !resultAttributedText.isEqual(to: initialAttributedText) { fullRange = NSRange(location: 0, length: textView.textStorage.length) @@ -546,6 +695,7 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange) textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange) textView.textStorage.removeAttribute(ChatTextInputAttributes.customEmoji, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.quote, range: fullRange) textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange) textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: primaryTextColor, range: fullRange) @@ -589,11 +739,25 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { textView.textStorage.addAttribute(key, value: value, range: range) textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } else if key == ChatTextInputAttributes.quote { + fontAttributes.insert(.blockQuote) + textView.textStorage.addAttribute(key, value: value, range: range) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 8.0 + paragraphStyle.headIndent = 8.0 + //paragraphStyle.paragraphSpacing = 8.0 + //paragraphStyle.paragraphSpacingBefore = 8.0 + //textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) } } if !fontAttributes.isEmpty { var font: UIFont? + var baseFontSize = baseFontSize + if fontAttributes.contains(.blockQuote) { + fontAttributes.remove(.blockQuote) + baseFontSize = round(baseFontSize * 0.8235294117647058) + } if fontAttributes == [.bold, .italic, .monospace] { font = Font.semiboldItalicMonospace(baseFontSize) } else if fontAttributes == [.bold, .italic] { @@ -608,6 +772,8 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo font = Font.italic(baseFontSize) } else if fontAttributes == [.monospace] { font = Font.monospace(baseFontSize) + } else { + font = Font.regular(baseFontSize) } if let font = font { @@ -620,10 +786,12 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo textView.textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment)) } } + + textView.textStorage.endEditing() } -public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) { - guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { +public func refreshGenericTextInputAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) { + guard let initialAttributedText = textView.attributedText, initialAttributedText.length != 0 else { return } @@ -645,51 +813,58 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) if !resultAttributedText.isEqual(to: initialAttributedText) { - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.backgroundColor, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.underlineStyle, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange) - textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange) - textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange) - textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.backgroundColor, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.underlineStyle, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.primaryTextColor, range: fullRange) + textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange) + textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.primaryTextColor, range: fullRange) attributedText.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in var fontAttributes: ChatTextFontAttributes = [] for (key, value) in attributes { if key == ChatTextInputAttributes.textMention || key == ChatTextInputAttributes.textUrl { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.panelControlAccentColor, range: range) + textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.panelControlAccentColor, range: range) if theme.chat.inputPanel.panelControlAccentColor.isEqual(theme.chat.inputPanel.primaryTextColor) { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } } else if key == ChatTextInputAttributes.bold { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(key, value: value, range: range) fontAttributes.insert(.bold) } else if key == ChatTextInputAttributes.italic { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(key, value: value, range: range) fontAttributes.insert(.italic) } else if key == ChatTextInputAttributes.monospace { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(key, value: value, range: range) fontAttributes.insert(.monospace) } else if key == ChatTextInputAttributes.strikethrough { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.underline { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.spoiler { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(key, value: value, range: range) if spoilersRevealed { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) } else { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) } + } else if key == ChatTextInputAttributes.quote { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 8.0 + paragraphStyle.headIndent = 8.0 + //paragraphStyle.paragraphSpacing = 8.0 + //paragraphStyle.paragraphSpacingBefore = 8.0 + //textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) } } @@ -712,7 +887,7 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th } if let font = font { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.font, value: font, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.font, value: font, range: range) } } }) @@ -744,7 +919,7 @@ public func refreshChatTextInputTypingAttributes(_ textView: UITextView, textCol textView.typingAttributes = filteredAttributes } -public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { +public func refreshChatTextInputTypingAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat) { var filteredAttributes: [NSAttributedString.Key: Any] = [ NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: theme.chat.inputPanel.primaryTextColor @@ -752,8 +927,8 @@ public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, let style = NSMutableParagraphStyle() style.baseWritingDirection = .natural filteredAttributes[NSAttributedString.Key.paragraphStyle] = style - if let attributedText = textNode.attributedText, attributedText.length != 0 { - let attributes = attributedText.attributes(at: max(0, min(textNode.selectedRange.location - 1, attributedText.length - 1)), effectiveRange: nil) + if let attributedText = textView.attributedText, attributedText.length != 0 { + let attributes = attributedText.attributes(at: max(0, min(textView.selectedRange.location - 1, attributedText.length - 1)), effectiveRange: nil) for (key, value) in attributes { if key == ChatTextInputAttributes.bold { filteredAttributes[key] = value @@ -766,7 +941,7 @@ public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, } } } - textNode.textView.typingAttributes = filteredAttributes + textView.typingAttributes = filteredAttributes } private func trimRangesForChatInputText(_ text: NSAttributedString) -> (Int, Int) { @@ -882,7 +1057,7 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu stringOffset -= match.range(at: 2).length + match.range(at: 4).length let substring = string.substring(with: match.range(at: 1)) + text + string.substring(with: match.range(at: 5)) - result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.monospace: true as NSNumber])) + result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.code: true as NSNumber])) offsetRanges.append((NSMakeRange(matchIndex + match.range(at: 1).length, text.count), 6)) } } @@ -902,13 +1077,20 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu } else { let text = string.substring(with: pre) - let entity = string.substring(with: match.range(at: 7)) - let substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9)) + var entity = string.substring(with: match.range(at: 7)) + var substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9)) + + if entity == "`" && substring.hasPrefix("``") && substring.hasSuffix("``") { + entity = "```" + substring = String(substring[substring.index(substring.startIndex, offsetBy: 2) ..< substring.index(substring.endIndex, offsetBy: -2)]) + } let textInputAttribute: NSAttributedString.Key? switch entity { case "`": textInputAttribute = ChatTextInputAttributes.monospace + case "```": + textInputAttribute = ChatTextInputAttributes.code case "**": textInputAttribute = ChatTextInputAttributes.bold case "__": diff --git a/submodules/TextFormat/Sources/CountNicePercent.swift b/submodules/TextFormat/Sources/CountNicePercent.swift new file mode 100644 index 00000000000..c62314da224 --- /dev/null +++ b/submodules/TextFormat/Sources/CountNicePercent.swift @@ -0,0 +1,77 @@ +import Foundation +import TelegramCore + +private struct PercentCounterItem: Comparable { + var index: Int = 0 + var percent: Int = 0 + var remainder: Int = 0 + + static func <(lhs: PercentCounterItem, rhs: PercentCounterItem) -> Bool { + if lhs.remainder > rhs.remainder { + return true + } else if lhs.remainder < rhs.remainder { + return false + } + return lhs.percent < rhs.percent + } + +} + +private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] { + var left = left + var items = items.sorted(by: <) + var i:Int = 0 + while i != items.count { + let item = items[i] + var j = i + 1 + loop: while j != items.count { + if items[j].percent != item.percent || items[j].remainder != item.remainder { + break loop + } + j += 1 + } + if items[i].remainder == 0 { + break + } + let equal = j - i + if equal <= left { + left -= equal + while i != j { + items[i].percent += 1 + i += 1 + } + } else { + i = j + } + } + return items +} + +public func countNicePercent(votes: [Int], total: Int) -> [Int] { + var result: [Int] = [] + var items: [PercentCounterItem] = [] + for _ in votes { + result.append(0) + items.append(PercentCounterItem()) + } + + let count = votes.count + + var left:Int = 100 + for i in 0 ..< votes.count { + let votes = votes[i] + items[i].index = i + items[i].percent = Int((Float(votes) * 100) / Float(total)) + items[i].remainder = (votes * 100) - (items[i].percent * total) + left -= items[i].percent + } + + if left > 0 && left <= count { + items = adjustPercentCount(items, left: left) + } + for item in items { + result[item.index] = item.percent + } + + return result +} diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index f30fb928c55..feacfe990da 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -144,7 +144,7 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, } } -public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil) -> [MessageTextEntity] { +public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil, generateLinks: Bool = false) -> [MessageTextEntity] { var entities: [MessageTextEntity] = [] text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in @@ -167,9 +167,20 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler)) } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: nil, fileId: value.fileId))) + } else if key == ChatTextInputAttributes.code { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Code)) + } else if key == ChatTextInputAttributes.quote { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .BlockQuote)) } } }) + + for entity in generateTextEntities(text.string, enabledTypes: .allUrl) { + if case .Url = entity.type { + entities.append(entity) + } + } + return entities } diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index a8cf179e0ef..c55343b4e3a 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Postbox import TelegramCore +import Display public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString { var nsString: NSString? @@ -45,6 +46,8 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range) case let .CustomEmoji(_, fileId): string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range) + case .BlockQuote: + string.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range) default: break } @@ -52,7 +55,9 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M return string } -public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:]) -> NSAttributedString { +public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false) -> NSAttributedString { + let baseQuoteTintColor = baseQuoteTintColor ?? baseColor + var nsString: NSString? let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor]) var skipEntity = false @@ -60,9 +65,14 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti if linkColor.argb == baseColor.argb { underlineAllLinks = true } - var fontAttributes: [NSRange: ChatTextFontAttributes] = [:] - var rangeOffset: Int = 0 + var fontAttributeMask: [ChatTextFontAttributes] = Array(repeating: [], count: string.length) + let addFontAttributes: (NSRange, ChatTextFontAttributes) -> Void = { range, attributes in + for i in range.lowerBound ..< range.upperBound { + fontAttributeMask[i].formUnion(attributes) + } + } + for i in 0 ..< entities.count { if skipEntity { skipEntity = false @@ -70,7 +80,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } let stringLength = string.length let entity = entities[i] - var range = NSRange(location: entity.range.lowerBound + rangeOffset, length: entity.range.upperBound - entity.range.lowerBound) + var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) if nsString == nil { nsString = text as NSString } @@ -121,17 +131,9 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: url, range: range) } case .Bold: - if let fontAttribute = fontAttributes[range] { - fontAttributes[range] = fontAttribute.union(.bold) - } else { - fontAttributes[range] = .bold - } + addFontAttributes(range, .bold) case .Italic: - if let fontAttribute = fontAttributes[range] { - fontAttributes[range] = fontAttribute.union(.italic) - } else { - fontAttributes[range] = .italic - } + addFontAttributes(range, .italic) case .Mention: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if underlineLinks && underlineAllLinks { @@ -197,31 +199,22 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti nsString = text as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range) - case .Code, .Pre: - string.addAttribute(NSAttributedString.Key.font, value: fixedFont, range: range) + case .Pre: + addFontAttributes(range, .monospace) if nsString == nil { nsString = text as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range) - case .BlockQuote: - if let fontAttribute = fontAttributes[range] { - fontAttributes[range] = fontAttribute.union(.blockQuote) - } else { - fontAttributes[range] = .blockQuote + case .Code: + addFontAttributes(range, .monospace) + if nsString == nil { + nsString = text as NSString } + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Code), value: nsString!.substring(with: range), range: range) + case .BlockQuote: + addFontAttributes(range, .blockQuote) - let paragraphBreak = "\n" - string.insert(NSAttributedString(string: paragraphBreak), at: range.lowerBound) - - let paragraphRange = NSRange(location: range.lowerBound + paragraphBreak.count, length: range.upperBound - range.lowerBound) - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.headIndent = 10.0 - paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])] - string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: paragraphRange) - - string.insert(NSAttributedString(string: paragraphBreak), at: paragraphRange.upperBound) - rangeOffset += paragraphBreak.count + string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(title: nil, color: baseQuoteTintColor, secondaryColor: baseQuoteSecondaryTintColor, tertiaryColor: baseQuoteTertiaryTintColor), range: range) case .BankCard: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if underlineLinks && underlineAllLinks { @@ -264,56 +257,45 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti break } - var addedAttributes: [(NSRange, ChatTextFontAttributes)] = [] - func addFont(ranges: [NSRange], fontAttributes: ChatTextFontAttributes) { - for range in ranges { - var font: UIFont? - if fontAttributes == [.bold, .italic] { - font = boldItalicFont - } else if fontAttributes == [.bold] { - font = boldFont - addedAttributes.append((range, fontAttributes)) - } else if fontAttributes == [.italic] { - font = italicFont - addedAttributes.append((range, fontAttributes)) - } - if let font = font { - string.addAttribute(NSAttributedString.Key.font, value: font, range: range) - } + func setFont(range: NSRange, fontAttributes: ChatTextFontAttributes) { + var font: UIFont + + var isQuote = false + var fontAttributes = fontAttributes + if fontAttributes.contains(.blockQuote) { + fontAttributes.remove(.blockQuote) + isQuote = true } + if fontAttributes == [.bold, .italic] { + font = boldItalicFont + } else if fontAttributes == [.bold] { + font = boldFont + } else if fontAttributes == [.italic] { + font = italicFont + } else if fontAttributes == [.monospace] { + font = fixedFont + } else { + font = baseFont + } + + if adjustQuoteFontSize, isQuote { + font = font.withSize(round(font.pointSize * 0.8235294117647058)) + } + + string.addAttribute(.font, value: font, range: range) } - for (range, fontAttributes) in fontAttributes { - var ranges = [range] - var fontAttributes = fontAttributes - if fontAttributes != [.bold, .italic] { - for (existingRange, existingAttributes) in addedAttributes { - if let intersection = existingRange.intersection(range) { - if intersection.length == range.length { - if existingAttributes == .bold || existingAttributes == .italic { - fontAttributes.insert(existingAttributes) - } - } else { - var fontAttributes = fontAttributes - if existingAttributes == .bold || existingAttributes == .italic { - fontAttributes.insert(existingAttributes) - } - addFont(ranges: [intersection], fontAttributes: fontAttributes) - - ranges = [] - if range.upperBound > existingRange.lowerBound { - ranges.append(NSRange(location: range.lowerBound, length: existingRange.lowerBound - range.lowerBound)) - } - if range.upperBound > existingRange.upperBound { - ranges.append(NSRange(location: existingRange.upperBound, length: range.upperBound - existingRange.upperBound)) - } - } - break - } + var currentAttributeSpan: (startIndex: Int, attributes: ChatTextFontAttributes)? + for i in 0 ..< fontAttributeMask.count { + if fontAttributeMask[i] != currentAttributeSpan?.attributes { + if let currentAttributeSpan { + setFont(range: NSRange(location: currentAttributeSpan.startIndex, length: i - currentAttributeSpan.startIndex), fontAttributes: currentAttributeSpan.attributes) } + currentAttributeSpan = (i, fontAttributeMask[i]) } - - addFont(ranges: ranges, fontAttributes: fontAttributes) + } + if let currentAttributeSpan { + setFont(range: NSRange(location: currentAttributeSpan.startIndex, length: fontAttributeMask.count - currentAttributeSpan.startIndex), fontAttributes: currentAttributeSpan.attributes) } } return string diff --git a/submodules/TextFormat/Sources/TelegramAttributes.swift b/submodules/TextFormat/Sources/TelegramAttributes.swift index ffa8508e857..1f75c378d3a 100644 --- a/submodules/TextFormat/Sources/TelegramAttributes.swift +++ b/submodules/TextFormat/Sources/TelegramAttributes.swift @@ -42,4 +42,5 @@ public struct TelegramTextAttributes { public static let BlockQuote = "TelegramBlockQuote" public static let Pre = "TelegramPre" public static let Spoiler = "TelegramSpoiler" + public static let Code = "TelegramCode" } diff --git a/submodules/TextFormat/Sources/TextFormat.h b/submodules/TextFormat/Sources/TextFormat.h deleted file mode 100644 index dee068c1cc8..00000000000 --- a/submodules/TextFormat/Sources/TextFormat.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// TextFormat.h -// TextFormat -// -// Created by Peter on 8/1/19. -// Copyright © 2019 Telegram Messenger LLP. All rights reserved. -// - -#import - -//! Project version number for TextFormat. -FOUNDATION_EXPORT double TextFormatVersionNumber; - -//! Project version string for TextFormat. -FOUNDATION_EXPORT const unsigned char TextFormatVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 5e19476099b..fd6cf39c1ab 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -61,11 +61,13 @@ public final class TextSelectionTheme { public let selection: UIColor public let knob: UIColor public let knobDiameter: CGFloat + public let isDark: Bool - public init(selection: UIColor, knob: UIColor, knobDiameter: CGFloat = 12.0) { + public init(selection: UIColor, knob: UIColor, knobDiameter: CGFloat = 12.0, isDark: Bool) { self.selection = selection self.knob = knob self.knobDiameter = knobDiameter + self.isDark = isDark } } @@ -204,12 +206,13 @@ public final class TextSelectionNodeView: UIView { } } -public enum TextSelectionAction { +public enum TextSelectionAction: Equatable { case copy case share case lookup case speak case translate + case quote(range: Range) } public final class TextSelectionNode: ASDisplayNode { @@ -219,6 +222,7 @@ public final class TextSelectionNode: ASDisplayNode { private let updateIsActive: (Bool) -> Void public var canBeginSelection: (CGPoint) -> Bool = { _ in true } public var updateRange: ((NSRange?) -> Void)? + public var presentMenu: ((UIView, CGPoint, [ContextMenuAction]) -> Void)? private let present: (ViewController, Any?) -> Void private let rootNode: () -> ASDisplayNode? private let performAction: (NSAttributedString, TextSelectionAction) -> Void @@ -234,12 +238,20 @@ public final class TextSelectionNode: ASDisplayNode { public private(set) var recognizer: TextSelectionGestureRecognizer? private var displayLinkAnimator: DisplayLinkAnimator? + public var enableCopy: Bool = true public var enableLookup: Bool = true + public var enableQuote: Bool = false + public var enableTranslate: Bool = true + public var enableShare: Bool = true + + public var menuSkipCoordnateConversion: Bool = false public var didRecognizeTap: Bool { return self.recognizer?.didRecognizeTap ?? false } + private weak var contextMenu: ContextMenuController? + public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: @escaping () -> ASDisplayNode?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) { self.theme = theme self.strings = strings @@ -436,6 +448,126 @@ public final class TextSelectionNode: ASDisplayNode { self.displayLinkAnimator = displayLinkAnimator } + public func setSelection(range: NSRange, displayMenu: Bool) { + guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else { + return + } + let range = self.convertSelectionFromOriginalText(attributedString: attributedString, range: range) + + self.currentRange = (range.lowerBound, range.upperBound) + self.updateSelection(range: range, animateIn: true) + self.updateIsActive(true) + + if displayMenu { + self.displayMenu() + } + } + + private func convertSelectionToOriginalText(attributedString: NSAttributedString, range: NSRange) -> NSRange { + var adjustedRange = range + + do { + attributedString.enumerateAttribute(originalTextAttributeKey, in: NSRange(location: 0, length: range.lowerBound), options: [], using: { value, range, stop in + guard let value = value as? OriginalTextAttribute else { + return + } + let updatedSubstring = NSMutableAttributedString(string: value.string) + let difference = updatedSubstring.length - range.length + + adjustedRange.location += difference + }) + } + + do { + attributedString.enumerateAttribute(originalTextAttributeKey, in: range, options: [], using: { value, range, stop in + guard let value = value as? OriginalTextAttribute else { + return + } + let updatedSubstring = NSMutableAttributedString(string: value.string) + let difference = updatedSubstring.length - range.length + + adjustedRange.length += difference + }) + } + + return adjustedRange + } + + private func convertSelectionFromOriginalText(attributedString: NSAttributedString, range: NSRange) -> NSRange { + var adjustedRange = range + + final class PreviousText: NSObject { + let id: Int + let string: String + + init(id: Int, string: String) { + self.id = id + self.string = string + } + } + + var nextId = 0 + let attributedString = NSMutableAttributedString(attributedString: attributedString) + var fullRange = NSRange(location: 0, length: attributedString.length) + while true { + var found = false + attributedString.enumerateAttribute(originalTextAttributeKey, in: fullRange, options: [], using: { value, range, stop in + if let value = value as? OriginalTextAttribute { + let updatedSubstring = NSMutableAttributedString(string: value.string) + + let replacementRange = NSRange(location: 0, length: updatedSubstring.length) + updatedSubstring.addAttributes(attributedString.attributes(at: range.location, effectiveRange: nil), range: replacementRange) + updatedSubstring.addAttribute(NSAttributedString.Key(rawValue: "__previous_text"), value: PreviousText(id: nextId, string: attributedString.attributedSubstring(from: range).string), range: replacementRange) + nextId += 1 + + attributedString.replaceCharacters(in: range, with: updatedSubstring) + let updatedRange = NSRange(location: range.location, length: updatedSubstring.length) + + found = true + stop.pointee = ObjCBool(true) + fullRange = NSRange(location: updatedRange.upperBound, length: fullRange.upperBound - range.upperBound) + } + }) + if !found { + break + } + } + + do { + attributedString.enumerateAttribute(NSAttributedString.Key(rawValue: "__previous_text"), in: NSRange(location: 0, length: range.lowerBound), options: [], using: { value, range, stop in + guard let value = value as? PreviousText else { + return + } + let updatedSubstring = NSMutableAttributedString(string: value.string) + let difference = updatedSubstring.length - range.length + + adjustedRange.location += difference + }) + } + + do { + attributedString.enumerateAttribute(NSAttributedString.Key(rawValue: "__previous_text"), in: range, options: [], using: { value, range, stop in + guard let value = value as? PreviousText else { + return + } + let updatedSubstring = NSMutableAttributedString(string: value.string) + let difference = updatedSubstring.length - range.length + + adjustedRange.length += difference + }) + } + + return adjustedRange + } + + public func getSelection() -> NSRange? { + guard let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else { + return nil + } + let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1)) + return self.convertSelectionToOriginalText(attributedString: attributedString, range: range) + } + private func updateSelection(range: NSRange?, animateIn: Bool) { self.updateRange?(range) @@ -524,6 +656,9 @@ public final class TextSelectionNode: ASDisplayNode { self.currentRange = nil self.recognizer?.isSelecting = false self.updateSelection(range: nil, animateIn: false) + + self.contextMenu?.dismiss() + self.contextMenu = nil } public func cancelSelection() { @@ -548,8 +683,8 @@ public final class TextSelectionNode: ASDisplayNode { while true { var found = false string.enumerateAttribute(originalTextAttributeKey, in: fullRange, options: [], using: { value, range, stop in - if let value = value as? String { - let updatedSubstring = NSMutableAttributedString(string: value) + if let value = value as? OriginalTextAttribute { + let updatedSubstring = NSMutableAttributedString(string: value.string) let replacementRange = NSRange(location: 0, length: updatedSubstring.length) updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) @@ -567,11 +702,21 @@ public final class TextSelectionNode: ASDisplayNode { } } + let adjustedRange = self.convertSelectionToOriginalText(attributedString: attributedString, range: range) + var actions: [ContextMenuAction] = [] - actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in - self?.performAction(string, .copy) - self?.cancelSelection() - })) + if self.enableCopy { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in + self?.performAction(string, .copy) + self?.cancelSelection() + })) + } + if self.enableQuote { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuQuote, accessibilityLabel: self.strings.Conversation_ContextMenuQuote), action: { [weak self] in + self?.performAction(string, .quote(range: adjustedRange.lowerBound ..< adjustedRange.upperBound)) + self?.cancelSelection() + })) + } if self.enableLookup { actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in self?.performAction(string, .lookup) @@ -579,10 +724,12 @@ public final class TextSelectionNode: ASDisplayNode { })) } if #available(iOS 15.0, *) { - actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in - self?.performAction(string, .translate) - self?.cancelSelection() - })) + if self.enableTranslate { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in + self?.performAction(string, .translate) + self?.cancelSelection() + })) + } } // if isSpeakSelectionEnabled() { // actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in @@ -590,16 +737,47 @@ public final class TextSelectionNode: ASDisplayNode { // self?.dismissSelection() // })) // } - actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in - self?.performAction(string, .share) - self?.cancelSelection() - })) - self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + let realFullRange = NSRange(location: 0, length: attributedString.length) + if range != realFullRange { + actions.append(ContextMenuAction(content: .text(title: self.strings.TextSelection_SelectAll, accessibilityLabel: self.strings.TextSelection_SelectAll), action: { [weak self] in + guard let self else { + return + } + self.contextMenu?.dismiss() + self.setSelection(range: realFullRange, displayMenu: true) + })) + } else if self.enableShare { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in + self?.performAction(string, .share) + self?.cancelSelection() + })) + } + + self.contextMenu?.dismiss() + + let contextMenu = makeContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false, isDark: self.theme.isDark, skipCoordnateConversion: self.menuSkipCoordnateConversion) + contextMenu.dismissOnTap = { [weak self] view, point in + guard let self else { + return true + } + if self.knobAtPoint(view.convert(point, to: self.view)) == nil { + //self.cancelSelection() + return true + } + return true + } + self.contextMenu = contextMenu + self.present(contextMenu, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in guard let strongSelf = self, let rootNode = strongSelf.rootNode() else { return nil } - return (strongSelf, completeRect, rootNode, rootNode.bounds) + + if strongSelf.menuSkipCoordnateConversion { + return (strongSelf, strongSelf.view.convert(completeRect, to: rootNode.view), rootNode, rootNode.bounds) + } else { + return (strongSelf, completeRect, rootNode, rootNode.bounds) + } }, bounce: false)) } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 8ca3a90988b..272dfe03416 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 8ca3a90988bba9e139efe0e9ba24f344f5a91da5 +Subproject commit 272dfe03416f1022dfc5d73875dbe8ace9ec9cfd diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index f87938649ed..4abd7584a20 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -217,7 +217,9 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) ranges.append(text.index(text.startIndex, offsetBy: entity.range.lowerBound) ..< text.index(text.startIndex, offsetBy: entity.range.upperBound)) } for range in ranges { - text.removeSubrange(range) + if range.upperBound < text.endIndex { + text.removeSubrange(range) + } } } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 5be099fc8e0..a5775b10087 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -11,8 +11,8 @@ public enum UndoOverlayContent { case archivedChat(peerId: Int64, title: String, text: String, undo: Bool) case hidArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool) - case succeed(text: String, timeout: Double?) - case info(title: String?, text: String, timeout: Double?) + case succeed(text: String, timeout: Double?, customUndoText: String?) + 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) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index d81161b01b9..592379be071 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -191,7 +191,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if text.contains("](") { isUserInteractionEnabled = true } - case let .succeed(text, timeout): + case let .succeed(text, timeout, customUndoText): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -203,9 +203,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 5 - displayUndo = false + if let customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } self.originalRemainingSeconds = timeout ?? 3 - case let .info(title, text, timeout): + case let .info(title, text, timeout, customUndoText): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -224,7 +229,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 10 - displayUndo = false + if let customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } if let timeout { self.originalRemainingSeconds = timeout } else { @@ -234,7 +244,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if text.contains("](") { isUserInteractionEnabled = true } - case let .actionSucceeded(title, text, cancel, destructive): self.avatarNode = nil self.iconNode = nil @@ -1313,7 +1322,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var contentHeight: CGFloat = 20.0 let margin: CGFloat = 12.0 - let leftMargin = 12.0 + layout.insets(options: []).left + let leftMargin = margin + layout.insets(options: []).left let buttonTextSize = self.undoButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let buttonMinX: CGFloat @@ -1365,7 +1374,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let undoButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - leftMargin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + leftMargin, height: contentHeight)) self.undoButtonNode.frame = undoButtonFrame - self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: undoButtonFrame.minX, height: contentHeight)) + self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.undoButtonNode.supernode == nil ? panelFrame.width : undoButtonFrame.minX, height: contentHeight)) var textContentHeight = textSize.height var textOffset: CGFloat = 0.0 diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index b8f4ec23ed9..ea595eb8dc5 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -100,6 +100,7 @@ public enum ParsedInternalUrl { case startAttach(String, String?, String?) case contactToken(String) case chatFolder(slug: String) + case premiumGiftCode(slug: String) } private enum ParsedUrl { @@ -212,6 +213,13 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } return .share(url: url, text: text, to: to) + } else if peerName == "boost" { + for queryItem in queryItems { + if queryItem.name == "c", let value = queryItem.value, let channelId = Int64(value) { + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + return .peer(.id(peerId), .boost) + } + } } else { for queryItem in queryItems { if let value = queryItem.value { @@ -444,6 +452,10 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .theme(pathComponents[1]) } else if pathComponents[0] == "addlist" || pathComponents[0] == "folder" || pathComponents[0] == "list" { return .chatFolder(slug: pathComponents[1]) + } else if pathComponents[0] == "boost", pathComponents.count == 2 { + return .peer(.name(pathComponents[1]), .boost) + } else if pathComponents[0] == "giftcode", pathComponents.count == 2 { + return .premiumGiftCode(slug: pathComponents[1]) } else if pathComponents.count == 3 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) { var threadId: Int32? @@ -579,227 +591,277 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return nil } -private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) -> Signal { +private enum ResolveInternalUrlResult { + case progress + case result(ResolvedUrl?) +} + +private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) -> Signal { switch url { case let .phone(phone, attach, startAttach): return context.engine.peers.resolvePeerByPhone(phone: phone) - |> take(1) - |> mapToSignal { peer -> Signal in + |> mapToSignal { peer -> Signal in if let peer = peer?._asPeer() { if let attach = attach { return context.engine.peers.resolvePeerByName(name: attach) - |> take(1) - |> map { botPeer -> ResolvedUrl? in - if let botPeer = botPeer?._asPeer() { - return .peer(peer, .withAttachBot(ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: startAttach, justInstalled: false))) - } else { - return .peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) + |> map { result -> ResolveInternalUrlResult in + switch result { + case .progress: + return .progress + case let .result(botPeer): + if let botPeer = botPeer?._asPeer() { + return .result(.peer(peer, .withAttachBot(ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: startAttach, justInstalled: false)))) + } else { + return .result(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))) + } } } } else { - return .single(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))) + return .single(.result(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)))) } } else { - return .single(.peer(nil, .info)) + return .single(.result(.peer(nil, .info))) } } case let .peer(reference, parameter): - let resolvedPeer: Signal + let resolvedPeer: Signal switch reference { case let .name(name): resolvedPeer = context.engine.peers.resolvePeerByName(name: name) - |> take(1) - |> mapToSignal { peer -> Signal in - return .single(peer?._asPeer()) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .single(.progress) + case let .result(peer): + return .single(.result(peer)) + } } case let .id(id): if id.namespace == Namespaces.Peer.CloudChannel { resolvedPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: id)) - |> mapToSignal { peer -> Signal in - let foundPeer: Signal + |> mapToSignal { peer -> Signal in + let foundPeer: Signal if let peer = peer { - foundPeer = .single(peer._asPeer()) + foundPeer = .single(.result(peer)) } else { - foundPeer = TelegramEngine(account: context.account).peers.findChannelById(channelId: id.id._internalGetInt64Value()) - |> map { peer -> Peer? in - return peer?._asPeer() - } + foundPeer = .single(.progress) |> then(context.engine.peers.findChannelById(channelId: id.id._internalGetInt64Value()) + |> map { peer -> ResolvePeerResult in + return .result(peer) + }) } return foundPeer } } else { - resolvedPeer = .single(nil) + resolvedPeer = .single(.result(nil)) } } return resolvedPeer - |> mapToSignal { peer -> Signal in + |> mapToSignal { result -> Signal in + guard case let .result(peer) = result else { + return .single(.progress) + } + if let peer = peer { if let parameter = parameter { switch parameter { case let .botStart(payload): - return .single(.botStart(peer: peer, payload: payload)) + return .single(.result(.botStart(peer: peer._asPeer(), payload: payload))) case let .groupBotStart(payload, adminRights): - return .single(.groupBotStart(peerId: peer.id, payload: payload, adminRights: adminRights)) + return .single(.result(.groupBotStart(peerId: peer.id, payload: payload, adminRights: adminRights))) case let .gameStart(game): - return .single(.gameStart(peerId: peer.id, game: game)) + return .single(.result(.gameStart(peerId: peer.id, game: game))) case let .attachBotStart(name, payload): return context.engine.peers.resolvePeerByName(name: name) - |> take(1) - |> mapToSignal { botPeer -> Signal in - return .single(botPeer?._asPeer()) - } - |> mapToSignal { botPeer -> Signal in - if let botPeer = botPeer { - return .single(.peer(peer, .withAttachBot(ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: false)))) - } else { - return .single(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))) + |> mapToSignal { botPeerResult -> Signal in + switch botPeerResult { + case .progress: + return .single(.progress) + case let .result(botPeer): + if let botPeer = botPeer { + return .single(.result(.peer(peer._asPeer(), .withAttachBot(ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: false))))) + } else { + return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) + } } } case let .appStart(name, payload): - return context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false) + return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> take(1) - |> mapToSignal { botApp -> Signal in + |> mapToSignal { botApp -> Signal in if let botApp { - return .single(.peer(peer, .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false)))) + return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false))))) } else { - return .single(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))) + return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } - } + }) case let .channelMessage(id, timecode): - if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { + 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)) |> take(1) - |> mapToSignal { messages -> Signal in - if let threadId = messages.first?.threadId { - return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: threadId) - |> map { info -> ResolvedUrl? in - if let _ = info { - return .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) - } else { - return .peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .single(.progress) + case let .result(messages): + if let threadId = messages.first?.threadId { + return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: threadId) + |> map { result -> ResolveInternalUrlResult in + switch result { + case .progress: + 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)) + } else { + return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + } + } } + } else { + return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } - } else { - return .single(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))) } } } else { - return .single(.channelMessage(peer: peer, messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), timecode: timecode)) + return .single(.result(.channelMessage(peer: peer._asPeer(), messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), timecode: timecode))) } case let .replyThread(id, replyId): let replyThreadMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id) - if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(replyThreadMessageId.id)) - |> map { info -> ResolvedUrl? in - if let _ = info { - return .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)) - } else { - return .peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) + |> map { result -> ResolveInternalUrlResult in + switch result { + case .progress: + 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))) + } else { + return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + } } } } else { - return context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil) + return .single(.progress) |> then(context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> map { result -> ResolvedUrl? in + |> map { result -> ResolveInternalUrlResult in guard let result = result else { - return .channelMessage(peer: peer, messageId: replyThreadMessageId, timecode: nil) + return .result(.channelMessage(peer: peer._asPeer(), messageId: replyThreadMessageId, timecode: nil)) } - return .replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId)) - } + return .result(.replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))) + }) } case let .voiceChat(invite): - return .single(.joinVoiceChat(peer.id, invite)) + return .single(.result(.joinVoiceChat(peer.id, invite))) case let .story(id): - return context.engine.messages.refreshStories(peerId: peer.id, ids: [id]) - |> map { _ -> ResolvedUrl? in + return .single(.progress) |> then(context.engine.messages.refreshStories(peerId: peer.id, ids: [id]) + |> map { _ -> ResolveInternalUrlResult in } - |> then(.single(.story(peerId: peer.id, id: id))) + |> then(.single(.result(.story(peerId: peer.id, id: id))))) case .boost: - return combineLatest( + return .single(.progress) |> then(combineLatest( context.engine.peers.getChannelBoostStatus(peerId: peer.id), - context.engine.peers.canApplyChannelBoost(peerId: peer.id) + context.engine.peers.getMyBoostStatus() ) - |> map { boostStatus, canApplyStatus -> ResolvedUrl? in - return .boost(peerId: peer.id, status: boostStatus, canApplyStatus: canApplyStatus) - } + |> map { boostStatus, myBoostStatus -> ResolveInternalUrlResult in + return .result(.boost(peerId: peer.id, status: boostStatus, myBoostStatus: myBoostStatus)) + }) } } else { - return .single(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))) + return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } } else { - return .single(.peer(nil, .info)) + return .single(.result(.peer(nil, .info))) } } case let .peerId(peerId): return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> mapToSignal { peer -> Signal in + |> mapToSignal { peer -> Signal in if let peer = peer { - return .single(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)))) } else { - return .single(.inaccessiblePeer) + return .single(.result(.inaccessiblePeer)) } } case let .contactToken(token): - return context.engine.peers.importContactToken(token: token) - |> mapToSignal { peer -> Signal in + return .single(.progress) |> then(context.engine.peers.importContactToken(token: token) + |> mapToSignal { peer -> Signal in if let peer = peer { - return .single(.peer(peer._asPeer(), .info)) + return .single(.result(.peer(peer._asPeer(), .info))) } else { - return .single(.peer(nil, .info)) + return .single(.result(.peer(nil, .info))) } - } + }) case let .privateMessage(messageId, threadId, timecode): return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) - |> mapToSignal { peer -> Signal in + |> mapToSignal { peer -> Signal in let foundPeer: Signal if let peer = peer { foundPeer = .single(peer) } else { - foundPeer = TelegramEngine(account: context.account).peers.findChannelById(channelId: messageId.peerId.id._internalGetInt64Value()) + foundPeer = context.engine.peers.findChannelById(channelId: messageId.peerId.id._internalGetInt64Value()) } - return foundPeer - |> mapToSignal { foundPeer -> Signal in + return .single(.progress) |> then(foundPeer + |> mapToSignal { foundPeer -> Signal in if let foundPeer = foundPeer { if case let .channel(channel) = foundPeer, channel.flags.contains(.isForum) { if let threadId = threadId { return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(threadId)) - |> map { info -> ResolvedUrl? in - if let _ = info { - return .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) - } else { - return .peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)) + |> map { result -> ResolveInternalUrlResult in + switch result { + case .progress: + 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)) + } else { + return .result(.peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + } } } } else { return context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: false)) - |> take(1) - |> mapToSignal { messages -> Signal in - if let threadId = messages.first?.threadId { - return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: threadId) - |> map { info -> ResolvedUrl? in - if let _ = info { - return .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) - } else { - return .peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .single(.progress) + case let .result(messages): + if let threadId = messages.first?.threadId { + return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: threadId) + |> map { result -> ResolveInternalUrlResult in + switch result { + case .progress: + 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)) + } else { + return .result(.peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + } + } } - } - } else { - return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(messageId.id)) - |> map { info -> ResolvedUrl? in - if let _ = info { - return .replyThread(messageId: messageId) - } else { - return .peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil)) + } else { + return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(messageId.id)) + |> map { result -> ResolveInternalUrlResult in + switch result { + case .progress: + return .progress + case let .result(info): + if let _ = info { + return .result(.replyThread(messageId: messageId)) + } else { + return .result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + } + } } } } @@ -807,60 +869,67 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } else if let threadId = threadId { let replyThreadMessageId = MessageId(peerId: foundPeer.id, namespace: Namespaces.Message.Cloud, id: threadId) - return context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil) + return .single(.progress) |> then(context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> map { result -> ResolvedUrl? in + |> map { result -> ResolveInternalUrlResult in guard let result = result else { - return .channelMessage(peer: foundPeer._asPeer(), messageId: replyThreadMessageId, timecode: timecode) + return .result(.channelMessage(peer: foundPeer._asPeer(), messageId: replyThreadMessageId, timecode: timecode)) } - return .replyThreadMessage(replyThreadMessage: result, messageId: messageId) - } + return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId)) + }) } else { - return .single(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: true, timecode: timecode), peekData: nil))) + return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode), peekData: nil)))) } } else { - return .single(.inaccessiblePeer) + return .single(.result(.inaccessiblePeer)) } - } + }) } case let .stickerPack(name, type): - return .single(.stickerPack(name: name, type: type)) + return .single(.result(.stickerPack(name: name, type: type))) case let .chatFolder(slug): - return .single(.chatFolder(slug: slug)) + return .single(.result(.chatFolder(slug: slug))) case let .invoice(slug): - return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) + return .single(.progress) |> then(context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> map { invoice -> ResolvedUrl? in + |> map { invoice -> ResolveInternalUrlResult in guard let invoice = invoice else { - return .invoice(slug: slug, invoice: nil) + return .result(.invoice(slug: slug, invoice: nil)) } - return .invoice(slug: slug, invoice: invoice) - } + return .result(.invoice(slug: slug, invoice: invoice)) + }) case let .join(link): - return .single(.join(link)) + return .single(.result(.join(link))) case let .localization(identifier): - return .single(.localization(identifier)) + return .single(.result(.localization(identifier))) case let .proxy(host, port, username, password, secret): - return .single(.proxy(host: host, port: port, username: username, password: password, secret: secret)) + return .single(.result(.proxy(host: host, port: port, username: username, password: password, secret: secret))) case let .internalInstantView(url): return resolveInstantViewUrl(account: context.account, url: url) - |> map(Optional.init) + |> map { result in + switch result { + case .progress: + return .progress + case let .result(result): + return .result(result) + } + } case let .confirmationCode(code): - return .single(.confirmationCode(code)) + return .single(.result(.confirmationCode(code))) case let .cancelAccountReset(phone, hash): - return .single(.cancelAccountReset(phone: phone, hash: hash)) + return .single(.result(.cancelAccountReset(phone: phone, hash: hash))) case let .share(url, text, to): - return .single(.share(url: url, text: text, to: to)) + return .single(.result(.share(url: url, text: text, to: to))) case let .wallpaper(parameter): - return .single(.wallpaper(parameter)) + return .single(.result(.wallpaper(parameter))) case let .theme(slug): - return .single(.theme(slug)) + return .single(.result(.theme(slug))) case let .startAttach(name, payload, chooseValue): var choose: ResolvedBotChoosePeerTypes = [] if let chooseValue = chooseValue?.lowercased() { @@ -879,17 +948,20 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } return context.engine.peers.resolvePeerByName(name: name) - |> take(1) - |> mapToSignal { peer -> Signal in - return .single(peer?._asPeer()) - } - |> mapToSignal { peer -> Signal in - if let peer = peer { - return .single(.startAttach(peerId: peer.id, payload: payload, choose: !choose.isEmpty ? choose : nil)) - } else { - return .single(.inaccessiblePeer) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .single(.progress) + case let .result(peer): + if let peer = peer { + return .single(.result(.startAttach(peerId: peer.id, payload: payload, choose: !choose.isEmpty ? choose : nil))) + } else { + return .single(.result(.inaccessiblePeer)) + } } } + case let .premiumGiftCode(slug): + return .single(.result(.premiumGiftCode(slug: slug))) } } @@ -1006,13 +1078,13 @@ private struct UrlHandlingConfiguration { } } -public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { +public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { let schemes = ["http://", "https://", ""] return ApplicationSpecificNotice.getSecretChatLinkPreviews(accountManager: context.sharedContext.accountManager) - |> mapToSignal { linkPreviews -> Signal in + |> mapToSignal { linkPreviews -> Signal in return context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.App(), TelegramEngine.EngineData.Item.Configuration.Links()) - |> mapToSignal { appConfiguration, linksConfiguration -> Signal in + |> mapToSignal { appConfiguration, linksConfiguration -> Signal in let urlHandlingConfiguration = UrlHandlingConfiguration.with(appConfiguration: appConfiguration) var skipUrlAuth = skipUrlAuth @@ -1038,7 +1110,7 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String components.queryItems = queryItems url = components.url?.absoluteString ?? url } else if !skipUrlAuth && urlHandlingConfiguration.urlAuthDomains.contains(host) { - return .single(.urlAuth(url)) + return .single(.result(.urlAuth(url))) } } @@ -1053,15 +1125,20 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String if url.lowercased().hasPrefix(basePrefix) { if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])) { return resolveInternalUrl(context: context, url: internalUrl) - |> map { resolved -> ResolvedUrl in - if let resolved = resolved { - return resolved - } else { - return .externalUrl(url) + |> map { result -> ResolveUrlResult in + switch result { + case .progress: + return .progress + case let .result(resolved): + if let resolved = resolved { + return .result(resolved) + } else { + return .result(.externalUrl(url)) + } } } } else { - return .single(.externalUrl(url)) + return .single(.result(.externalUrl(url))) } } } @@ -1074,33 +1151,38 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String } } } - return .single(.externalUrl(url)) + return .single(.result(.externalUrl(url))) } } } -public func resolveInstantViewUrl(account: Account, url: String) -> Signal { +public func resolveInstantViewUrl(account: Account, url: String) -> Signal { return webpagePreview(account: account, url: url) - |> mapToSignal { webpage -> Signal in - if let webpage = webpage { - if case let .Loaded(content) = webpage.content { - if content.instantPage != nil { - var anchorValue: String? - if let anchorRange = url.range(of: "#") { - let anchor = url[anchorRange.upperBound...] - if !anchor.isEmpty { - anchorValue = String(anchor) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .single(.progress) + case let .result(webpage): + if let webpage = webpage { + if case let .Loaded(content) = webpage.content { + if content.instantPage != nil { + var anchorValue: String? + if let anchorRange = url.range(of: "#") { + let anchor = url[anchorRange.upperBound...] + if !anchor.isEmpty { + anchorValue = String(anchor) + } } + return .single(.result(.instantView(webpage, anchorValue))) + } else { + return .single(.result(.externalUrl(url))) } - return .single(.instantView(webpage, anchorValue)) } else { - return .single(.externalUrl(url)) + return .complete() } } else { - return .complete() + return .single(.result(.externalUrl(url))) } - } else { - return .single(.externalUrl(url)) } } } diff --git a/submodules/Utils/DarwinDirStat/Package.swift b/submodules/Utils/DarwinDirStat/Package.swift index 4011c1c65b2..7a11c464653 100644 --- a/submodules/Utils/DarwinDirStat/Package.swift +++ b/submodules/Utils/DarwinDirStat/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "DarwinDirStat", - platforms: [.macOS(.v10_12)], + 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/RangeSet/Package.swift b/submodules/Utils/RangeSet/Package.swift index f8494e36082..6a9b31a7c71 100644 --- a/submodules/Utils/RangeSet/Package.swift +++ b/submodules/Utils/RangeSet/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "RangeSet", - platforms: [.macOS(.v10_12)], + 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/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 8a97099ee9e..dc5c7d58d8f 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -514,6 +514,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode overlayNode = current } else { overlayNode = ASDisplayNode() + overlayNode.frame = self.bounds self.overlayNode = overlayNode self.addSubnode(overlayNode) } diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 213927b9608..e4a7ce0c921 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -199,17 +199,17 @@ final class WatchSendMessageHandler: WatchRequestHandler { if args.replyToMid != 0, let peerId = peerId { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.replyToMid) } - messageSignal = .single((.message(text: args.text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) + messageSignal = .single((.message(text: args.text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) } else if let args = subscription as? TGBridgeSendLocationMessageSubscription, let location = args.location { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) - messageSignal = .single((.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: map), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) + messageSignal = .single((.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: map), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) } else if let args = subscription as? TGBridgeSendStickerMessageSubscription { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) messageSignal = mediaForSticker(documentId: args.document.documentId, account: context.account) |> map({ media -> (EnqueueMessage?, PeerId?) in if let media = media { - return (.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId) + return (.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId) } else { return (nil, nil) } @@ -720,7 +720,7 @@ final class WatchAudioHandler: WatchRequestHandler { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMid) } - let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() } }) } else { @@ -742,9 +742,15 @@ final class WatchLocationHandler: WatchRequestHandler { |> mapToSignal({ context -> Signal<[ChatContextResultMessage], NoError> in if let context = context { return context.engine.peers.resolvePeerByName(name: "foursquare") + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> take(1) |> mapToSignal { peer -> Signal in - guard let peer = peer else { + guard let peer = peer?._asPeer() else { return .single(nil) } return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: "", location: .single((args.coordinate.latitude, args.coordinate.longitude)), offset: "") diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift index 50088460bb0..751c1bdeb67 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift @@ -338,7 +338,7 @@ func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, t let (items, focusItem) = galleryItems(account: context.account, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext) - let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName)! + let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false)! model.stickersContext = paintStickersContext controller.model = model model.controller = controller diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 009dcc2d8bd..cbd8e9554bd 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -490,8 +490,11 @@ public final class WebSearchController: ViewController { let context = self.context let contextBot = self.context.engine.peers.resolvePeerByName(name: name) - |> mapToSignal { peer -> Signal in - return .single(peer) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 7c78ea5b634..93500cf246a 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -52,14 +52,17 @@ public class WebAppCancelButtonNode: ASDisplayNode { } private let strings: PresentationStrings + private weak var colorSnapshotView: UIView? + public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { let previousColor = self.color self.color = color - - if case let .animated(duration, curve) = transition, previousColor != color { + + if case let .animated(duration, curve) = transition, previousColor != color, !self.animatingStateChange { if let snapshotView = self.view.snapshotContentTree() { snapshotView.frame = self.bounds self.view.addSubview(snapshotView) + self.colorSnapshotView = snapshotView snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in snapshotView.removeFromSuperview() @@ -125,6 +128,11 @@ public class WebAppCancelButtonNode: ASDisplayNode { } self.state = state + if let colorSnapshotView = self.colorSnapshotView { + self.colorSnapshotView = nil + colorSnapshotView.removeFromSuperview() + } + if animated, let snapshotView = self.buttonNode.view.snapshotContentTree() { self.animatingStateChange = true snapshotView.layer.sublayerTransform = self.buttonNode.subnodeTransform @@ -225,12 +233,9 @@ public struct WebAppParameters { } public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] { - var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb - var secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb - if presentationTheme.list.blocksBackgroundColor.rgb == presentationTheme.list.plainBackgroundColor.rgb { - backgroundColor = presentationTheme.list.modalPlainBackgroundColor.rgb - secondaryBackgroundColor = presentationTheme.list.plainBackgroundColor.rgb - } + let backgroundColor = presentationTheme.list.plainBackgroundColor.rgb + let secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb + return [ "bg_color": Int32(bitPattern: backgroundColor), "secondary_bg_color": Int32(bitPattern: secondaryBackgroundColor), @@ -238,7 +243,13 @@ public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> "hint_color": Int32(bitPattern: presentationTheme.list.itemSecondaryTextColor.rgb), "link_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb), "button_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.fillColor.rgb), - "button_text_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.foregroundColor.rgb) + "button_text_color": Int32(bitPattern: presentationTheme.list.itemCheckColors.foregroundColor.rgb), + "header_bg_color": Int32(bitPattern: presentationTheme.rootController.navigationBar.opaqueBackgroundColor.rgb), + "accent_text_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb), + "section_bg_color": Int32(bitPattern: presentationTheme.list.itemBlocksBackgroundColor.rgb), + "section_header_text_color": Int32(bitPattern: presentationTheme.list.freeTextColor.rgb), + "subtitle_text_color": Int32(bitPattern: presentationTheme.list.itemSecondaryTextColor.rgb), + "destructive_text_color": Int32(bitPattern: presentationTheme.list.itemDestructiveColor.rgb) ] } @@ -295,13 +306,13 @@ public final class WebAppController: ViewController, AttachmentContainable { self.topOverscrollNode = ASDisplayNode() super.init() - + if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 { self.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor } else { self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor } - + let webView = WebAppWebView() webView.alpha = 0.0 webView.navigationDelegate = self @@ -768,7 +779,8 @@ public final class WebAppController: ViewController, AttachmentContainable { case "web_app_open_tg_link": if let json = json, let path = json["path_full"] as? String { controller.openUrl("https://t.me\(path)", false, { [weak controller] in - controller?.dismiss() + let _ = controller +// controller?.dismiss() }) } case "web_app_open_invoice": @@ -808,6 +820,12 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.lastTouchTimestamp = nil if tryInstantView { let _ = (resolveInstantViewUrl(account: self.context.account, url: url) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return @@ -968,13 +986,28 @@ public final class WebAppController: ViewController, AttachmentContainable { } case "web_app_read_text_from_clipboard": if let json = json, let requestId = json["req_id"] as? String { - let currentTimestamp = CACurrentMediaTime() - var fillData = false - if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0, self.controller?.url == nil { - self.webView?.lastTouchTimestamp = nil - fillData = true - } - self.sendClipboardTextEvent(requestId: requestId, fillData: fillData) + let botId = controller.botId + let isAttachMenu = controller.url == nil + + let _ = (self.context.engine.messages.attachMenuBots() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] attachMenuBots in + guard let self else { + return + } + let currentTimestamp = CACurrentMediaTime() + var fillData = false + + let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) + if isAttachMenu || attachMenuBot != nil { + if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { + self.webView?.lastTouchTimestamp = nil + fillData = true + } + } + + self.sendClipboardTextEvent(requestId: requestId, fillData: fillData) + }) } case "web_app_request_write_access": self.requestWriteAccess() @@ -990,6 +1023,10 @@ public final class WebAppController: ViewController, AttachmentContainable { } self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}") } + case "web_app_setup_settings_button": + if let json = json, let isVisible = json["is_visible"] as? Bool { + self.controller?.hasSettings = isVisible + } default: break } @@ -1009,12 +1046,8 @@ public final class WebAppController: ViewController, AttachmentContainable { let color: UIColor? var primaryTextColor: UIColor? var secondaryTextColor: UIColor? - var backgroundColor = self.presentationData.theme.list.plainBackgroundColor - var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor - if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb { - backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor - secondaryBackgroundColor = self.presentationData.theme.list.plainBackgroundColor - } + let backgroundColor = self.presentationData.theme.list.plainBackgroundColor + let secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor if let headerColor = self.headerColor { color = headerColor let textColor = headerColor.lightness > 0.5 ? UIColor(rgb: 0x000000) : UIColor(rgb: 0xffffff) @@ -1258,7 +1291,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [ - .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) ]) |> mapToSignal { messageIds in if let maybeMessageId = messageIds.first, let messageId = maybeMessageId { @@ -1342,6 +1375,8 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? private var presentationDataDisposable: Disposable? + private var hasSettings = false + public var openUrl: (String, Bool, @escaping () -> Void) -> Void = { _, _, _ in } public var getNavigationController: () -> NavigationController? = { return nil } public var completion: () -> Void = {} @@ -1363,7 +1398,11 @@ public final class WebAppController: ViewController, AttachmentContainable { self.threadId = threadId self.updatedPresentationData = updatedPresentationData - self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + var presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + self.presentationData = presentationData self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) @@ -1399,6 +1438,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { + let updatedTheme = presentationData.theme.withModalBlocksBackground() + let presentationData = presentationData.withUpdated(theme: updatedTheme) strongSelf.presentationData = presentationData strongSelf.updateNavigationBarTheme(transition: .immediate) @@ -1469,11 +1510,11 @@ public final class WebAppController: ViewController, AttachmentContainable { let peerId = self.peerId let botId = self.botId - let url = self.url - let forceHasSettings = self.forceHasSettings let source = self.source + let hasSettings = self.hasSettings + let items = context.engine.messages.attachMenuBots() |> take(1) |> map { [weak self] attachMenuBots -> ContextController.Items in @@ -1481,17 +1522,6 @@ public final class WebAppController: ViewController, AttachmentContainable { let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) - let hasSettings: Bool - if url == nil { - if forceHasSettings { - hasSettings = true - } else { - hasSettings = attachMenuBot?.flags.contains(.hasSettings) == true - } - } else { - hasSettings = forceHasSettings - } - if hasSettings { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Settings, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index e0eeb0e35ff..26a8eca6218 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -88,9 +88,9 @@ final class WebAppWebView: WKWebView { return point.x > 30.0 } self.allowsBackForwardNavigationGestures = false -// if #available(iOS 16.4, *) { -// self.isInspectable = true -// } + if #available(iOS 16.4, *) { + self.isInspectable = true + } handleScriptMessageImpl = { [weak self] message in if let strongSelf = self { diff --git a/submodules/YuvConversion/Package.swift b/submodules/YuvConversion/Package.swift index d3a43056cf1..f6bb29070b9 100644 --- a/submodules/YuvConversion/Package.swift +++ b/submodules/YuvConversion/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "YuvConversion", - platforms: [.macOS(.v10_12)], + 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/libphonenumber/Package.swift b/submodules/libphonenumber/Package.swift index 0d64d78ced0..2d7b64648fb 100644 --- a/submodules/libphonenumber/Package.swift +++ b/submodules/libphonenumber/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "libphonenumber", - platforms: [.macOS(.v10_12)], + 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/sqlcipher/Package.swift b/submodules/sqlcipher/Package.swift index b6c02557fbc..0b760023aa2 100644 --- a/submodules/sqlcipher/Package.swift +++ b/submodules/sqlcipher/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "sqlcipher", - platforms: [.macOS(.v10_12)], + 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/swift_deps.bzl b/swift_deps.bzl index 259cf5b433c..6cdd5dd7973 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 = "6ac92187e0dac03bffc786d6bf8378169ed425aa", + commit = "42ed26bd295a28a8a800f54583af10f4acdaaf34", dependencies_index = "@//:swift_deps_index.json", remote = "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", ) @@ -44,7 +44,7 @@ def swift_dependencies(): # version: 7.3.2 swift_package( name = "swiftpkg_r.swift", - commit = "77c0d9c202b9ac83c1992111b76b3fb10468015b", + commit = "4a0f8c97f1baa27d165dc801982c55bbf51126e5", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/denis15yo/R.swift.git", ) @@ -52,7 +52,7 @@ def swift_dependencies(): # version: 5.15.5 swift_package( name = "swiftpkg_sdwebimage", - commit = "1f06ef5007b6a580b3873ed2adee19e05d3b215a", + commit = "fd1950de05a5ad77cb252fd88576c1e1809ee50d", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/SDWebImage/SDWebImage.git", ) diff --git a/swift_deps_index.json b/swift_deps_index.json index 01d134b4644..a66ca1df0f9 100644 --- a/swift_deps_index.json +++ b/swift_deps_index.json @@ -727,7 +727,7 @@ "name": "swiftpkg_nicegram_assistant_ios", "identity": "nicegram-assistant-ios", "remote": { - "commit": "6ac92187e0dac03bffc786d6bf8378169ed425aa", + "commit": "42ed26bd295a28a8a800f54583af10f4acdaaf34", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "branch": "develop" } @@ -736,7 +736,7 @@ "name": "swiftpkg_r.swift", "identity": "r.swift", "remote": { - "commit": "77c0d9c202b9ac83c1992111b76b3fb10468015b", + "commit": "4a0f8c97f1baa27d165dc801982c55bbf51126e5", "remote": "https://github.com/denis15yo/R.swift.git", "branch": "main" } @@ -745,9 +745,9 @@ "name": "swiftpkg_sdwebimage", "identity": "sdwebimage", "remote": { - "commit": "1f06ef5007b6a580b3873ed2adee19e05d3b215a", + "commit": "fd1950de05a5ad77cb252fd88576c1e1809ee50d", "remote": "https://github.com/SDWebImage/SDWebImage.git", - "version": "5.18.3" + "version": "5.18.4" } }, { diff --git a/third-party/libjxl/build-libjxl-bazel.sh b/third-party/libjxl/build-libjxl-bazel.sh index 4a821d3c8fb..993dbf929f5 100755 --- a/third-party/libjxl/build-libjxl-bazel.sh +++ b/third-party/libjxl/build-libjxl-bazel.sh @@ -15,7 +15,7 @@ if [ "$ARCH" = "arm64" ]; then IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneOS.platform" IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk) export CFLAGS="-Wall -arch arm64 -miphoneos-version-min=12.0 -funwind-tables" - export CXXFLAGS="-Wall -arch arm64 -miphoneos-version-min=12.0 -funwind-tables" + export CXXFLAGS="$CFLAGS" cd "$BUILD_DIR" mkdir build @@ -32,8 +32,8 @@ elif [ "$ARCH" = "sim_arm64" ]; then IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform" IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk) export CFLAGS="-Wall -arch arm64 --target=arm64-apple-ios12.0-simulator -miphonesimulator-version-min=12.0 -funwind-tables" - export CXXFLAGS="-Wall -arch arm64 --target=arm64-apple-ios12.0-simulator -miphonesimulator-version-min=12.0 -funwind-tables" - + export CXXFLAGS="$CFLAGS" + cd "$BUILD_DIR" mkdir build cd build @@ -49,8 +49,8 @@ elif [ "$ARCH" = "x86_64" ]; then IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform" IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk) export CFLAGS="-Wall -arch x86_64 -miphoneos-version-min=12.0 -funwind-tables" - export CXXFLAGS="-Wall -arch x86_64 -miphoneos-version-min=12.0 -funwind-tables" - + export CXXFLAGS="$CFLAGS" + cd "$BUILD_DIR" mkdir build cd build diff --git a/third-party/mozjpeg/mozjpeg/.github/pull_request_template.md b/third-party/mozjpeg/mozjpeg/.github/pull_request_template.md new file mode 100644 index 00000000000..f1511657f1c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/.github/pull_request_template.md @@ -0,0 +1,8 @@ +**Complete description of the bug fix or feature that this pull request implements** + + +**Checklist before submitting the pull request, to maximize the chances that the pull request will be accepted** + +- [ ] Read CONTRIBUTING.md, a link to which appears under "Helpful resources" below. That document discusses general guidelines for contributing to libjpeg-turbo, as well as the types of contributions that will not be accepted or are unlikely to be accepted. +- [ ] Search the existing issues and pull requests (both open and closed) to ensure that a similar request has not already been submitted and rejected. +- [ ] Discuss the proposed bug fix or feature in a GitHub issue, through direct e-mail with the project maintainer, or on the libjpeg-turbo-devel mailing list. diff --git a/third-party/mozjpeg/mozjpeg/BUILDING.md b/third-party/mozjpeg/mozjpeg/BUILDING.md index 4468ea55c07..9e0d7150f86 100644 --- a/third-party/mozjpeg/mozjpeg/BUILDING.md +++ b/third-party/mozjpeg/mozjpeg/BUILDING.md @@ -10,38 +10,24 @@ Build Requirements - [CMake](http://www.cmake.org) v2.8.12 or later -- [NASM](http://www.nasm.us) or [YASM](http://yasm.tortall.net) +- [NASM](http://www.nasm.us) or [Yasm](http://yasm.tortall.net) (if building x86 or x86-64 SIMD extensions) - * If using NASM, 2.10 or later is required. - * If using NASM, 2.10 or later (except 2.11.08) is required for an x86-64 Mac - build (2.11.08 does not work properly with libjpeg-turbo's x86-64 SIMD code - when building macho64 objects.) - * If using YASM, 1.2.0 or later is required. - * If building on macOS, NASM or YASM can be obtained from + * If using NASM, 2.13 or later is required. + * If using Yasm, 1.2.0 or later is required. + * If building on macOS, NASM or Yasm can be obtained from [MacPorts](http://www.macports.org/) or [Homebrew](http://brew.sh/). - NOTE: Currently, if it is desirable to hide the SIMD function symbols in Mac executables or shared libraries that statically link with - libjpeg-turbo, then NASM 2.14 or later or YASM must be used when + libjpeg-turbo, then NASM 2.14 or later or Yasm must be used when building libjpeg-turbo. - * If building on Windows, **nasm.exe**/**yasm.exe** should be in your `PATH`. - * NASM and YASM are located in the CRB (Code Ready Builder) repository on - Red Hat Enterprise Linux 8 and in the PowerTools repository on CentOS 8, - which is not enabled by default. - - The binary RPMs released by the NASM project do not work on older Linux - systems, such as Red Hat Enterprise Linux 5. On such systems, you can easily - build and install NASM from a source RPM by downloading one of the SRPMs from - - - - and executing the following as root: - - ARCH=`uname -m` - rpmbuild --rebuild nasm-{version}.src.rpm - rpm -Uvh /usr/src/redhat/RPMS/$ARCH/nasm-{version}.$ARCH.rpm - - NOTE: the NASM build will fail if texinfo is not installed. - + * If NASM or Yasm is not in your `PATH`, then you can specify the full path + to the assembler by using either the `CMAKE_ASM_NASM_COMPILER` CMake + variable or the `ASM_NASM` environment variable. On Windows, use forward + slashes rather than backslashes in the path (for example, + **c:/nasm/nasm.exe**). + * NASM and Yasm are located in the CRB (Code Ready Builder) or PowerTools + repository on Red Hat Enterprise Linux 8+ and derivatives, which is not + enabled by default. ### Un*x Platforms (including Linux, Mac, FreeBSD, Solaris, and Cygwin) @@ -49,10 +35,8 @@ Build Requirements - If building the TurboJPEG Java wrapper, JDK or OpenJDK 1.5 or later is required. Most modern Linux distributions, as well as Solaris 10 and later, - include JDK or OpenJDK. On OS X 10.5 and 10.6, it will be necessary to - install the Java Developer Package, which can be downloaded from - (Apple ID required.) For other - systems, you can obtain the Oracle Java Development Kit from + include JDK or OpenJDK. For other systems, you can obtain the Oracle Java + Development Kit from . * If using JDK 11 or later, CMake 3.10.x or later must also be used. @@ -62,25 +46,43 @@ Build Requirements - Microsoft Visual C++ 2005 or later If you don't already have Visual C++, then the easiest way to get it is by - installing the - [Windows SDK](http://msdn.microsoft.com/en-us/windows/bb980924.aspx). - The Windows SDK includes both 32-bit and 64-bit Visual C++ compilers and - everything necessary to build libjpeg-turbo. - - * You can also use Microsoft Visual Studio Express/Community Edition, which - is a free download. (NOTE: versions prior to 2012 can only be used to - build 32-bit code.) + installing + [Visual Studio Community Edition](https://visualstudio.microsoft.com). + + * You can also download and install the standalone Windows SDK (for Windows 7 + or later), which includes command-line versions of the 32-bit and 64-bit + Visual C++ compilers. * If you intend to build libjpeg-turbo from the command line, then add the appropriate compiler and SDK directories to the `INCLUDE`, `LIB`, and `PATH` environment variables. This is generally accomplished by - executing `vcvars32.bat` or `vcvars64.bat` and `SetEnv.cmd`. - `vcvars32.bat` and `vcvars64.bat` are part of Visual C++ and are located in - the same directory as the compiler. `SetEnv.cmd` is part of the Windows - SDK. You can pass optional arguments to `SetEnv.cmd` to specify a 32-bit - or 64-bit build environment. + executing `vcvars32.bat` or `vcvars64.bat`, which are located in the same + directory as the compiler. + * If built with Visual C++ 2015 or later, the libjpeg-turbo static libraries + cannot be used with earlier versions of Visual C++, and vice versa. + * The libjpeg API DLL (**jpeg{version}.dll**) will depend on the C run-time + DLLs corresponding to the version of Visual C++ that was used to build it. + +- Vcpkg + + You need to download and install libpng using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager: + + git clone https://github.com/Microsoft/vcpkg.git + cd vcpkg + ./bootstrap-vcpkg.sh + ./vcpkg integrate install + vcpkg install libpng:x64-windows + vcpkg install libpng:x64-windows-static + + Actually, you can just download and install MozJPEG using vcpkg dependency manager: + + vcpkg install mozjpeg + + The mozjpeg port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository. + ... OR ... + - MinGW [MSYS2](http://msys2.github.io/) or [tdm-gcc](http://tdm-gcc.tdragon.net/) @@ -94,17 +96,13 @@ Build Requirements * If using JDK 11 or later, CMake 3.10.x or later must also be used. -- Vcpkg - - You can download and install mozjpeg using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager: - git clone https://github.com/Microsoft/vcpkg.git - cd vcpkg - ./bootstrap-vcpkg.sh - ./vcpkg integrate install - vcpkg install mozjpeg - - The mozjpeg port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository. +Sub-Project Builds +------------------ + +The libjpeg-turbo build system does not support being included as a sub-project +using the CMake `add_subdirectory()` function. Use the CMake +`ExternalProject_Add()` function instead. Out-of-Tree Builds @@ -120,6 +118,14 @@ directory, whereas *{source_directory}* refers to the libjpeg-turbo source directory. For in-tree builds, these directories are the same. +Ninja +----- + +If using Ninja, then replace `make` or `nmake` with `ninja`, and replace the +CMake generator (specified with the `-G` option) with `Ninja`, in all of the +procedures and recipes below. + + Build Procedure --------------- @@ -345,7 +351,7 @@ Build Recipes ------------- -### 32-bit Build on 64-bit Linux/Unix/Mac +### 32-bit Build on 64-bit Linux/Unix Use export/setenv to set the following environment variables before running CMake: @@ -384,9 +390,13 @@ located (usually **/usr/bin**.) Next, execute the following commands: cd {build_directory} cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ + -DCMAKE_INSTALL_PREFIX={install_path} \ [additional CMake flags] {source_directory} make +*{install\_path}* is the path under which the libjpeg-turbo binaries should be +installed. + ### 64-bit MinGW Build on Un*x (including Mac and Cygwin) @@ -403,124 +413,34 @@ located (usually **/usr/bin**.) Next, execute the following commands: cd {build_directory} cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ + -DCMAKE_INSTALL_PREFIX={install_path} \ [additional CMake flags] {source_directory} make +*{install\_path}* is the path under which the libjpeg-turbo binaries should be +installed. + Building libjpeg-turbo for iOS ------------------------------ -iOS platforms, such as the iPhone and iPad, use ARM processors, and all -currently supported models include NEON instructions. Thus, they can take +iOS platforms, such as the iPhone and iPad, use Arm processors, and all +currently supported models include Neon instructions. Thus, they can take advantage of libjpeg-turbo's SIMD extensions to significantly accelerate JPEG compression/decompression. This section describes how to build libjpeg-turbo for these platforms. -### Additional build requirements - -- For configurations that require [gas-preprocessor.pl] - (https://raw.githubusercontent.com/libjpeg-turbo/gas-preprocessor/master/gas-preprocessor.pl), - it should be installed in your `PATH`. - - -### ARMv7 (32-bit) +### Armv8 (64-bit) -**gas-preprocessor.pl required** - -The following scripts demonstrate how to build libjpeg-turbo to run on the -iPhone 3GS-4S/iPad 1st-3rd Generation and newer: - -#### Xcode 4.2 and earlier (LLVM-GCC) - - IOS_PLATFORMDIR=/Developer/Platforms/iPhoneOS.platform - IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk) - export CFLAGS="-mfloat-abi=softfp -march=armv7 -mcpu=cortex-a8 -mtune=cortex-a8 -mfpu=neon -miphoneos-version-min=3.0" - - cd {build_directory} - - cat <toolchain.cmake - set(CMAKE_SYSTEM_NAME Darwin) - set(CMAKE_SYSTEM_PROCESSOR arm) - set(CMAKE_C_COMPILER ${IOS_PLATFORMDIR}/Developer/usr/bin/arm-apple-darwin10-llvm-gcc-4.2) - EOF - - cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ - -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} \ - [additional CMake flags] {source_directory} - make - -#### Xcode 4.3-4.6 (LLVM-GCC) - -Same as above, but replace the first line with: - - IOS_PLATFORMDIR=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform - -#### Xcode 5 and later (Clang) - - IOS_PLATFORMDIR=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform - IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk) - export CFLAGS="-mfloat-abi=softfp -arch armv7 -miphoneos-version-min=3.0" - export ASMFLAGS="-no-integrated-as" - - cd {build_directory} - - cat <toolchain.cmake - set(CMAKE_SYSTEM_NAME Darwin) - set(CMAKE_SYSTEM_PROCESSOR arm) - set(CMAKE_C_COMPILER /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang) - EOF - - cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ - -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} \ - [additional CMake flags] {source_directory} - make - - -### ARMv7s (32-bit) - -**gas-preprocessor.pl required** - -The following scripts demonstrate how to build libjpeg-turbo to run on the -iPhone 5/iPad 4th Generation and newer: - -#### Xcode 4.5-4.6 (LLVM-GCC) - - IOS_PLATFORMDIR=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform - IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk) - export CFLAGS="-Wall -mfloat-abi=softfp -march=armv7s -mcpu=swift -mtune=swift -mfpu=neon -miphoneos-version-min=6.0" - - cd {build_directory} - - cat <toolchain.cmake - set(CMAKE_SYSTEM_NAME Darwin) - set(CMAKE_SYSTEM_PROCESSOR arm) - set(CMAKE_C_COMPILER ${IOS_PLATFORMDIR}/Developer/usr/bin/arm-apple-darwin10-llvm-gcc-4.2) - EOF - - cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ - -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} \ - [additional CMake flags] {source_directory} - make - -#### Xcode 5 and later (Clang) - -Same as the ARMv7 build procedure for Xcode 5 and later, except replace the -compiler flags as follows: - - export CFLAGS="-Wall -mfloat-abi=softfp -arch armv7s -miphoneos-version-min=6.0" - - -### ARMv8 (64-bit) - -**gas-preprocessor.pl required if using Xcode < 6** +**Xcode 5 or later required, Xcode 6.3.x or later recommended** The following script demonstrates how to build libjpeg-turbo to run on the iPhone 5S/iPad Mini 2/iPad Air and newer. IOS_PLATFORMDIR=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk) - export CFLAGS="-Wall -arch arm64 -miphoneos-version-min=7.0 -funwind-tables" + export CFLAGS="-Wall -arch arm64 -miphoneos-version-min=8.0 -funwind-tables" cd {build_directory} @@ -535,8 +455,9 @@ iPhone 5S/iPad Mini 2/iPad Air and newer. [additional CMake flags] {source_directory} make -Once built, lipo can be used to combine the ARMv7, v7s, and/or v8 variants into -a universal library. +Replace `iPhoneOS` with `iPhoneSimulator` and `-miphoneos-version-min` with +`-miphonesimulator-version-min` to build libjpeg-turbo for the iOS simulator on +Macs with Apple silicon CPUs. Building libjpeg-turbo for Android @@ -546,7 +467,9 @@ Building libjpeg-turbo for Android platforms requires v13b or later of the [Android NDK](https://developer.android.com/tools/sdk/ndk). -### ARMv7 (32-bit) +### Armv7 (32-bit) + +**NDK r19 or later with Clang recommended** The following is a general recipe script that can be modified for your specific needs. @@ -571,7 +494,9 @@ needs. make -### ARMv8 (64-bit) +### Armv8 (64-bit) + +**Clang recommended** The following is a general recipe script that can be modified for your specific needs. @@ -747,44 +672,23 @@ Mac make dmg Create Mac package/disk image. This requires pkgbuild and productbuild, which -are installed by default on OS X 10.7 and later and which can be obtained by -installing Xcode 3.2.6 (with the "Unix Development" option) on OS X 10.6. -Packages built in this manner can be installed on OS X 10.5 and later, but they -must be built on OS X 10.6 or later. - - make udmg - -This creates a Mac package/disk image that contains universal x86-64/i386/ARM -binaries. The following CMake variables control which architectures are -included in the universal binaries. Setting any of these variables to an empty -string excludes that architecture from the package. - -* `OSX_32BIT_BUILD`: Directory containing an i386 (32-bit) Mac build of - libjpeg-turbo (default: *{source_directory}*/osxx86) -* `IOS_ARMV7_BUILD`: Directory containing an ARMv7 (32-bit) iOS build of - libjpeg-turbo (default: *{source_directory}*/iosarmv7) -* `IOS_ARMV7S_BUILD`: Directory containing an ARMv7s (32-bit) iOS build of - libjpeg-turbo (default: *{source_directory}*/iosarmv7s) -* `IOS_ARMV8_BUILD`: Directory containing an ARMv8 (64-bit) iOS build of - libjpeg-turbo (default: *{source_directory}*/iosarmv8) - -You should first use CMake to configure i386, ARMv7, ARMv7s, and/or ARMv8 -sub-builds of libjpeg-turbo (see "Build Recipes" and "Building libjpeg-turbo -for iOS" above) in build directories that match those specified in the -aforementioned CMake variables. Next, configure the primary build of -libjpeg-turbo as an out-of-tree build, and build it. Once the primary build -has been built, run `make udmg` from the build directory. The packaging system -will build the sub-builds, use lipo to combine them into a single set of -universal binaries, then package the universal binaries in the same manner as -`make dmg`. - - -Cygwin ------- - - make cygwinpkg - -Build a Cygwin binary package. +are installed by default on OS X/macOS 10.7 and later. + +In order to create a Mac package/disk image that contains universal +x86-64/Arm binaries, set the following CMake variable: + +* `ARMV8_BUILD`: Directory containing an Armv8 (64-bit) iOS or macOS build of + libjpeg-turbo to include in the universal binaries + +You should first use CMake to configure an Armv8 sub-build of libjpeg-turbo +(see "Building libjpeg-turbo for iOS" above, if applicable) in a build +directory that matches the one specified in the aforementioned CMake variable. +Next, configure the primary (x86-64) build of libjpeg-turbo as an out-of-tree +build, specifying the aforementioned CMake variable, and build it. Once the +primary build has been built, run `make dmg` from the build directory. The +packaging system will build the sub-build, use lipo to combine it with the +primary build into a single set of universal binaries, then package the +universal binaries. Windows diff --git a/third-party/mozjpeg/mozjpeg/CMakeLists.txt b/third-party/mozjpeg/mozjpeg/CMakeLists.txt index 51f4714bea3..ea21a2c1e69 100644 --- a/third-party/mozjpeg/mozjpeg/CMakeLists.txt +++ b/third-party/mozjpeg/mozjpeg/CMakeLists.txt @@ -1,11 +1,17 @@ cmake_minimum_required(VERSION 2.8.12) +# When using CMake 3.4 and later, don't export symbols from executables unless +# the CMAKE_ENABLE_EXPORTS variable is set. +if(POLICY CMP0065) + cmake_policy(SET CMP0065 NEW) +endif() if(CMAKE_EXECUTABLE_SUFFIX) set(CMAKE_EXECUTABLE_SUFFIX_TMP ${CMAKE_EXECUTABLE_SUFFIX}) endif() project(mozjpeg C) -set(VERSION 4.0.0) +set(VERSION 4.1.4) +set(COPYRIGHT_YEAR "1991-2023") string(REPLACE "." ";" VERSION_TRIPLET ${VERSION}) list(GET VERSION_TRIPLET 0 VERSION_MAJOR) list(GET VERSION_TRIPLET 1 VERSION_MINOR) @@ -25,6 +31,52 @@ pad_number(VERSION_MINOR 3) pad_number(VERSION_REVISION 3) set(LIBJPEG_TURBO_VERSION_NUMBER ${VERSION_MAJOR}${VERSION_MINOR}${VERSION_REVISION}) +# The libjpeg-turbo build system has never supported and will never support +# being integrated into another build system using add_subdirectory(), because +# doing so would require that we (minimally): +# +# 1. avoid using certain CMake variables, such as CMAKE_SOURCE_DIR, +# CMAKE_BINARY_DIR, and CMAKE_PROJECT_NAME; +# 2. avoid using implicit include directories and relative paths; +# 3. optionally provide a way to skip the installation of libjpeg-turbo +# components when the 'install' target is built; +# 4. optionally provide a way to postfix target names, to avoid namespace +# conflicts; +# 5. restructure the top-level CMakeLists.txt so that it properly sets the +# PROJECT_VERSION variable; and +# 6. design automated regression tests to ensure that new commits don't break +# any of the above. +# +# Even if we did all of that, issues would still arise, because it is +# impossible for an upstream build system to anticipate the widely varying +# needs of every downstream build system. That's why the CMake +# ExternalProject_Add() function exists. Downstream projects that wish to +# integrate libjpeg-turbo as a subdirectory should either use +# ExternalProject_Add() or make downstream modifications to the libjpeg-turbo +# build system to suit their specific needs. Please do not file bug reports, +# feature requests, or pull requests regarding this. +if(NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + message(FATAL_ERROR "The libjpeg-turbo build system cannot be integrated into another build system using add_subdirectory(). Use ExternalProject_Add() instead.") +endif() + +# CMake 3.14 and later sets CMAKE_MACOSX_BUNDLE to TRUE by default when +# CMAKE_SYSTEM_NAME is iOS, tvOS, or watchOS, which breaks the libjpeg-turbo +# build. (Specifically, when CMAKE_MACOSX_BUNDLE is TRUE, executables for +# Apple platforms are built as application bundles, which causes CMake to +# complain that our install() directives for executables do not specify a +# BUNDLE DESTINATION. Even if CMake did not complain, building executables as +# application bundles would break our iOS packages.) +set(CMAKE_MACOSX_BUNDLE FALSE) + +get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY + GENERATOR_IS_MULTI_CONFIG) +# If the GENERATOR_IS_MULTI_CONFIG property doesn't exist (CMake < 3.9), then +# set the GENERATOR_IS_MULTI_CONFIG variable manually if the generator is +# Visual Studio or Xcode (the only multi-config generators in CMake < 3.9). +if(NOT GENERATOR_IS_MULTI_CONFIG AND (MSVC_IDE OR XCODE)) + set(GENERATOR_IS_MULTI_CONFIG TRUE) +endif() + string(TIMESTAMP DEFAULT_BUILD "%Y%m%d") set(BUILD ${DEFAULT_BUILD} CACHE STRING "Build string (default: ${DEFAULT_BUILD})") @@ -38,15 +90,24 @@ message(STATUS "CMAKE_BUILD_TYPE = ${CMAKE_BUILD_TYPE}") message(STATUS "VERSION = ${VERSION}, BUILD = ${BUILD}") +include(cmakescripts/PackageInfo.cmake) + # Detect CPU type and whether we're building 64-bit or 32-bit code math(EXPR BITS "${CMAKE_SIZEOF_VOID_P} * 8") string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} CMAKE_SYSTEM_PROCESSOR_LC) +set(COUNT 1) +foreach(ARCH ${CMAKE_OSX_ARCHITECTURES}) + if(COUNT GREATER 1) + message(FATAL_ERROR "libjpeg-turbo contains assembly code, so it cannot be built with multiple values in CMAKE_OSX_ARCHITECTURES.") + endif() + math(EXPR COUNT "${COUNT}+1") +endforeach() if(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "x86_64" OR CMAKE_SYSTEM_PROCESSOR_LC MATCHES "amd64" OR CMAKE_SYSTEM_PROCESSOR_LC MATCHES "i[0-9]86" OR CMAKE_SYSTEM_PROCESSOR_LC MATCHES "x86" OR CMAKE_SYSTEM_PROCESSOR_LC MATCHES "ia32") - if(BITS EQUAL 64) + if(BITS EQUAL 64 OR CMAKE_C_COMPILER_ABI MATCHES "ELF X32") set(CPU_TYPE x86_64) else() set(CPU_TYPE i386) @@ -55,16 +116,30 @@ if(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "x86_64" OR set(CMAKE_SYSTEM_PROCESSOR ${CPU_TYPE}) endif() elseif(CMAKE_SYSTEM_PROCESSOR_LC STREQUAL "aarch64" OR - CMAKE_SYSTEM_PROCESSOR_LC MATCHES "arm*64*") - set(CPU_TYPE arm64) -elseif(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "arm*") - set(CPU_TYPE arm) -elseif(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "ppc*" OR - CMAKE_SYSTEM_PROCESSOR_LC MATCHES "powerpc*") + CMAKE_SYSTEM_PROCESSOR_LC MATCHES "^arm") + if(BITS EQUAL 64) + set(CPU_TYPE arm64) + else() + set(CPU_TYPE arm) + endif() +elseif(CMAKE_SYSTEM_PROCESSOR_LC MATCHES "^ppc" OR + CMAKE_SYSTEM_PROCESSOR_LC MATCHES "^powerpc") set(CPU_TYPE powerpc) else() set(CPU_TYPE ${CMAKE_SYSTEM_PROCESSOR_LC}) endif() +if(CMAKE_OSX_ARCHITECTURES MATCHES "x86_64" OR + CMAKE_OSX_ARCHITECTURES MATCHES "arm64" OR + CMAKE_OSX_ARCHITECTURES MATCHES "i386") + set(CPU_TYPE ${CMAKE_OSX_ARCHITECTURES}) +endif() +if(CMAKE_OSX_ARCHITECTURES MATCHES "ppc") + set(CPU_TYPE powerpc) +endif() +if(MSVC_IDE AND CMAKE_GENERATOR_PLATFORM MATCHES "arm64") + set(CPU_TYPE arm64) +endif() + message(STATUS "${BITS}-bit build (${CPU_TYPE})") @@ -82,7 +157,9 @@ if(WIN32) set(CMAKE_INSTALL_DEFAULT_PREFIX "${CMAKE_INSTALL_DEFAULT_PREFIX}64") endif() else() - set(CMAKE_INSTALL_DEFAULT_PREFIX /opt/${CMAKE_PROJECT_NAME}) + if(NOT CMAKE_INSTALL_DEFAULT_PREFIX) + set(CMAKE_INSTALL_DEFAULT_PREFIX /opt/${CMAKE_PROJECT_NAME}) + endif() endif() if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_DEFAULT_PREFIX}" CACHE PATH @@ -101,6 +178,8 @@ if(CMAKE_INSTALL_PREFIX STREQUAL "${CMAKE_INSTALL_DEFAULT_PREFIX}") if(UNIX AND NOT APPLE) if(BITS EQUAL 64) set(CMAKE_INSTALL_DEFAULT_LIBDIR "lib64") + elseif(CMAKE_C_COMPILER_ABI MATCHES "ELF X32") + set(CMAKE_INSTALL_DEFAULT_LIBDIR "libx32") else() set(CMAKE_INSTALL_DEFAULT_LIBDIR "lib32") endif() @@ -133,9 +212,9 @@ endforeach() macro(boolean_number var) if(${var}) - set(${var} 1) + set(${var} 1 ${ARGN}) else() - set(${var} 0) + set(${var} 0 ${ARGN}) endif() endmacro() @@ -153,8 +232,12 @@ option(WITH_ARITH_DEC "Include arithmetic decoding support when emulating the li boolean_number(WITH_ARITH_DEC) option(WITH_ARITH_ENC "Include arithmetic encoding support when emulating the libjpeg v6b API/ABI" FALSE) boolean_number(WITH_ARITH_ENC) -option(WITH_JAVA "Build Java wrapper for the TurboJPEG API library (implies ENABLE_SHARED=1)" FALSE) -boolean_number(WITH_JAVA) +if(CMAKE_C_COMPILER_ABI MATCHES "ELF X32") + set(WITH_JAVA 0) +else() + option(WITH_JAVA "Build Java wrapper for the TurboJPEG API library (implies ENABLE_SHARED=1)" FALSE) + boolean_number(WITH_JAVA) +endif() option(WITH_JPEG7 "Emulate libjpeg v7 API/ABI (this makes ${CMAKE_PROJECT_NAME} backward-incompatible with libjpeg v6b)" FALSE) boolean_number(WITH_JPEG7) option(WITH_JPEG8 "Emulate libjpeg v8 API/ABI (this makes ${CMAKE_PROJECT_NAME} backward-incompatible with libjpeg v6b)" FALSE) @@ -165,6 +248,7 @@ option(WITH_SIMD "Include SIMD extensions, if available for this platform" TRUE) boolean_number(WITH_SIMD) option(WITH_TURBOJPEG "Include the TurboJPEG API library and associated test programs" TRUE) boolean_number(WITH_TURBOJPEG) +option(WITH_FUZZ "Build fuzz targets" FALSE) macro(report_option var desc) if(${var}) @@ -195,6 +279,14 @@ if(ENABLE_SHARED) set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}) endif() +if(WITH_JPEG8 OR WITH_JPEG7) + set(WITH_ARITH_ENC 1) + set(WITH_ARITH_DEC 1) +endif() +if(WITH_JPEG8) + set(WITH_MEM_SRCDST 0) +endif() + if(WITH_12BIT) set(WITH_ARITH_DEC 0) set(WITH_ARITH_ENC 0) @@ -207,14 +299,6 @@ else() endif() report_option(WITH_12BIT "12-bit JPEG support") -if(WITH_JPEG8 OR WITH_JPEG7) - set(WITH_ARITH_ENC 1) - set(WITH_ARITH_DEC 1) -endif() -if(WITH_JPEG8) - set(WITH_MEM_SRCDST 0) -endif() - if(WITH_ARITH_DEC) set(D_ARITH_CODING_SUPPORTED 1) endif() @@ -242,6 +326,16 @@ if(NOT WITH_JPEG8) report_option(WITH_MEM_SRCDST "In-memory source/destination managers") endif() +# 0: Original libjpeg v6b/v7/v8 API/ABI +# +# libjpeg v6b/v7 API/ABI emulation: +# 1: + In-memory source/destination managers (libjpeg-turbo 1.3.x) +# 2: + Partial image decompression functions (libjpeg-turbo 1.5.x) +# 3: + ICC functions (libjpeg-turbo 2.0.x) +# +# libjpeg v8 API/ABI emulation: +# 1: + Partial image decompression functions (libjpeg-turbo 1.5.x) +# 2: + ICC functions (libjpeg-turbo 2.0.x) set(SO_AGE 2) if(WITH_MEM_SRCDST) set(SO_AGE 3) @@ -292,9 +386,21 @@ message(STATUS "libjpeg API shared library version = ${SO_MAJOR_VERSION}.${SO_AG # names of functions whenever they are modified in a backward-incompatible # manner, it is always backward-ABI-compatible with itself, so the major and # minor SO versions don't change. However, we increase the middle number (the -# SO "age") whenever functions are added to the API. +# SO "age") whenever functions are added to the API, because adding functions +# affects forward API/ABI compatibility. set(TURBOJPEG_SO_MAJOR_VERSION 0) -set(TURBOJPEG_SO_VERSION 0.2.0) +# 0: TurboJPEG 1.3.x API +# 1: TurboJPEG 1.4.x API +# The TurboJPEG 1.5.x API modified some of the function prototypes, adding +# the const keyword in front of pointers to unmodified buffers, but that did +# not affect forward API/ABI compatibility. +# 2: TurboJPEG 2.0.x API +# The TurboJPEG 2.1.x API modified the behavior of the tjDecompressHeader3() +# function so that it accepts "abbreviated table specification" (AKA +# "tables-only") datastreams as well as JPEG images, but that did not affect +# forward API/ABI compatibility. +set(TURBOJPEG_SO_AGE 2) +set(TURBOJPEG_SO_VERSION 0.${TURBOJPEG_SO_AGE}.0) ############################################################################### @@ -314,7 +420,7 @@ if(MSVC) endif() endforeach() endif() - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W3 /wd4996") + add_definitions(-D_CRT_NONSTDC_NO_WARNINGS) endif() if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID STREQUAL "Clang") @@ -364,34 +470,6 @@ if(MSVC) endif() if(UNIX) - # Check for headers - check_include_files(locale.h HAVE_LOCALE_H) - check_include_files(stddef.h HAVE_STDDEF_H) - check_include_files(stdlib.h HAVE_STDLIB_H) - check_include_files(sys/types.h NEED_SYS_TYPES_H) - - # Check for functions - include(CheckSymbolExists) - check_symbol_exists(memset string.h HAVE_MEMSET) - check_symbol_exists(memcpy string.h HAVE_MEMCPY) - if(NOT HAVE_MEMSET AND NOT HAVE_MEMCPY) - set(NEED_BSD_STRINGS 1) - endif() - - # Check for types - check_type_size("unsigned char" UNSIGNED_CHAR) - check_type_size("unsigned short" UNSIGNED_SHORT) - - # Check for compiler features - check_c_source_compiles("int main(void) { typedef struct undefined_structure *undef_struct_ptr; undef_struct_ptr ptr = 0; return ptr != 0; }" - INCOMPLETE_TYPES) - if(INCOMPLETE_TYPES) - message(STATUS "Compiler supports pointers to undefined structures.") - else() - set(INCOMPLETE_TYPES_BROKEN 1) - message(STATUS "Compiler does not support pointers to undefined structures.") - endif() - if(CMAKE_CROSSCOMPILING) set(RIGHT_SHIFT_IS_UNSIGNED 0) else() @@ -416,13 +494,6 @@ if(UNIX) exit(is_shifting_signed(-0x7F7E80B1L)); }" RIGHT_SHIFT_IS_UNSIGNED) endif() - - if(CMAKE_CROSSCOMPILING) - set(__CHAR_UNSIGNED__ 0) - else() - check_c_source_runs("int main(void) { return ((char) -1 < 0); }" - __CHAR_UNSIGNED__) - endif() endif() if(MSVC) @@ -453,6 +524,19 @@ if(NOT INLINE_WORKS) endif() message(STATUS "INLINE = ${INLINE} (FORCE_INLINE = ${FORCE_INLINE})") +if(MSVC) + set(THREAD_LOCAL "__declspec(thread)") +else() + set(THREAD_LOCAL "__thread") +endif() +check_c_source_compiles("${THREAD_LOCAL} int i; int main(void) { i = 0; return i; }" HAVE_THREAD_LOCAL) +if(HAVE_THREAD_LOCAL) + message(STATUS "THREAD_LOCAL = ${THREAD_LOCAL}") +else() + message(WARNING "Thread-local storage is not available. The TurboJPEG API library's global error handler will not be thread-safe.") + unset(THREAD_LOCAL) +endif() + if(UNIX AND NOT APPLE) file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/conftest.map "VERS_1 { global: *; };") set(CMAKE_REQUIRED_FLAGS @@ -494,6 +578,7 @@ else() configure_file(jconfig.h.in jconfig.h) endif() configure_file(jconfigint.h.in jconfigint.h) +configure_file(jversion.h.in jversion.h) if(UNIX) configure_file(libjpeg.map.in libjpeg.map) endif() @@ -533,6 +618,9 @@ endif() if(WITH_SIMD) add_subdirectory(simd) + if(NEON_INTRINSICS) + add_definitions(-DNEON_INTRINSICS) + endif() elseif(NOT WITH_12BIT) message(STATUS "SIMD extensions: None (WITH_SIMD = ${WITH_SIMD})") endif() @@ -543,6 +631,9 @@ if(WITH_SIMD) endif() else() add_library(simd OBJECT jsimd_none.c) + if(NOT WIN32 AND (CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED)) + set_target_properties(simd PROPERTIES POSITION_INDEPENDENT_CODE 1) + endif() endif() if(WITH_JAVA) @@ -572,6 +663,12 @@ if(WITH_TURBOJPEG) include_directories(${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2}) set(TJMAPFILE ${CMAKE_CURRENT_SOURCE_DIR}/turbojpeg-mapfile.jni) endif() + if(MSVC) + configure_file(${CMAKE_SOURCE_DIR}/win/turbojpeg.rc.in + ${CMAKE_BINARY_DIR}/win/turbojpeg.rc) + set(TURBOJPEG_SOURCES ${TURBOJPEG_SOURCES} + ${CMAKE_BINARY_DIR}/win/turbojpeg.rc) + endif() add_library(turbojpeg SHARED ${TURBOJPEG_SOURCES}) set_property(TARGET turbojpeg PROPERTY COMPILE_FLAGS "-DBMP_SUPPORTED -DPPM_SUPPORTED") @@ -666,9 +763,22 @@ if(ENABLE_STATIC) endif() if(PNG_SUPPORTED) + # to avoid finding shared library from CMake cache + unset(PNG_LIBRARY CACHE) + unset(PNG_LIBRARY_RELEASE CACHE) + unset(PNG_LIBRARY_DEBUG CACHE) + unset(ZLIB_LIBRARY CACHE) + unset(ZLIB_LIBRARY_RELEASE CACHE) + unset(ZLIB_LIBRARY_DEBUG CACHE) + + if (APPLE) + find_package(ZLIB REQUIRED) # macos doesn't have static zlib + endif() set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX}) find_package(PNG 1.6 REQUIRED) - find_package(ZLIB REQUIRED) + if (NOT APPLE) + find_package(ZLIB REQUIRED) + endif() target_include_directories(cjpeg-static PUBLIC ${PNG_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) target_link_libraries(cjpeg-static ${PNG_LIBRARY} ${ZLIB_LIBRARY}) endif() @@ -699,9 +809,15 @@ add_executable(wrjpgcom wrjpgcom.c) # TESTS ############################################################################### +if(WITH_FUZZ) + add_subdirectory(fuzz) +endif() + +add_executable(strtest strtest.c) + add_subdirectory(md5) -if(MSVC_IDE OR XCODE) +if(GENERATOR_IS_MULTI_CONFIG) set(OBJDIR "\${CTEST_CONFIGURATION_TYPE}/") else() set(OBJDIR "") @@ -716,8 +832,10 @@ if(WITH_12BIT) set(MD5_PPM_RGB_ISLOW f3301d2219783b8b3d942b7239fa50c0) set(MD5_JPEG_422_IFAST_OPT 7322e3bd2f127f7de4b40d4480ce60e4) set(MD5_PPM_422_IFAST 79807fa552899e66a04708f533e16950) + set(MD5_JPEG_440_ISLOW e25c1912e38367be505a89c410c1c2d2) + set(MD5_PPM_440_ISLOW e7d2e26288870cfcb30f3114ad01e380) set(MD5_PPM_422M_IFAST 07737bfe8a7c1c87aaa393a0098d16b0) - set(MD5_JPEG_420_IFAST_Q100_PROG 008ab68d6ddbba04a8f01deee4e0f9f8) + set(MD5_JPEG_420_IFAST_Q100_PROG 9447cef4803d9b0f74bcf333cc710a29) set(MD5_PPM_420_Q100_IFAST 1b3730122709f53d007255e8dfd3305e) set(MD5_PPM_420M_Q100_IFAST 980a1a3c5bf9510022869d30b7d26566) set(MD5_JPEG_GRAY_ISLOW 235c90707b16e2e069f37c888b2636d9) @@ -727,10 +845,11 @@ if(WITH_12BIT) set(MD5_JPEG_3x2_FLOAT_PROG_SSE a8c17daf77b457725ec929e215b603f8) set(MD5_PPM_3x2_FLOAT_SSE 42876ab9e5c2f76a87d08db5fbd57956) - set(MD5_JPEG_3x2_FLOAT_PROG_32BIT a8c17daf77b457725ec929e215b603f8) - set(MD5_PPM_3x2_FLOAT_32BIT ${MD5_PPM_3x2_FLOAT_SSE}) - set(MD5_JPEG_3x2_FLOAT_PROG_64BIT ${MD5_JPEG_3x2_FLOAT_PROG_32BIT}) - set(MD5_PPM_3x2_FLOAT_64BIT ${MD5_PPM_3x2_FLOAT_SSE}) + set(MD5_JPEG_3x2_FLOAT_PROG_NO_FP_CONTRACT a8c17daf77b457725ec929e215b603f8) + set(MD5_PPM_3x2_FLOAT_NO_FP_CONTRACT ${MD5_PPM_3x2_FLOAT_SSE}) + set(MD5_JPEG_3x2_FLOAT_PROG_FP_CONTRACT + ${MD5_JPEG_3x2_FLOAT_PROG_NO_FP_CONTRACT}) + set(MD5_PPM_3x2_FLOAT_FP_CONTRACT ${MD5_PPM_3x2_FLOAT_SSE}) set(MD5_JPEG_3x2_FLOAT_PROG_387 bc6dbbefac2872f6b9d6c4a0ae60c3c0) set(MD5_PPM_3x2_FLOAT_387 bcc5723c61560463ac60f772e742d092) set(MD5_JPEG_3x2_FLOAT_PROG_MSVC e27840755870fa849872e58aa0cd1400) @@ -751,9 +870,9 @@ if(WITH_12BIT) set(MD5_PPM_420M_ISLOW_1_4 35fd59d866e44659edfa3c18db2a3edb) set(MD5_PPM_420M_ISLOW_1_8 ccaed48ac0aedefda5d4abe4013f4ad7) set(MD5_PPM_420_ISLOW_SKIP15_31 86664cd9dc956536409e44e244d20a97) - set(MD5_PPM_420_ISLOW_PROG_CROP62x62_71_71 452a21656115a163029cfba5c04fa76a) - set(MD5_PPM_444_ISLOW_SKIP1_6 ef63901f71ef7a75cd78253fc0914f84) - set(MD5_PPM_444_ISLOW_PROG_CROP98x98_13_13 15b173fb5872d9575572fbcc1b05956f) + set(MD5_PPM_420_ISLOW_PROG_CROP62x62_71_71 b1fd2e6de6a6db2b3f5447a0b774a117) + set(MD5_PPM_444_ISLOW_SKIP1_6 4bfeeafcbaeb2ec4b32a71cc72f64fbc) + set(MD5_PPM_444_ISLOW_PROG_CROP98x98_13_13 e6d8226f43bb30afe927e8b96646b147) set(MD5_JPEG_CROP cdb35ff4b4519392690ea040c56ea99c) else() set(TESTORIG testorig.jpg) @@ -764,10 +883,12 @@ else() set(MD5_BMP_RGB_ISLOW_565D 4cfa0928ef3e6bb626d7728c924cfda4) set(MD5_JPEG_422_IFAST_OPT 2540287b79d913f91665e660303ab2c8) set(MD5_PPM_422_IFAST 35bd6b3f833bad23de82acea847129fa) + set(MD5_JPEG_440_ISLOW 368200a98a9d5041170a6232491522f9) + set(MD5_PPM_440_ISLOW 59d718725c83d37a0b59b7e4e355d2fb) set(MD5_PPM_422M_IFAST 8dbc65323d62cca7c91ba02dd1cfa81d) set(MD5_BMP_422M_IFAST_565 3294bd4d9a1f2b3d08ea6020d0db7065) set(MD5_BMP_422M_IFAST_565D da98c9c7b6039511be4a79a878a9abc1) - set(MD5_JPEG_420_IFAST_Q100_PROG e59bb462016a8d9a748c330a3474bb55) + set(MD5_JPEG_420_IFAST_Q100_PROG 0ba15f9dab81a703505f835f9dbbac6d) set(MD5_PPM_420_Q100_IFAST 5a732542015c278ff43635e473a8a294) set(MD5_PPM_420M_Q100_IFAST ff692ee9323a3b424894862557c092f1) set(MD5_JPEG_GRAY_ISLOW 72b51f894b8f4a10b3ee3066770aa38d) @@ -779,10 +900,11 @@ else() set(MD5_JPEG_3x2_FLOAT_PROG_SSE 343e3f8caf8af5986ebaf0bdc13b5c71) set(MD5_PPM_3x2_FLOAT_SSE 1a75f36e5904d6fc3a85a43da9ad89bb) - set(MD5_JPEG_3x2_FLOAT_PROG_32BIT 9bca803d2042bd1eb03819e2bf92b3e5) - set(MD5_PPM_3x2_FLOAT_32BIT f6bfab038438ed8f5522fbd33595dcdc) - set(MD5_JPEG_3x2_FLOAT_PROG_64BIT ${MD5_JPEG_3x2_FLOAT_PROG_32BIT}) - set(MD5_PPM_3x2_FLOAT_64BIT 0e917a34193ef976b679a6b069b1be26) + set(MD5_JPEG_3x2_FLOAT_PROG_NO_FP_CONTRACT 9bca803d2042bd1eb03819e2bf92b3e5) + set(MD5_PPM_3x2_FLOAT_NO_FP_CONTRACT f6bfab038438ed8f5522fbd33595dcdc) + set(MD5_JPEG_3x2_FLOAT_PROG_FP_CONTRACT + ${MD5_JPEG_3x2_FLOAT_PROG_NO_FP_CONTRACT}) + set(MD5_PPM_3x2_FLOAT_FP_CONTRACT 0e917a34193ef976b679a6b069b1be26) set(MD5_JPEG_3x2_FLOAT_PROG_387 1657664a410e0822c924b54f6f65e6e9) set(MD5_PPM_3x2_FLOAT_387 cb0a1f027f3d2917c902b5640214e025) set(MD5_JPEG_3x2_FLOAT_PROG_MSVC 7999ce9cd0ee9b6c7043b7351ab7639d) @@ -792,7 +914,7 @@ else() set(MD5_PPM_3x2_IFAST fd283664b3b49127984af0a7f118fccd) set(MD5_JPEG_420_ISLOW_ARI e986fb0a637a8d833d96e8a6d6d84ea1) set(MD5_JPEG_444_ISLOW_PROGARI 0a8f1c8f66e113c3cf635df0a475a617) - set(MD5_PPM_420M_IFAST_ARI 72b59a99bcf1de24c5b27d151bde2437) + set(MD5_PPM_420M_IFAST_ARI 57251da28a35b46eecb7177d82d10e0e) set(MD5_JPEG_420_ISLOW 9a68f56bc76e466aa7e52f415d0f4a5f) set(MD5_PPM_420M_ISLOW_2_1 9f9de8c0612f8d06869b960b05abf9c9) set(MD5_PPM_420M_ISLOW_15_8 b6875bc070720b899566cc06459b63b7) @@ -813,10 +935,10 @@ else() set(MD5_BMP_420M_ISLOW_565D ce034037d212bc403330df6f915c161b) set(MD5_PPM_420_ISLOW_SKIP15_31 c4c65c1e43d7275cd50328a61e6534f0) set(MD5_PPM_420_ISLOW_ARI_SKIP16_139 087c6b123db16ac00cb88c5b590bb74a) - set(MD5_PPM_420_ISLOW_PROG_CROP62x62_71_71 26eb36ccc7d1f0cb80cdabb0ac8b5d99) + set(MD5_PPM_420_ISLOW_PROG_CROP62x62_71_71 f2c1179887f0ff6c5689ed881ea3f32c) set(MD5_PPM_420_ISLOW_ARI_CROP53x53_4_4 886c6775af22370257122f8b16207e6d) - set(MD5_PPM_444_ISLOW_SKIP1_6 5606f86874cf26b8fcee1117a0a436a6) - set(MD5_PPM_444_ISLOW_PROG_CROP98x98_13_13 db87dc7ce26bcdc7a6b56239ce2b9d6c) + set(MD5_PPM_444_ISLOW_SKIP1_6 2f4d84e9832ce741f5f0666a84632a5a) + set(MD5_PPM_444_ISLOW_PROG_CROP98x98_13_13 d04ed9d1a2a059455b343bf07a2433a3) set(MD5_PPM_444_ISLOW_ARI_CROP37x37_0_0 cb57b32bd6d03e35432362f7bf184b6d) set(MD5_JPEG_CROP b4197f377e621c4e9b1d20471432610d) endif() @@ -870,11 +992,16 @@ endif() # # sse = validate against the expected results from the libjpeg-turbo SSE SIMD # extensions -# 32bit = validate against the expected results from the C code when running on -# a 32-bit FPU (or when SSE is being used for floating point math, -# which is generally the default with x86-64 compilers) -# 64bit = validate against the expected results from the C code when running -# on a 64-bit FPU +# no-fp-contract = validate against the expected results from the C code when +# floating point expression contraction is disabled (the +# default with Clang, with GCC when building for platforms +# that lack fused multiply-add [FMA] instructions, or when +# passing -ffp-contract=off to the compiler) +# fp-contract = validate against the expected results from the C code when +# floating point expression contraction is enabled (the default +# with GCC when building for platforms that have fused multiply- +# add [FMA] instructions or when passing -ffp-contract=fast to +# the compiler) # 387 = validate against the expected results from the C code when the 387 FPU # is being used for floating point math (which is generally the default # with x86 compilers) @@ -885,15 +1012,18 @@ if(CPU_TYPE STREQUAL "x86_64" OR CPU_TYPE STREQUAL "i386") if(WITH_SIMD) set(DEFAULT_FLOATTEST sse) elseif(CPU_TYPE STREQUAL "x86_64") - set(DEFAULT_FLOATTEST 32bit) - elseif(CPU_TYPE STREQUAL "i386" AND MSVC) - set(DEFAULT_FLOATTEST msvc) + set(DEFAULT_FLOATTEST no-fp-contract) + # else we can't really set an intelligent default for i386. The appropriate + # value could be no-fp-contract, fp-contract, 387, or msvc, depending on the + # compiler and compiler options. We leave it to the user to set FLOATTEST + # manually. endif() else() - if(BITS EQUAL 64) - set(DEFAULT_FLOATTEST 64bit) - elseif(BITS EQUAL 32) - set(DEFAULT_FLOATTEST 32bit) + if((CPU_TYPE STREQUAL "powerpc" OR CPU_TYPE STREQUAL "arm64") AND + NOT CMAKE_C_COMPILER_ID STREQUAL "Clang" AND NOT MSVC) + set(DEFAULT_FLOATTEST fp-contract) + else() + set(DEFAULT_FLOATTEST no-fp-contract) endif() endif() @@ -904,15 +1034,17 @@ if(DEFINED WITH_SIMD_INT AND NOT WITH_SIMD EQUAL WITH_SIMD_INT) endif() set(WITH_SIMD_INT ${WITH_SIMD} CACHE INTERNAL "") set(FLOATTEST ${DEFAULT_FLOATTEST} CACHE STRING - "The type of floating point math used by the floating point DCT/IDCT algorithms. This tells the testing system which numerical results it should expect from those tests. [sse = libjpeg-turbo x86/x86-64 SIMD extensions, 32bit = generic 32-bit FPU or SSE, 64bit = generic 64-bit FPU, 387 = 387 FPU, msvc = 32-bit Visual Studio] (default = ${DEFAULT_FLOATTEST})" + "The type of floating point math used by the floating point DCT/IDCT algorithms. This tells the testing system which numerical results it should expect from those tests. [sse = libjpeg-turbo x86/x86-64 SIMD extensions, no-fp-contract = generic FPU with floating point expression contraction disabled, fp-contract = generic FPU with floating point expression contraction enabled, 387 = 387 FPU, msvc = 32-bit Visual Studio] (default = ${DEFAULT_FLOATTEST})" ${FORCE_FLOATTEST}) message(STATUS "FLOATTEST = ${FLOATTEST}") if(FLOATTEST) string(TOUPPER ${FLOATTEST} FLOATTEST_UC) + string(REGEX REPLACE "-" "_" FLOATTEST_UC ${FLOATTEST_UC}) string(TOLOWER ${FLOATTEST} FLOATTEST) - if(NOT FLOATTEST STREQUAL "sse" AND NOT FLOATTEST STREQUAL "32bit" AND - NOT FLOATTEST STREQUAL "64bit" AND NOT FLOATTEST STREQUAL "387" AND + if(NOT FLOATTEST STREQUAL "sse" AND + NOT FLOATTEST STREQUAL "no-fp-contract" AND + NOT FLOATTEST STREQUAL "fp-contract" AND NOT FLOATTEST STREQUAL "387" AND NOT FLOATTEST STREQUAL "msvc") message(FATAL_ERROR "\"${FLOATTEST}\" is not a valid value for FLOATTEST.") endif() @@ -923,29 +1055,35 @@ foreach(libtype ${TEST_LIBTYPES}) set(suffix -static) endif() if(WITH_TURBOJPEG) - add_test(tjunittest-${libtype} tjunittest${suffix}) - add_test(tjunittest-${libtype}-alloc tjunittest${suffix} -alloc) - add_test(tjunittest-${libtype}-yuv tjunittest${suffix} -yuv) - add_test(tjunittest-${libtype}-yuv-alloc tjunittest${suffix} -yuv -alloc) - add_test(tjunittest-${libtype}-yuv-nopad tjunittest${suffix} -yuv -noyuvpad) - add_test(tjunittest-${libtype}-bmp tjunittest${suffix} -bmp) - - set(MD5_PPM_GRAY_TILE 89d3ca21213d9d864b50b4e4e7de4ca6) - set(MD5_PPM_420_8x8_TILE 847fceab15c5b7b911cb986cf0f71de3) - set(MD5_PPM_420_16x16_TILE ca45552a93687e078f7137cc4126a7b0) - set(MD5_PPM_420_32x32_TILE d8676f1d6b68df358353bba9844f4a00) + add_test(tjunittest-${libtype} + ${CMAKE_CROSSCOMPILING_EMULATOR} tjunittest${suffix}) + add_test(tjunittest-${libtype}-alloc + ${CMAKE_CROSSCOMPILING_EMULATOR} tjunittest${suffix} -alloc) + add_test(tjunittest-${libtype}-yuv + ${CMAKE_CROSSCOMPILING_EMULATOR} tjunittest${suffix} -yuv) + add_test(tjunittest-${libtype}-yuv-alloc + ${CMAKE_CROSSCOMPILING_EMULATOR} tjunittest${suffix} -yuv -alloc) + add_test(tjunittest-${libtype}-yuv-nopad + ${CMAKE_CROSSCOMPILING_EMULATOR} tjunittest${suffix} -yuv -noyuvpad) + add_test(tjunittest-${libtype}-bmp + ${CMAKE_CROSSCOMPILING_EMULATOR} tjunittest${suffix} -bmp) + + set(MD5_PPM_GRAY_TILE b8f5defbd8e76a1d557b860b457bcd05) + set(MD5_PPM_420_8x8_TILE cb8bb3f7de153b4cfbf6cc6f36b43bf0) + set(MD5_PPM_420_16x16_TILE a144f52396cba331bb85c6b43d1deedc) + set(MD5_PPM_420_32x32_TILE 5d6a7c35bb44adc89581466b253ef920) set(MD5_PPM_420_64x64_TILE 4e4c1a3d7ea4bace4f868bcbe83b7050) - set(MD5_PPM_420_128x128_TILE f24c3429c52265832beab9df72a0ceae) + set(MD5_PPM_420_128x128_TILE d8b12baac07d24d9705d712359ae2181) set(MD5_PPM_420M_8x8_TILE bc25320e1f4c31ce2e610e43e9fd173c) - set(MD5_PPM_420M_TILE 75ffdf14602258c5c189522af57fa605) + set(MD5_PPM_420M_TILE 391df0063a66d7656f18f3a5cb107357) set(MD5_PPM_422_8x8_TILE d83dacd9fc73b0a6f10c09acad64eb1e) set(MD5_PPM_422_16x16_TILE 35077fb610d72dd743b1eb0cbcfe10fb) - set(MD5_PPM_422_32x32_TILE e6902ed8a449ecc0f0d6f2bf945f65f7) - set(MD5_PPM_422_64x64_TILE 2b4502a8f316cedbde1da7bce3d2231e) + set(MD5_PPM_422_32x32_TILE 06a45b730ffa26990af4930140c3233b) + set(MD5_PPM_422_64x64_TILE 007d991538e571e6e56c54b6224b060a) set(MD5_PPM_422_128x128_TILE f0b5617d578f5e13c8eee215d64d4877) set(MD5_PPM_422M_8x8_TILE 828941d7f41cd6283abd6beffb7fd51d) - set(MD5_PPM_422M_TILE e877ae1324c4a280b95376f7f018172f) - set(MD5_PPM_444_TILE 7964e41e67cfb8d0a587c0aa4798f9c3) + set(MD5_PPM_422M_TILE df4b4c784feb513d250c2dd76d61fa1a) + set(MD5_PPM_444_TILE c757cfea44ac6439fea03ef57d04b7de) # Test compressing from/decompressing to an arbitrary subregion of a larger # image buffer @@ -953,55 +1091,19 @@ foreach(libtype ${TEST_LIBTYPES}) ${CMAKE_COMMAND} -E copy_if_different ${TESTIMAGES}/testorig.ppm testout_tile.ppm) add_test(tjbench-${libtype}-tile - tjbench${suffix} testout_tile.ppm 95 -rgb -quiet -tile -benchtime 0.01 - -warmup 0) + ${CMAKE_CROSSCOMPILING_EMULATOR} tjbench${suffix} testout_tile.ppm 95 + -rgb -quiet -tile -benchtime 0.01 -warmup 0) set_tests_properties(tjbench-${libtype}-tile PROPERTIES DEPENDS tjbench-${libtype}-tile-cp) - foreach(tile 8 16 32 64 128) - add_test(tjbench-${libtype}-tile-gray-${tile}x${tile}-cmp - ${MD5CMP} ${MD5_PPM_GRAY_TILE} - testout_tile_GRAY_Q95_${tile}x${tile}.ppm) - foreach(subsamp 420 422) - add_test(tjbench-${libtype}-tile-${subsamp}-${tile}x${tile}-cmp - ${MD5CMP} ${MD5_PPM_${subsamp}_${tile}x${tile}_TILE} - testout_tile_${subsamp}_Q95_${tile}x${tile}.ppm) - endforeach() - add_test(tjbench-${libtype}-tile-444-${tile}x${tile}-cmp - ${MD5CMP} ${MD5_PPM_444_TILE} - testout_tile_444_Q95_${tile}x${tile}.ppm) - foreach(subsamp gray 420 422 444) - set_tests_properties(tjbench-${libtype}-tile-${subsamp}-${tile}x${tile}-cmp - PROPERTIES DEPENDS tjbench-${libtype}-tile) - endforeach() - endforeach() - add_test(tjbench-${libtype}-tilem-cp ${CMAKE_COMMAND} -E copy_if_different ${TESTIMAGES}/testorig.ppm testout_tilem.ppm) add_test(tjbench-${libtype}-tilem - tjbench${suffix} testout_tilem.ppm 95 -rgb -fastupsample -quiet -tile - -benchtime 0.01 -warmup 0) + ${CMAKE_CROSSCOMPILING_EMULATOR} tjbench${suffix} testout_tilem.ppm 95 + -rgb -fastupsample -quiet -tile -benchtime 0.01 -warmup 0) set_tests_properties(tjbench-${libtype}-tilem PROPERTIES DEPENDS tjbench-${libtype}-tilem-cp) - - add_test(tjbench-${libtype}-tile-420m-8x8-cmp - ${MD5CMP} ${MD5_PPM_420M_8x8_TILE} testout_tilem_420_Q95_8x8.ppm) - add_test(tjbench-${libtype}-tile-422m-8x8-cmp - ${MD5CMP} ${MD5_PPM_422M_8x8_TILE} testout_tilem_422_Q95_8x8.ppm) - foreach(tile 16 32 64 128) - foreach(subsamp 420 422) - add_test(tjbench-${libtype}-tile-${subsamp}m-${tile}x${tile}-cmp - ${MD5CMP} ${MD5_PPM_${subsamp}M_TILE} - testout_tilem_${subsamp}_Q95_${tile}x${tile}.ppm) - endforeach() - endforeach() - foreach(tile 8 16 32 64 128) - foreach(subsamp 420 422) - set_tests_properties(tjbench-${libtype}-tile-${subsamp}m-${tile}x${tile}-cmp - PROPERTIES DEPENDS tjbench-${libtype}-tilem) - endforeach() - endforeach() endif() # These tests are carefully crafted to provide full coverage of as many of @@ -1010,9 +1112,10 @@ foreach(libtype ${TEST_LIBTYPES}) macro(add_bittest PROG NAME ARGS OUTFILE INFILE MD5SUM) add_test(${PROG}-${libtype}-${NAME} - ${PROG}${suffix} ${ARGS} -outfile ${OUTFILE} ${INFILE}) + ${CMAKE_CROSSCOMPILING_EMULATOR} ${PROG}${suffix} ${ARGS} + -outfile ${OUTFILE} ${INFILE}) add_test(${PROG}-${libtype}-${NAME}-cmp - ${MD5CMP} ${MD5SUM} ${OUTFILE}) + ${CMAKE_CROSSCOMPILING_EMULATOR} ${MD5CMP} ${MD5SUM} ${OUTFILE}) set_tests_properties(${PROG}-${libtype}-${NAME}-cmp PROPERTIES DEPENDS ${PROG}-${libtype}-${NAME}) if(${ARGC} GREATER 6) @@ -1023,7 +1126,7 @@ foreach(libtype ${TEST_LIBTYPES}) endmacro() # CC: null SAMP: fullsize FDCT: islow ENT: huff - add_bittest(cjpeg rgb-islow "-rgb;-dct;int;-icc;${TESTIMAGES}/test1.icc" + add_bittest(cjpeg rgb-islow "-revert;-rgb;-dct;int;-icc;${TESTIMAGES}/test1.icc" testout_rgb_islow.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_RGB_ISLOW}) @@ -1033,12 +1136,14 @@ foreach(libtype ${TEST_LIBTYPES}) ${MD5_PPM_RGB_ISLOW} cjpeg-${libtype}-rgb-islow) add_test(djpeg-${libtype}-rgb-islow-icc-cmp - ${MD5CMP} b06a39d730129122e85c1363ed1bbc9e testout_rgb_islow.icc) + ${CMAKE_CROSSCOMPILING_EMULATOR} ${MD5CMP} + b06a39d730129122e85c1363ed1bbc9e testout_rgb_islow.icc) set_tests_properties(djpeg-${libtype}-rgb-islow-icc-cmp PROPERTIES DEPENDS djpeg-${libtype}-rgb-islow) - add_bittest(jpegtran icc "-copy;all;-icc;${TESTIMAGES}/test2.icc" - testout_rgb_islow2.jpg testout_rgb_islow.jpg ${MD5_JPEG_RGB_ISLOW2}) + add_bittest(jpegtran icc "-revert;-copy;all;-icc;${TESTIMAGES}/test2.icc" + testout_rgb_islow2.jpg testout_rgb_islow.jpg + ${MD5_JPEG_RGB_ISLOW2} cjpeg-${libtype}-rgb-islow) if(NOT WITH_12BIT) # CC: RGB->RGB565 SAMP: fullsize IDCT: islow ENT: huff @@ -1053,7 +1158,7 @@ foreach(libtype ${TEST_LIBTYPES}) endif() # CC: RGB->YCC SAMP: fullsize/h2v1 FDCT: ifast ENT: 2-pass huff - add_bittest(cjpeg 422-ifast-opt "-sample;2x1;-dct;fast;-opt" + add_bittest(cjpeg 422-ifast-opt "-revert;-sample;2x1;-dct;fast;-opt" testout_422_ifast_opt.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_422_IFAST_OPT}) @@ -1062,6 +1167,16 @@ foreach(libtype ${TEST_LIBTYPES}) testout_422_ifast.ppm testout_422_ifast_opt.jpg ${MD5_PPM_422_IFAST} cjpeg-${libtype}-422-ifast-opt) + # CC: RGB->YCC SAMP: fullsize/h1v2 FDCT: islow ENT: huff + add_bittest(cjpeg 440-islow "-sample;1x2;-dct;int" + testout_440_islow.jpg ${TESTIMAGES}/testorig.ppm + ${MD5_JPEG_440_ISLOW}) + + # CC: YCC->RGB SAMP: fullsize/h1v2 fancy IDCT: islow ENT: huff + add_bittest(djpeg 440-islow "-dct;int" + testout_440_islow.ppm testout_440_islow.jpg + ${MD5_PPM_440_ISLOW} cjpeg-${libtype}-440-islow) + # CC: YCC->RGB SAMP: h2v1 merged IDCT: ifast ENT: huff add_bittest(djpeg 422m-ifast "-dct;fast;-nosmooth" testout_422m_ifast.ppm testout_422_ifast_opt.jpg @@ -1082,7 +1197,7 @@ foreach(libtype ${TEST_LIBTYPES}) # CC: RGB->YCC SAMP: fullsize/h2v2 FDCT: ifast ENT: prog huff add_bittest(cjpeg 420-q100-ifast-prog - "-sample;2x2;-quality;100;-dct;fast;-scans;${TESTIMAGES}/test.scan" + "-revert;-sample;2x2;-quality;100;-dct;fast;-scans;${TESTIMAGES}/test.scan" testout_420_q100_ifast_prog.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_420_IFAST_Q100_PROG}) @@ -1097,7 +1212,7 @@ foreach(libtype ${TEST_LIBTYPES}) ${MD5_PPM_420M_Q100_IFAST} cjpeg-${libtype}-420-q100-ifast-prog) # CC: RGB->Gray SAMP: fullsize FDCT: islow ENT: huff - add_bittest(cjpeg gray-islow "-gray;-dct;int" + add_bittest(cjpeg gray-islow "-revert;-gray;-dct;int" testout_gray_islow.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_GRAY_ISLOW}) @@ -1125,13 +1240,13 @@ foreach(libtype ${TEST_LIBTYPES}) # CC: RGB->YCC SAMP: fullsize smooth/h2v2 smooth FDCT: islow # ENT: 2-pass huff - add_bittest(cjpeg 420s-ifast-opt "-sample;2x2;-smooth;1;-dct;int;-opt" + add_bittest(cjpeg 420s-ifast-opt "-revert;-sample;2x2;-smooth;1;-dct;int;-opt" testout_420s_ifast_opt.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_420S_IFAST_OPT}) if(FLOATTEST) # CC: RGB->YCC SAMP: fullsize/int FDCT: float ENT: prog huff - add_bittest(cjpeg 3x2-float-prog "-sample;3x2;-dct;float;-prog" + add_bittest(cjpeg 3x2-float-prog "-revert;-sample;3x2;-dct;float;-prog" testout_3x2_float_prog.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_3x2_FLOAT_PROG_${FLOATTEST_UC}}) @@ -1142,7 +1257,7 @@ foreach(libtype ${TEST_LIBTYPES}) endif() # CC: RGB->YCC SAMP: fullsize/int FDCT: ifast ENT: prog huff - add_bittest(cjpeg 3x2-ifast-prog "-sample;3x2;-dct;fast;-prog" + add_bittest(cjpeg 3x2-ifast-prog "-revert;-sample;3x2;-dct;fast;-prog" testout_3x2_ifast_prog.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_3x2_IFAST_PROG}) @@ -1153,28 +1268,28 @@ foreach(libtype ${TEST_LIBTYPES}) if(WITH_ARITH_ENC) # CC: YCC->RGB SAMP: fullsize/h2v2 FDCT: islow ENT: arith - add_bittest(cjpeg 420-islow-ari "-dct;int;-arithmetic" + add_bittest(cjpeg 420-islow-ari "-revert;-dct;int;-arithmetic" testout_420_islow_ari.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_420_ISLOW_ARI}) - add_bittest(jpegtran 420-islow-ari "-arithmetic" + add_bittest(jpegtran 420-islow-ari "-revert;-arithmetic" testout_420_islow_ari2.jpg ${TESTIMAGES}/testimgint.jpg ${MD5_JPEG_420_ISLOW_ARI}) # CC: YCC->RGB SAMP: fullsize FDCT: islow ENT: prog arith add_bittest(cjpeg 444-islow-progari - "-sample;1x1;-dct;int;-prog;-arithmetic" + "-revert;-sample;1x1;-dct;int;-prog;-arithmetic" testout_444_islow_progari.jpg ${TESTIMAGES}/testorig.ppm ${MD5_JPEG_444_ISLOW_PROGARI}) endif() if(WITH_ARITH_DEC) # CC: RGB->YCC SAMP: h2v2 merged IDCT: ifast ENT: arith - add_bittest(djpeg 420m-ifast-ari "-fast;-ppm" + add_bittest(djpeg 420m-ifast-ari "-fast;-skip;1,20;-ppm" testout_420m_ifast_ari.ppm ${TESTIMAGES}/testimgari.jpg ${MD5_PPM_420M_IFAST_ARI}) - add_bittest(jpegtran 420-islow "" + add_bittest(jpegtran 420-islow "-revert" testout_420_islow.jpg ${TESTIMAGES}/testimgari.jpg ${MD5_JPEG_420_ISLOW}) endif() @@ -1251,7 +1366,7 @@ foreach(libtype ${TEST_LIBTYPES}) # Context rows: Yes Intra-iMCU row: No iMCU row prefetch: No ENT: prog huff add_test(cjpeg-${libtype}-420-islow-prog - cjpeg${suffix} -dct int -prog + ${CMAKE_CROSSCOMPILING_EMULATOR} cjpeg${suffix} -dct int -prog -outfile testout_420_islow_prog.jpg ${TESTIMAGES}/testorig.ppm) add_bittest(djpeg 420-islow-prog-crop62x62_71_71 "-dct;int;-crop;62x62+71+71;-ppm" @@ -1268,7 +1383,7 @@ foreach(libtype ${TEST_LIBTYPES}) # Context rows: No Intra-iMCU row: Yes ENT: huff add_test(cjpeg-${libtype}-444-islow - cjpeg${suffix} -dct int -sample 1x1 + ${CMAKE_CROSSCOMPILING_EMULATOR} cjpeg${suffix} -dct int -sample 1x1 -outfile testout_444_islow.jpg ${TESTIMAGES}/testorig.ppm) add_bittest(djpeg 444-islow-skip1_6 "-dct;int;-skip;1,6;-ppm" testout_444_islow_skip1,6.ppm testout_444_islow.jpg @@ -1276,7 +1391,7 @@ foreach(libtype ${TEST_LIBTYPES}) # Context rows: No Intra-iMCU row: No ENT: prog huff add_test(cjpeg-${libtype}-444-islow-prog - cjpeg${suffix} -dct int -prog -sample 1x1 + ${CMAKE_CROSSCOMPILING_EMULATOR} cjpeg${suffix} -dct int -prog -sample 1x1 -outfile testout_444_islow_prog.jpg ${TESTIMAGES}/testorig.ppm) add_bittest(djpeg 444-islow-prog-crop98x98_13_13 "-dct;int;-crop;98x98+13+13;-ppm" @@ -1286,8 +1401,9 @@ foreach(libtype ${TEST_LIBTYPES}) # Context rows: No Intra-iMCU row: No ENT: arith if(WITH_ARITH_ENC) add_test(cjpeg-${libtype}-444-islow-ari - cjpeg${suffix} -dct int -arithmetic -sample 1x1 - -outfile testout_444_islow_ari.jpg ${TESTIMAGES}/testorig.ppm) + ${CMAKE_CROSSCOMPILING_EMULATOR} cjpeg${suffix} -dct int -arithmetic + -sample 1x1 -outfile testout_444_islow_ari.jpg + ${TESTIMAGES}/testorig.ppm) if(WITH_ARITH_DEC) add_bittest(djpeg 444-islow-ari-crop37x37_0_0 "-dct;int;-crop;37x37+0+0;-ppm" @@ -1296,7 +1412,7 @@ foreach(libtype ${TEST_LIBTYPES}) endif() endif() - add_bittest(jpegtran crop "-crop;120x90+20+50;-transpose;-perfect" + add_bittest(jpegtran crop "-revert;-crop;120x90+20+50;-transpose;-perfect" testout_crop.jpg ${TESTIMAGES}/${TESTORIG} ${MD5_JPEG_CROP}) @@ -1305,6 +1421,11 @@ endforeach() add_custom_target(testclean COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmakescripts/testclean.cmake) +configure_file(croptest.in croptest @ONLY) +add_custom_target(croptest + COMMAND echo croptest + COMMAND ${BASH} ${CMAKE_CURRENT_BINARY_DIR}/croptest) + if(WITH_TURBOJPEG) configure_file(tjbenchtest.in tjbenchtest @ONLY) configure_file(tjexampletest.in tjexampletest @ONLY) @@ -1335,14 +1456,15 @@ if(WITH_TURBOJPEG) COMMAND ${BASH} ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest.java -yuv COMMAND echo tjbenchtest.java -progressive COMMAND ${BASH} ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest.java -progressive - COMMAND echo tjexampletest.java -progressive -yuv + COMMAND echo tjbenchtest.java -progressive -yuv COMMAND ${BASH} ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest.java -progressive -yuv COMMAND echo tjexampletest.java COMMAND ${BASH} ${CMAKE_CURRENT_BINARY_DIR}/tjexampletest.java DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest.java - ${CMAKE_CURRENT_BINARY_DIR}/tjexampletest) + ${CMAKE_CURRENT_BINARY_DIR}/tjexampletest + ${CMAKE_CURRENT_BINARY_DIR}/tjexampletest.java) else() add_custom_target(tjtest COMMAND echo tjbenchtest @@ -1359,7 +1481,8 @@ if(WITH_TURBOJPEG) COMMAND ${BASH} ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest -progressive -yuv COMMAND echo tjexampletest COMMAND ${BASH} ${CMAKE_CURRENT_BINARY_DIR}/tjexampletest - DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest) + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/tjbenchtest + ${CMAKE_CURRENT_BINARY_DIR}/tjexampletest) endif() endif() @@ -1372,10 +1495,13 @@ set(EXE ${CMAKE_EXECUTABLE_SUFFIX}) if(WITH_TURBOJPEG) if(ENABLE_SHARED) - install(TARGETS turbojpeg tjbench + install(TARGETS turbojpeg EXPORT ${CMAKE_PROJECT_NAME}Targets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(TARGETS tjbench + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) if(NOT CMAKE_VERSION VERSION_LESS "3.1" AND MSVC AND CMAKE_C_LINKER_SUPPORTS_PDB) install(FILES "$" @@ -1383,10 +1509,11 @@ if(WITH_TURBOJPEG) endif() endif() if(ENABLE_STATIC) - install(TARGETS turbojpeg-static ARCHIVE - DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(TARGETS turbojpeg-static EXPORT ${CMAKE_PROJECT_NAME}Targets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) if(NOT ENABLE_SHARED) - if(MSVC_IDE OR XCODE) + if(GENERATOR_IS_MULTI_CONFIG) set(DIR "${CMAKE_CURRENT_BINARY_DIR}/\${CMAKE_INSTALL_CONFIG_NAME}") else() set(DIR ${CMAKE_CURRENT_BINARY_DIR}) @@ -1400,9 +1527,11 @@ if(WITH_TURBOJPEG) endif() if(ENABLE_STATIC) - install(TARGETS jpeg-static ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(TARGETS jpeg-static EXPORT ${CMAKE_PROJECT_NAME}Targets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) if(NOT ENABLE_SHARED) - if(MSVC_IDE OR XCODE) + if(GENERATOR_IS_MULTI_CONFIG) set(DIR "${CMAKE_CURRENT_BINARY_DIR}/\${CMAKE_INSTALL_CONFIG_NAME}") else() set(DIR ${CMAKE_CURRENT_BINARY_DIR}) @@ -1438,8 +1567,18 @@ if(UNIX OR MINGW) DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) endif() install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pkgscripts/libjpeg.pc - ${CMAKE_CURRENT_BINARY_DIR}/pkgscripts/libturbojpeg.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +if(WITH_TURBOJPEG) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pkgscripts/libturbojpeg.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +endif() +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/pkgscripts/${CMAKE_PROJECT_NAME}Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/pkgscripts/${CMAKE_PROJECT_NAME}ConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CMAKE_PROJECT_NAME}) +install(EXPORT ${CMAKE_PROJECT_NAME}Targets + NAMESPACE ${CMAKE_PROJECT_NAME}:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CMAKE_PROJECT_NAME}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/jconfig.h ${CMAKE_CURRENT_SOURCE_DIR}/jerror.h ${CMAKE_CURRENT_SOURCE_DIR}/jmorecfg.h diff --git a/third-party/mozjpeg/mozjpeg/ChangeLog.md b/third-party/mozjpeg/mozjpeg/ChangeLog.md index 4d1219e555d..92d82cce415 100644 --- a/third-party/mozjpeg/mozjpeg/ChangeLog.md +++ b/third-party/mozjpeg/mozjpeg/ChangeLog.md @@ -1,3 +1,500 @@ +2.1.6 +===== + +### Significant changes relative to 2.1.5.1: + +1. Fixed an oversight in 1.4 beta1[8] that caused various segfaults and buffer +overruns when attempting to decompress various specially-crafted malformed +12-bit-per-component JPEG images using a 12-bit-per-component build of djpeg +(`-DWITH_12BIT=1`) with both color quantization and RGB565 color conversion +enabled. + +2. Fixed an issue whereby `jpeg_crop_scanline()` sometimes miscalculated the +downsampled width for components with 4x2 or 2x4 subsampling factors if +decompression scaling was enabled. This caused the components to be upsampled +incompletely, which caused the color converter to read from uninitialized +memory. With 12-bit data precision, this caused a buffer overrun or underrun +and subsequent segfault if the sample value read from uninitialized memory was +outside of the valid sample range. + +3. Fixed a long-standing issue whereby the `tjTransform()` function, when used +with the `TJXOP_TRANSPOSE`, `TJXOP_TRANSVERSE`, `TJXOP_ROT90`, or +`TJXOP_ROT270` transform operation and without automatic JPEG destination +buffer (re)allocation or lossless cropping, computed the worst-case transformed +JPEG image size based on the source image dimensions rather than the +transformed image dimensions. If a calling program allocated the JPEG +destination buffer based on the transformed image dimensions, as the API +documentation instructs, and attempted to transform a specially-crafted 4:2:2, +4:4:0, or 4:1:1 JPEG source image containing a large amount of metadata, the +issue caused `tjTransform()` to overflow the JPEG destination buffer rather +than fail gracefully. The issue could be worked around by setting +`TJXOPT_COPYNONE`. Note that, irrespective of this issue, `tjTransform()` +cannot reliably transform JPEG source images that contain a large amount of +metadata unless automatic JPEG destination buffer (re)allocation is used or +`TJXOPT_COPYNONE` is set. + +4. Fixed an issue that caused the C Huffman encoder (which is not used by +default on x86 and Arm CPUs) to read from uninitialized memory when attempting +to transform a specially-crafted malformed arithmetic-coded JPEG source image +into a baseline Huffman-coded JPEG destination image. + + +2.1.5.1 +======= + +### Significant changes relative to 2.1.5: + +1. The SIMD dispatchers in libjpeg-turbo 2.1.4 and prior stored the list of +supported SIMD instruction sets in a global variable, which caused an innocuous +race condition whereby the variable could have been initialized multiple times +if `jpeg_start_*compress()` was called simultaneously in multiple threads. +libjpeg-turbo 2.1.5 included an undocumented attempt to fix this race condition +by making the SIMD support variable thread-local. However, that caused another +issue whereby, if `jpeg_start_*compress()` was called in one thread and +`jpeg_read_*()` or `jpeg_write_*()` was called in a second thread, the SIMD +support variable was never initialized in the second thread. On x86 systems, +this led the second thread to incorrectly assume that AVX2 instructions were +always available, and when it attempted to use those instructions on older x86 +CPUs that do not support them, an illegal instruction error occurred. The SIMD +dispatchers now ensure that the SIMD support variable is initialized before +dispatching based on its value. + + +2.1.5 +===== + +### Significant changes relative to 2.1.4: + +1. Fixed issues in the build system whereby, when using the Ninja Multi-Config +CMake generator, a static build of libjpeg-turbo (a build in which +`ENABLE_SHARED` is `0`) could not be installed, a Windows installer could not +be built, and the Java regression tests failed. + +2. Fixed a regression introduced by 2.0 beta1[15] that caused a buffer overrun +in the progressive Huffman encoder when attempting to transform a +specially-crafted malformed 12-bit-per-component JPEG image into a progressive +12-bit-per-component JPEG image using a 12-bit-per-component build of +libjpeg-turbo (`-DWITH_12BIT=1`.) Given that the buffer overrun was fully +contained within the progressive Huffman encoder structure and did not cause a +segfault or other user-visible errant behavior, given that the lossless +transformer (unlike the decompressor) is not generally exposed to arbitrary +data exploits, and given that 12-bit-per-component builds of libjpeg-turbo are +uncommon, this issue did not likely pose a security risk. + +3. Fixed an issue whereby, when using a 12-bit-per-component build of +libjpeg-turbo (`-DWITH_12BIT=1`), passing samples with values greater than 4095 +or less than 0 to `jpeg_write_scanlines()` caused a buffer overrun or underrun +in the RGB-to-YCbCr color converter. + +4. Fixed a floating point exception that occurred when attempting to use the +jpegtran `-drop` and `-trim` options to losslessly transform a +specially-crafted malformed JPEG image. + +5. Fixed an issue in `tjBufSizeYUV2()` whereby it returned a bogus result, +rather than throwing an error, if the `align` parameter was not a power of 2. +Fixed a similar issue in `tjCompressFromYUV()` whereby it generated a corrupt +JPEG image in certain cases, rather than throwing an error, if the `align` +parameter was not a power of 2. + +6. Fixed an issue whereby `tjDecompressToYUV2()`, which is a wrapper for +`tjDecompressToYUVPlanes()`, used the desired YUV image dimensions rather than +the actual scaled image dimensions when computing the plane pointers and +strides to pass to `tjDecompressToYUVPlanes()`. This caused a buffer overrun +and subsequent segfault if the desired image dimensions exceeded the scaled +image dimensions. + +7. Fixed an issue whereby, when decompressing a 12-bit-per-component JPEG image +(`-DWITH_12BIT=1`) using an alpha-enabled output color space such as +`JCS_EXT_RGBA`, the alpha channel was set to 255 rather than 4095. + +8. Fixed an issue whereby the Java version of TJBench did not accept a range of +quality values. + +9. Fixed an issue whereby, when `-progressive` was passed to TJBench, the JPEG +input image was not transformed into a progressive JPEG image prior to +decompression. + + +2.1.4 +===== + +### Significant changes relative to 2.1.3: + +1. Fixed a regression introduced in 2.1.3 that caused build failures with +Visual Studio 2010. + +2. The `tjDecompressHeader3()` function in the TurboJPEG C API and the +`TJDecompressor.setSourceImage()` method in the TurboJPEG Java API now accept +"abbreviated table specification" (AKA "tables-only") datastreams, which can be +used to prime the decompressor with quantization and Huffman tables that can be +used when decompressing subsequent "abbreviated image" datastreams. + +3. libjpeg-turbo now performs run-time detection of AltiVec instructions on +OS X/PowerPC systems if AltiVec instructions are not enabled at compile time. +This allows both AltiVec-equipped (PowerPC G4 and G5) and non-AltiVec-equipped +(PowerPC G3) CPUs to be supported using the same build of libjpeg-turbo. + +4. Fixed an error ("Bogus virtual array access") that occurred when attempting +to decompress a progressive JPEG image with a height less than or equal to one +iMCU (8 * the vertical sampling factor) using buffered-image mode with +interblock smoothing enabled. This was a regression introduced by +2.1 beta1[6(b)]. + +5. Fixed two issues that prevented partial image decompression from working +properly with buffered-image mode: + + - Attempting to call `jpeg_crop_scanline()` after +`jpeg_start_decompress()` but before `jpeg_start_output()` resulted in an error +("Improper call to JPEG library in state 207".) + - Attempting to use `jpeg_skip_scanlines()` resulted in an error ("Bogus +virtual array access") under certain circumstances. + + +2.1.3 +===== + +### Significant changes relative to 2.1.2: + +1. Fixed a regression introduced by 2.0 beta1[7] whereby cjpeg compressed PGM +input files into full-color JPEG images unless the `-grayscale` option was +used. + +2. cjpeg now automatically compresses GIF and 8-bit BMP input files into +grayscale JPEG images if the input files contain only shades of gray. + +3. The build system now enables the intrinsics implementation of the AArch64 +(Arm 64-bit) Neon SIMD extensions by default when using GCC 12 or later. + +4. Fixed a segfault that occurred while decompressing a 4:2:0 JPEG image using +the merged (non-fancy) upsampling algorithms (that is, with +`cinfo.do_fancy_upsampling` set to `FALSE`) along with `jpeg_crop_scanline()`. +Specifically, the segfault occurred if the number of bytes remaining in the +output buffer was less than the number of bytes required to represent one +uncropped scanline of the output image. For that reason, the issue could only +be reproduced using the libjpeg API, not using djpeg. + + +2.1.2 +===== + +### Significant changes relative to 2.1.1: + +1. Fixed a regression introduced by 2.1 beta1[13] that caused the remaining +GAS implementations of AArch64 (Arm 64-bit) Neon SIMD functions (which are used +by default with GCC for performance reasons) to be placed in the `.rodata` +section rather than in the `.text` section. This caused the GNU linker to +automatically place the `.rodata` section in an executable segment, which +prevented libjpeg-turbo from working properly with other linkers and also +represented a potential security risk. + +2. Fixed an issue whereby the `tjTransform()` function incorrectly computed the +MCU block size for 4:4:4 JPEG images with non-unary sampling factors and thus +unduly rejected some cropping regions, even though those regions aligned with +8x8 MCU block boundaries. + +3. Fixed a regression introduced by 2.1 beta1[13] that caused the build system +to enable the Arm Neon SIMD extensions when targetting Armv6 and other legacy +architectures that do not support Neon instructions. + +4. libjpeg-turbo now performs run-time detection of AltiVec instructions on +FreeBSD/PowerPC systems if AltiVec instructions are not enabled at compile +time. This allows both AltiVec-equipped and non-AltiVec-equipped CPUs to be +supported using the same build of libjpeg-turbo. + +5. cjpeg now accepts a `-strict` argument similar to that of djpeg and +jpegtran, which causes the compressor to abort if an LZW-compressed GIF input +image contains incomplete or corrupt image data. + + +2.1.1 +===== + +### Significant changes relative to 2.1.0: + +1. Fixed a regression introduced in 2.1.0 that caused build failures with +non-GCC-compatible compilers for Un*x/Arm platforms. + +2. Fixed a regression introduced by 2.1 beta1[13] that prevented the Arm 32-bit +(AArch32) Neon SIMD extensions from building unless the C compiler flags +included `-mfloat-abi=softfp` or `-mfloat-abi=hard`. + +3. Fixed an issue in the AArch32 Neon SIMD Huffman encoder whereby reliance on +undefined C compiler behavior led to crashes ("SIGBUS: illegal alignment") on +Android systems when running AArch32/Thumb builds of libjpeg-turbo built with +recent versions of Clang. + +4. Added a command-line argument (`-copy icc`) to jpegtran that causes it to +copy only the ICC profile markers from the source file and discard any other +metadata. + +5. libjpeg-turbo should now build and run on CHERI-enabled architectures, which +use capability pointers that are larger than the size of `size_t`. + +6. Fixed a regression (CVE-2021-37972) introduced by 2.1 beta1[5] that caused a +segfault in the 64-bit SSE2 Huffman encoder when attempting to losslessly +transform a specially-crafted malformed JPEG image. + + +2.1.0 +===== + +### Significant changes relative to 2.1 beta1: + +1. Fixed a regression introduced by 2.1 beta1[6(b)] whereby attempting to +decompress certain progressive JPEG images with one or more component planes of +width 8 or less caused a buffer overrun. + +2. Fixed a regression introduced by 2.1 beta1[6(b)] whereby attempting to +decompress a specially-crafted malformed progressive JPEG image caused the +block smoothing algorithm to read from uninitialized memory. + +3. Fixed an issue in the Arm Neon SIMD Huffman encoders that caused the +encoders to generate incorrect results when using the Clang compiler with +Visual Studio. + +4. Fixed a floating point exception (CVE-2021-20205) that occurred when +attempting to compress a specially-crafted malformed GIF image with a specified +image width of 0 using cjpeg. + +5. Fixed a regression introduced by 2.0 beta1[15] whereby attempting to +generate a progressive JPEG image on an SSE2-capable CPU using a scan script +containing one or more scans with lengths divisible by 32 and non-zero +successive approximation low bit positions would, under certain circumstances, +result in an error ("Missing Huffman code table entry") and an invalid JPEG +image. + +6. Introduced a new flag (`TJFLAG_LIMITSCANS` in the TurboJPEG C API and +`TJ.FLAG_LIMIT_SCANS` in the TurboJPEG Java API) and a corresponding TJBench +command-line argument (`-limitscans`) that causes the TurboJPEG decompression +and transform functions/operations to return/throw an error if a progressive +JPEG image contains an unreasonably large number of scans. This allows +applications that use the TurboJPEG API to guard against an exploit of the +progressive JPEG format described in the report +["Two Issues with the JPEG Standard"](https://libjpeg-turbo.org/pmwiki/uploads/About/TwoIssueswiththeJPEGStandard.pdf). + +7. The PPM reader now throws an error, rather than segfaulting (due to a buffer +overrun, CVE-2021-46822) or generating incorrect pixels, if an application +attempts to use the `tjLoadImage()` function to load a 16-bit binary PPM file +(a binary PPM file with a maximum value greater than 255) into a grayscale +image buffer or to load a 16-bit binary PGM file into an RGB image buffer. + +8. Fixed an issue in the PPM reader that caused incorrect pixels to be +generated when using the `tjLoadImage()` function to load a 16-bit binary PPM +file into an extended RGB image buffer. + +9. Fixed an issue whereby, if a JPEG buffer was automatically re-allocated by +one of the TurboJPEG compression or transform functions and an error +subsequently occurred during compression or transformation, the JPEG buffer +pointer passed by the application was not updated when the function returned. + + +2.0.90 (2.1 beta1) +================== + +### Significant changes relative to 2.0.6: + +1. The build system, x86-64 SIMD extensions, and accelerated Huffman codec now +support the x32 ABI on Linux, which allows for using x86-64 instructions with +32-bit pointers. The x32 ABI is generally enabled by adding `-mx32` to the +compiler flags. + + Caveats: + - CMake 3.9.0 or later is required in order for the build system to +automatically detect an x32 build. + - Java does not support the x32 ABI, and thus the TurboJPEG Java API will +automatically be disabled with x32 builds. + +2. Added Loongson MMI SIMD implementations of the RGB-to-grayscale, 4:2:2 fancy +chroma upsampling, 4:2:2 and 4:2:0 merged chroma upsampling/color conversion, +and fast integer DCT/IDCT algorithms. Relative to libjpeg-turbo 2.0.x, this +speeds up: + + - the compression of RGB source images into grayscale JPEG images by +approximately 20% + - the decompression of 4:2:2 JPEG images by approximately 40-60% when +using fancy upsampling + - the decompression of 4:2:2 and 4:2:0 JPEG images by approximately +15-20% when using merged upsampling + - the compression of RGB source images by approximately 30-45% when using +the fast integer DCT + - the decompression of JPEG images into RGB destination images by +approximately 2x when using the fast integer IDCT + + The overall decompression speedup for RGB images is now approximately +2.3-3.7x (compared to 2-3.5x with libjpeg-turbo 2.0.x.) + +3. 32-bit (Armv7 or Armv7s) iOS builds of libjpeg-turbo are no longer +supported, and the libjpeg-turbo build system can no longer be used to package +such builds. 32-bit iOS apps cannot run in iOS 11 and later, and the App Store +no longer allows them. + +4. 32-bit (i386) OS X/macOS builds of libjpeg-turbo are no longer supported, +and the libjpeg-turbo build system can no longer be used to package such +builds. 32-bit Mac applications cannot run in macOS 10.15 "Catalina" and +later, and the App Store no longer allows them. + +5. The SSE2 (x86 SIMD) and C Huffman encoding algorithms have been +significantly optimized, resulting in a measured average overall compression +speedup of 12-28% for 64-bit code and 22-52% for 32-bit code on various Intel +and AMD CPUs, as well as a measured average overall compression speedup of +0-23% on platforms that do not have a SIMD-accelerated Huffman encoding +implementation. + +6. The block smoothing algorithm that is applied by default when decompressing +progressive Huffman-encoded JPEG images has been improved in the following +ways: + + - The algorithm is now more fault-tolerant. Previously, if a particular +scan was incomplete, then the smoothing parameters for the incomplete scan +would be applied to the entire output image, including the parts of the image +that were generated by the prior (complete) scan. Visually, this had the +effect of removing block smoothing from lower-frequency scans if they were +followed by an incomplete higher-frequency scan. libjpeg-turbo now applies +block smoothing parameters to each iMCU row based on which scan generated the +pixels in that row, rather than always using the block smoothing parameters for +the most recent scan. + - When applying block smoothing to DC scans, a Gaussian-like kernel with a +5x5 window is used to reduce the "blocky" appearance. + +7. Added SIMD acceleration for progressive Huffman encoding on Arm platforms. +This speeds up the compression of full-color progressive JPEGs by about 30-40% +on average (relative to libjpeg-turbo 2.0.x) when using modern Arm CPUs. + +8. Added configure-time and run-time auto-detection of Loongson MMI SIMD +instructions, so that the Loongson MMI SIMD extensions can be included in any +MIPS64 libjpeg-turbo build. + +9. Added fault tolerance features to djpeg and jpegtran, mainly to demonstrate +methods by which applications can guard against the exploits of the JPEG format +described in the report +["Two Issues with the JPEG Standard"](https://libjpeg-turbo.org/pmwiki/uploads/About/TwoIssueswiththeJPEGStandard.pdf). + + - Both programs now accept a `-maxscans` argument, which can be used to +limit the number of allowable scans in the input file. + - Both programs now accept a `-strict` argument, which can be used to +treat all warnings as fatal. + +10. CMake package config files are now included for both the libjpeg and +TurboJPEG API libraries. This facilitates using libjpeg-turbo with CMake's +`find_package()` function. For example: + + find_package(libjpeg-turbo CONFIG REQUIRED) + + add_executable(libjpeg_program libjpeg_program.c) + target_link_libraries(libjpeg_program PUBLIC libjpeg-turbo::jpeg) + + add_executable(libjpeg_program_static libjpeg_program.c) + target_link_libraries(libjpeg_program_static PUBLIC + libjpeg-turbo::jpeg-static) + + add_executable(turbojpeg_program turbojpeg_program.c) + target_link_libraries(turbojpeg_program PUBLIC + libjpeg-turbo::turbojpeg) + + add_executable(turbojpeg_program_static turbojpeg_program.c) + target_link_libraries(turbojpeg_program_static PUBLIC + libjpeg-turbo::turbojpeg-static) + +11. Since the Unisys LZW patent has long expired, cjpeg and djpeg can now +read/write both LZW-compressed and uncompressed GIF files (feature ported from +jpeg-6a and jpeg-9d.) + +12. jpegtran now includes the `-wipe` and `-drop` options from jpeg-9a and +jpeg-9d, as well as the ability to expand the image size using the `-crop` +option. Refer to jpegtran.1 or usage.txt for more details. + +13. Added a complete intrinsics implementation of the Arm Neon SIMD extensions, +thus providing SIMD acceleration on Arm platforms for all of the algorithms +that are SIMD-accelerated on x86 platforms. This new implementation is +significantly faster in some cases than the old GAS implementation-- +depending on the algorithms used, the type of CPU core, and the compiler. GCC, +as of this writing, does not provide a full or optimal set of Neon intrinsics, +so for performance reasons, the default when building libjpeg-turbo with GCC is +to continue using the GAS implementation of the following algorithms: + + - 32-bit RGB-to-YCbCr color conversion + - 32-bit fast and accurate inverse DCT + - 64-bit RGB-to-YCbCr and YCbCr-to-RGB color conversion + - 64-bit accurate forward and inverse DCT + - 64-bit Huffman encoding + + A new CMake variable (`NEON_INTRINSICS`) can be used to override this +default. + + Since the new intrinsics implementation includes SIMD acceleration +for merged upsampling/color conversion, 1.5.1[5] is no longer necessary and has +been reverted. + +14. The Arm Neon SIMD extensions can now be built using Visual Studio. + +15. The build system can now be used to generate a universal x86-64 + Armv8 +libjpeg-turbo SDK package for both iOS and macOS. + + +2.0.6 +===== + +### Significant changes relative to 2.0.5: + +1. Fixed "using JNI after critical get" errors that occurred on Android +platforms when using any of the YUV encoding/compression/decompression/decoding +methods in the TurboJPEG Java API. + +2. Fixed or worked around multiple issues with `jpeg_skip_scanlines()`: + + - Fixed segfaults (CVE-2020-35538) or "Corrupt JPEG data: premature end of +data segment" errors in `jpeg_skip_scanlines()` that occurred when +decompressing 4:2:2 or 4:2:0 JPEG images using merged (non-fancy) +upsampling/color conversion (that is, when setting `cinfo.do_fancy_upsampling` +to `FALSE`.) 2.0.0[6] was a similar fix, but it did not cover all cases. + - `jpeg_skip_scanlines()` now throws an error if two-pass color +quantization is enabled. Two-pass color quantization never worked properly +with `jpeg_skip_scanlines()`, and the issues could not readily be fixed. + - Fixed an issue whereby `jpeg_skip_scanlines()` always returned 0 when +skipping past the end of an image. + +3. The Arm 64-bit (Armv8) Neon SIMD extensions can now be built using MinGW +toolchains targetting Arm64 (AArch64) Windows binaries. + +4. Fixed unexpected visual artifacts that occurred when using +`jpeg_crop_scanline()` and interblock smoothing while decompressing only the DC +scan of a progressive JPEG image. + +5. Fixed an issue whereby libjpeg-turbo would not build if 12-bit-per-component +JPEG support (`WITH_12BIT`) was enabled along with libjpeg v7 or libjpeg v8 +API/ABI emulation (`WITH_JPEG7` or `WITH_JPEG8`.) + + +2.0.5 +===== + +### Significant changes relative to 2.0.4: + +1. Worked around issues in the MIPS DSPr2 SIMD extensions that caused failures +in the libjpeg-turbo regression tests. Specifically, the +`jsimd_h2v1_downsample_dspr2()` and `jsimd_h2v2_downsample_dspr2()` functions +in the MIPS DSPr2 SIMD extensions are now disabled until/unless they can be +fixed, and other functions that are incompatible with big endian MIPS CPUs are +disabled when building libjpeg-turbo for such CPUs. + +2. Fixed an oversight in the `TJCompressor.compress(int)` method in the +TurboJPEG Java API that caused an error ("java.lang.IllegalStateException: No +source image is associated with this instance") when attempting to use that +method to compress a YUV image. + +3. Fixed an issue (CVE-2020-13790) in the PPM reader that caused a buffer +overrun in cjpeg, TJBench, or the `tjLoadImage()` function if one of the values +in a binary PPM/PGM input file exceeded the maximum value defined in the file's +header and that maximum value was less than 255. libjpeg-turbo 1.5.0 already +included a similar fix for binary PPM/PGM files with maximum values greater +than 255. + +4. The TurboJPEG API library's global error handler, which is used in functions +such as `tjBufSize()` and `tjLoadImage()` that do not require a TurboJPEG +instance handle, is now thread-safe on platforms that support thread-local +storage. + + 2.0.4 ===== @@ -24,17 +521,17 @@ JPEG images. This was known to cause a buffer overflow when attempting to decompress some such images using `tjDecompressToYUV2()` or `tjDecompressToYUVPlanes()`. -5. Fixed an issue, detected by ASan, whereby attempting to losslessly transform -a specially-crafted malformed JPEG image containing an extremely-high-frequency -coefficient block (junk image data that could never be generated by a -legitimate JPEG compressor) could cause the Huffman encoder's local buffer to -be overrun. (Refer to 1.4.0[9] and 1.4beta1[15].) Given that the buffer -overrun was fully contained within the stack and did not cause a segfault or -other user-visible errant behavior, and given that the lossless transformer -(unlike the decompressor) is not generally exposed to arbitrary data exploits, -this issue did not likely pose a security risk. - -6. The ARM 64-bit (ARMv8) NEON SIMD assembly code now stores constants in a +5. Fixed an issue (CVE-2020-17541), detected by ASan, whereby attempting to +losslessly transform a specially-crafted malformed JPEG image containing an +extremely-high-frequency coefficient block (junk image data that could never be +generated by a legitimate JPEG compressor) could cause the Huffman encoder's +local buffer to be overrun. (Refer to 1.4.0[9] and 1.4beta1[15].) Given that +the buffer overrun was fully contained within the stack and did not cause a +segfault or other user-visible errant behavior, and given that the lossless +transformer (unlike the decompressor) is not generally exposed to arbitrary +data exploits, this issue did not likely pose a security risk. + +6. The Arm 64-bit (Armv8) Neon SIMD assembly code now stores constants in a separate read-only data section rather than in the text section, to support execute-only memory layouts. @@ -216,7 +713,7 @@ detect actual security issues, should they arise in the future. 1. Added AVX2 SIMD implementations of the colorspace conversion, chroma downsampling and upsampling, integer quantization and sample conversion, and -slow integer DCT/IDCT algorithms. When using the slow integer DCT/IDCT +accurate integer DCT/IDCT algorithms. When using the accurate integer DCT/IDCT algorithms on AVX2-equipped CPUs, the compression of RGB images is approximately 13-36% (avg. 22%) faster (relative to libjpeg-turbo 1.5.x) with 64-bit code and 11-21% (avg. 17%) faster with 32-bit code, and the @@ -320,16 +817,16 @@ algorithm that caused incorrect dithering in the output image. This algorithm now produces bitwise-identical results to the unmerged algorithms. 12. The SIMD function symbols for x86[-64]/ELF, MIPS/ELF, macOS/x86[-64] (if -libjpeg-turbo is built with YASM), and iOS/ARM[64] builds are now private. +libjpeg-turbo is built with Yasm), and iOS/Arm[64] builds are now private. This prevents those symbols from being exposed in applications or shared libraries that link statically with libjpeg-turbo. 13. Added Loongson MMI SIMD implementations of the RGB-to-YCbCr and YCbCr-to-RGB colorspace conversion, 4:2:0 chroma downsampling, 4:2:0 fancy -chroma upsampling, integer quantization, and slow integer DCT/IDCT algorithms. -When using the slow integer DCT/IDCT, this speeds up the compression of RGB -images by approximately 70-100% and the decompression of RGB images by -approximately 2-3.5x. +chroma upsampling, integer quantization, and accurate integer DCT/IDCT +algorithms. When using the accurate integer DCT/IDCT, this speeds up the +compression of RGB images by approximately 70-100% and the decompression of RGB +images by approximately 2-3.5x. 14. Fixed a build error when building with older MinGW releases (regression caused by 1.5.1[7].) @@ -379,9 +876,9 @@ end of a single-scan (non-progressive) image, subsequent calls to `jpeg_consume_input()` would return `JPEG_SUSPENDED` rather than `JPEG_REACHED_EOI`. -9. `jpeg_crop_scanlines()` now works correctly when decompressing grayscale -JPEG images that were compressed with a sampling factor other than 1 (for -instance, with `cjpeg -grayscale -sample 2x2`). +9. `jpeg_crop_scanline()` now works correctly when decompressing grayscale JPEG +images that were compressed with a sampling factor other than 1 (for instance, +with `cjpeg -grayscale -sample 2x2`). 1.5.2 @@ -405,7 +902,7 @@ on PowerPC-based AmigaOS 4 and OpenBSD systems. 5. Fixed build and runtime errors on Windows that occurred when building libjpeg-turbo with libjpeg v7 API/ABI emulation and the in-memory source/destination managers. Due to an oversight, the `jpeg_skip_scanlines()` -and `jpeg_crop_scanlines()` functions were not being included in jpeg7.dll when +and `jpeg_crop_scanline()` functions were not being included in jpeg7.dll when libjpeg-turbo was built with `-DWITH_JPEG7=1` and `-DWITH_MEMSRCDST=1`. 6. Fixed "Bogus virtual array access" error that occurred when using the @@ -562,10 +1059,10 @@ application was linked against. 3. Fixed a couple of issues in the PPM reader that would cause buffer overruns in cjpeg if one of the values in a binary PPM/PGM input file exceeded the -maximum value defined in the file's header. libjpeg-turbo 1.4.2 already -included a similar fix for ASCII PPM/PGM files. Note that these issues were -not security bugs, since they were confined to the cjpeg program and did not -affect any of the libjpeg-turbo libraries. +maximum value defined in the file's header and that maximum value was greater +than 255. libjpeg-turbo 1.4.2 already included a similar fix for ASCII PPM/PGM +files. Note that these issues were not security bugs, since they were confined +to the cjpeg program and did not affect any of the libjpeg-turbo libraries. 4. Fixed an issue whereby attempting to decompress a JPEG file with a corrupt header using the `tjDecompressToYUV2()` function would cause the function to @@ -661,8 +1158,8 @@ benchmarking or regression testing, SIMD-accelerated Huffman encoding can be disabled by setting the `JSIMD_NOHUFFENC` environment variable to `1`. 13. Added ARM 64-bit (ARMv8) NEON SIMD implementations of the commonly-used -compression algorithms (including the slow integer forward DCT and h2v2 & h2v1 -downsampling algorithms, which are not accelerated in the 32-bit NEON +compression algorithms (including the accurate integer forward DCT and h2v2 & +h2v1 downsampling algorithms, which are not accelerated in the 32-bit NEON implementation.) This speeds up the compression of full-color JPEGs by about 75% on average on a Cavium ThunderX processor and by about 2-2.5x on average on Cortex-A53 and Cortex-A57 cores. @@ -793,8 +1290,8 @@ platforms other than Windows or Linux. Oops. 7. Fixed an extremely rare bug in the Huffman encoder that caused 64-bit builds of libjpeg-turbo to incorrectly encode a few specific test images when -quality=98, an optimized Huffman table, and the slow integer forward DCT were -used. +quality=98, an optimized Huffman table, and the accurate integer forward DCT +were used. 8. The Windows (CMake) build system now supports building only static or only shared libraries. This is accomplished by adding either `-DENABLE_STATIC=0` or @@ -953,8 +1450,8 @@ floating point inverse DCT (using code borrowed from libjpeg v8a and later.) The accuracy of this implementation now matches the accuracy of the SSE/SSE2 implementation. Note, however, that the floating point DCT/IDCT algorithms are mainly a legacy feature. They generally do not produce significantly better -accuracy than the slow integer DCT/IDCT algorithms, and they are quite a bit -slower. +accuracy than the accurate integer DCT/IDCT algorithms, and they are quite a +bit slower. 8. Added a new output colorspace (`JCS_RGB565`) to the libjpeg API that allows for decompressing JPEG images into RGB565 (16-bit) pixels. If dithering is not @@ -1205,8 +1702,8 @@ either the fast or the accurate DCT/IDCT algorithms in the underlying codec. ### Significant changes relative to 1.2 beta1: -1. Fixed build issue with YASM on Unix systems (the libjpeg-turbo build system -was not adding the current directory to the assembler include path, so YASM +1. Fixed build issue with Yasm on Unix systems (the libjpeg-turbo build system +was not adding the current directory to the assembler include path, so Yasm was not able to find jsimdcfg.inc.) 2. Fixed out-of-bounds read in SSE2 SIMD code that occurred when decompressing @@ -1274,7 +1771,7 @@ transposed or rotated 90 degrees. 8. All legacy VirtualGL code has been re-factored, and this has allowed libjpeg-turbo, in its entirety, to be re-licensed under a BSD-style license. -9. libjpeg-turbo can now be built with YASM. +9. libjpeg-turbo can now be built with Yasm. 10. Added SIMD acceleration for ARM Linux and iOS platforms that support NEON instructions. @@ -1364,8 +1861,8 @@ cases. 2. Despite the above, the fast integer forward DCT still degrades somewhat for JPEG qualities greater than 95, so the TurboJPEG wrapper will now automatically -use the slow integer forward DCT when generating JPEG images of quality 96 or -greater. This reduces compression performance by as much as 15% for these +use the accurate integer forward DCT when generating JPEG images of quality 96 +or greater. This reduces compression performance by as much as 15% for these high-quality images but is necessary to ensure that the images are perceptually lossless. It also ensures that the library can avoid the performance pitfall created by [1]. diff --git a/third-party/mozjpeg/mozjpeg/LICENSE.md b/third-party/mozjpeg/mozjpeg/LICENSE.md index 99c9aadcc47..bf8a7fda7f5 100644 --- a/third-party/mozjpeg/mozjpeg/LICENSE.md +++ b/third-party/mozjpeg/mozjpeg/LICENSE.md @@ -91,7 +91,7 @@ best of our understanding. The Modified (3-clause) BSD License =================================== -Copyright (C)2009-2020 D. R. Commander. All Rights Reserved. +Copyright (C)2009-2023 D. R. Commander. All Rights Reserved.
Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. Redistribution and use in source and binary forms, with or without diff --git a/third-party/mozjpeg/mozjpeg/README-mozilla.txt b/third-party/mozjpeg/mozjpeg/README-mozilla.txt index 7187f939ccb..11d2e5c28a3 100644 --- a/third-party/mozjpeg/mozjpeg/README-mozilla.txt +++ b/third-party/mozjpeg/mozjpeg/README-mozilla.txt @@ -24,7 +24,7 @@ mozjpeg's implementation of the libjpeg API includes an extensibility framework that allows new features to be added without modifying the transparent libjpeg compress/decompress structures (which would break backward ABI compatibility.) Extension parameters are placed into the opaque jpeg_comp_master structure, and -a set of accessor functions and globally unique tokens allows for +a set of accessor functions and globally unique tokens allows for getting/setting those parameters without directly accessing the structure. Currently, only the accessor functions necessary to support the mozjpeg @@ -185,7 +185,7 @@ Integer Extension Parameters Supported by mozjpeg 8 = Table from: An Improved Detection Model for DCT Coefficient Quantization (1993) Peterson, Ahumada and Watson -* JINT_DC_SCAN_OPT_MODE (default: 1) +* JINT_DC_SCAN_OPT_MODE (default: 0) Specifies the DC scan optimization mode. The following options are available: 0 = One scan for all components diff --git a/third-party/mozjpeg/mozjpeg/README.ijg b/third-party/mozjpeg/mozjpeg/README.ijg index 2e39f965c21..9453c195010 100644 --- a/third-party/mozjpeg/mozjpeg/README.ijg +++ b/third-party/mozjpeg/mozjpeg/README.ijg @@ -128,7 +128,7 @@ with respect to this software, its quality, accuracy, merchantability, or fitness for a particular purpose. This software is provided "AS IS", and you, its user, assume the entire risk as to its quality and accuracy. -This software is copyright (C) 1991-2016, Thomas G. Lane, Guido Vollbeding. +This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. All Rights Reserved except as specified below. Permission is hereby granted to use, copy, modify, and distribute this @@ -159,19 +159,6 @@ commercial products, provided that all warranty or liability claims are assumed by the product vendor. -The IJG distribution formerly included code to read and write GIF files. -To avoid entanglement with the Unisys LZW patent (now expired), GIF reading -support has been removed altogether, and the GIF writer has been simplified -to produce "uncompressed GIFs". This technique does not use the LZW -algorithm; the resulting GIF files are larger than usual, but are readable -by all standard GIF decoders. - -We are required to state that - "The Graphics Interchange Format(c) is the Copyright property of - CompuServe Incorporated. GIF(sm) is a Service Mark property of - CompuServe Incorporated." - - REFERENCES ========== @@ -223,12 +210,12 @@ https://www.iso.org/standard/54989.html and http://www.itu.int/rec/T-REC-T.871. A PDF file of the older JFIF 1.02 specification is available at http://www.w3.org/Graphics/JPEG/jfif3.pdf. -The TIFF 6.0 file format specification can be obtained by FTP from -ftp://ftp.sgi.com/graphics/tiff/TIFF6.ps.gz. The JPEG incorporation scheme -found in the TIFF 6.0 spec of 3-June-92 has a number of serious problems. -IJG does not recommend use of the TIFF 6.0 design (TIFF Compression tag 6). -Instead, we recommend the JPEG design proposed by TIFF Technical Note #2 -(Compression tag 7). Copies of this Note can be obtained from +The TIFF 6.0 file format specification can be obtained from +http://mirrors.ctan.org/graphics/tiff/TIFF6.ps.gz. The JPEG incorporation +scheme found in the TIFF 6.0 spec of 3-June-92 has a number of serious +problems. IJG does not recommend use of the TIFF 6.0 design (TIFF Compression +tag 6). Instead, we recommend the JPEG design proposed by TIFF Technical Note +#2 (Compression tag 7). Copies of this Note can be obtained from http://www.ijg.org/files/. It is expected that the next revision of the TIFF spec will replace the 6.0 JPEG design with the Note's design. Although IJG's own code does not support TIFF/JPEG, the free libtiff library @@ -243,14 +230,8 @@ The most recent released version can always be found there in directory "files". The JPEG FAQ (Frequently Asked Questions) article is a source of some -general information about JPEG. -It is available on the World Wide Web at http://www.faqs.org/faqs/jpeg-faq/ -and other news.answers archive sites, including the official news.answers -archive at rtfm.mit.edu: ftp://rtfm.mit.edu/pub/usenet/news.answers/jpeg-faq/. -If you don't have Web or FTP access, send e-mail to mail-server@rtfm.mit.edu -with body - send usenet/news.answers/jpeg-faq/part1 - send usenet/news.answers/jpeg-faq/part2 +general information about JPEG. It is available at +http://www.faqs.org/faqs/jpeg-faq. FILE FORMAT COMPATIBILITY diff --git a/third-party/mozjpeg/mozjpeg/README.md b/third-party/mozjpeg/mozjpeg/README.md index f536a94735d..a101ed8282e 100644 --- a/third-party/mozjpeg/mozjpeg/README.md +++ b/third-party/mozjpeg/mozjpeg/README.md @@ -1,13 +1,13 @@ Mozilla JPEG Encoder Project [![Build Status](https://ci.appveyor.com/api/projects/status/github/mozilla/mozjpeg?branch=master&svg=true)](https://ci.appveyor.com/project/kornel/mozjpeg-4ekrx) ============================ -MozJPEG reduces file sizes of JPEG images while retaining quality and compatibility with the vast majority of the world's deployed decoders. +MozJPEG improves JPEG compression efficiency achieving higher visual quality and smaller file sizes at the same time. It is compatible with the JPEG standard, and the vast majority of the world's deployed JPEG decoders. -MozJPEG is based on [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo). **Please send pull requests to libjpeg-turbo** if the changes aren't specific to newly-added MozJPEG-only compression code. This project aims to keep differences with libjpeg-turbo minimal, so whenever possible, improvements and bug fixes should go there first. +MozJPEG is a patch for [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo). **Please send pull requests to libjpeg-turbo** if the changes aren't specific to newly-added MozJPEG-only compression code. This project aims to keep differences with libjpeg-turbo minimal, so whenever possible, improvements and bug fixes should go there first. -It's compatible with libjpeg API and ABI, and can be used as a drop-in replacement for libjpeg. MozJPEG makes tradeoffs that are intended to benefit Web use cases and focuses solely on improving encoding, so it's best used as part of a Web encoding workflow. +MozJPEG is compatible with the libjpeg API and ABI. It is intended to be a drop-in replacement for libjpeg. MozJPEG is a strict superset of libjpeg-turbo's functionality. All MozJPEG's improvements can be disabled at run time, and in that case it behaves exactly like libjpeg-turbo. -MozJPEG is meant to be used as a library in graphics programs and image processing tools. We include a demo `cjpeg` tool, but it's not intended for serious use. We encourage authors of graphics programs to use MozJPEG's [C API](libjpeg.txt) instead. +MozJPEG is meant to be used as a library in graphics programs and image processing tools. We include a demo `cjpeg` command-line tool, but it's not intended for serious use. We encourage authors of graphics programs to use libjpeg's [C API](libjpeg.txt) and link with MozJPEG library instead. ## Features @@ -15,7 +15,7 @@ MozJPEG is meant to be used as a library in graphics programs and image processi * Trellis quantization. When converting other formats to JPEG it maximizes quality/filesize ratio. * Comes with new quantization table presets, e.g. tuned for high-resolution displays. * Fully compatible with all web browsers. -* Can be seamlessly integrated into any program using libjpeg. +* Can be seamlessly integrated into any program that uses the industry-standard libjpeg API. There's no need to write any MozJPEG-specific integration code. ## Releases @@ -26,4 +26,4 @@ MozJPEG is meant to be used as a library in graphics programs and image processi ## Compiling -See [BUILDING](BUILDING.md). +See [BUILDING](BUILDING.md). MozJPEG is built exactly the same way as libjpeg-turbo, so if you need additional help please consult [libjpeg-turbo documentation](https://libjpeg-turbo.org/). diff --git a/third-party/mozjpeg/mozjpeg/appveyor.yml b/third-party/mozjpeg/mozjpeg/appveyor.yml deleted file mode 100644 index df8808370f2..00000000000 --- a/third-party/mozjpeg/mozjpeg/appveyor.yml +++ /dev/null @@ -1,26 +0,0 @@ -image: Visual Studio 2017 -configuration: Release - -install: - ## Download nasm - - mkdir nasm - - cd nasm - - appveyor DownloadFile https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/win64/nasm-2.14.02-win64.zip -FileName nasm.zip - - 7z e -y nasm.zip - - set PATH=%PATH%;%CD% - ## Prepare cmake - - cd %APPVEYOR_BUILD_FOLDER% - - mkdir cmake_build - -before_build: - - cmake --version - - cd cmake_build - - cmake .. -G "Visual Studio 15 2017" -DPNG_SUPPORTED=NO - -build_script: - - cd %APPVEYOR_BUILD_FOLDER% - - msbuild cmake_build\mozjpeg.sln - -artifacts: - - path: cmake_build\**\Release\**\*.exe - - path: cmake_build\**\Release\**\*.lib diff --git a/third-party/mozjpeg/mozjpeg/cderror.h b/third-party/mozjpeg/mozjpeg/cderror.h index 24395fad895..1627f27bd24 100644 --- a/third-party/mozjpeg/mozjpeg/cderror.h +++ b/third-party/mozjpeg/mozjpeg/cderror.h @@ -1,9 +1,11 @@ /* * cderror.h * + * This file was part of the Independent JPEG Group's software: * Copyright (C) 1994-1997, Thomas G. Lane. * Modified 2009-2017 by Guido Vollbeding. - * This file is part of the Independent JPEG Group's software. + * libjpeg-turbo Modifications: + * Copyright (C) 2021, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -42,7 +44,7 @@ JMESSAGE(JMSG_FIRSTADDONCODE = 1000, NULL) /* Must be first entry! */ #ifdef BMP_SUPPORTED JMESSAGE(JERR_BMP_BADCMAP, "Unsupported BMP colormap format") -JMESSAGE(JERR_BMP_BADDEPTH, "Only 8- and 24-bit BMP files are supported") +JMESSAGE(JERR_BMP_BADDEPTH, "Only 8-, 24-, and 32-bit BMP files are supported") JMESSAGE(JERR_BMP_BADHEADER, "Invalid BMP file: bad header length") JMESSAGE(JERR_BMP_BADPLANES, "Invalid BMP file: biPlanes not equal to 1") JMESSAGE(JERR_BMP_COLORSPACE, "BMP output must be grayscale or RGB") @@ -50,9 +52,9 @@ JMESSAGE(JERR_BMP_COMPRESSED, "Sorry, compressed BMPs not yet supported") JMESSAGE(JERR_BMP_EMPTY, "Empty BMP image") JMESSAGE(JERR_BMP_NOT, "Not a BMP file - does not start with BM") JMESSAGE(JERR_BMP_OUTOFRANGE, "Numeric value out of range in BMP file") -JMESSAGE(JTRC_BMP, "%ux%u 24-bit BMP image") +JMESSAGE(JTRC_BMP, "%ux%u %d-bit BMP image") JMESSAGE(JTRC_BMP_MAPPED, "%ux%u 8-bit colormapped BMP image") -JMESSAGE(JTRC_BMP_OS2, "%ux%u 24-bit OS2 BMP image") +JMESSAGE(JTRC_BMP_OS2, "%ux%u %d-bit OS2 BMP image") JMESSAGE(JTRC_BMP_OS2_MAPPED, "%ux%u 8-bit colormapped OS2 BMP image") #endif /* BMP_SUPPORTED */ @@ -60,6 +62,7 @@ JMESSAGE(JTRC_BMP_OS2_MAPPED, "%ux%u 8-bit colormapped OS2 BMP image") JMESSAGE(JERR_GIF_BUG, "GIF output got confused") JMESSAGE(JERR_GIF_CODESIZE, "Bogus GIF codesize %d") JMESSAGE(JERR_GIF_COLORSPACE, "GIF output must be grayscale or RGB") +JMESSAGE(JERR_GIF_EMPTY, "Empty GIF image") JMESSAGE(JERR_GIF_IMAGENOTFOUND, "Too few images in GIF file") JMESSAGE(JERR_GIF_NOT, "Not a GIF file") JMESSAGE(JTRC_GIF, "%ux%ux%d GIF image") @@ -84,23 +87,6 @@ JMESSAGE(JTRC_PPM, "%ux%u PPM image") JMESSAGE(JTRC_PPM_TEXT, "%ux%u text PPM image") #endif /* PPM_SUPPORTED */ -#ifdef RLE_SUPPORTED -JMESSAGE(JERR_RLE_BADERROR, "Bogus error code from RLE library") -JMESSAGE(JERR_RLE_COLORSPACE, "RLE output must be grayscale or RGB") -JMESSAGE(JERR_RLE_DIMENSIONS, "Image dimensions (%ux%u) too large for RLE") -JMESSAGE(JERR_RLE_EMPTY, "Empty RLE file") -JMESSAGE(JERR_RLE_EOF, "Premature EOF in RLE header") -JMESSAGE(JERR_RLE_MEM, "Insufficient memory for RLE header") -JMESSAGE(JERR_RLE_NOT, "Not an RLE file") -JMESSAGE(JERR_RLE_TOOMANYCHANNELS, "Cannot handle %d output channels for RLE") -JMESSAGE(JERR_RLE_UNSUPPORTED, "Cannot handle this RLE setup") -JMESSAGE(JTRC_RLE, "%ux%u full-color RLE file") -JMESSAGE(JTRC_RLE_FULLMAP, "%ux%u full-color RLE file with map of length %d") -JMESSAGE(JTRC_RLE_GRAY, "%ux%u grayscale RLE file") -JMESSAGE(JTRC_RLE_MAPGRAY, "%ux%u grayscale RLE file with map of length %d") -JMESSAGE(JTRC_RLE_MAPPED, "%ux%u colormapped RLE file with map of length %d") -#endif /* RLE_SUPPORTED */ - #ifdef TARGA_SUPPORTED JMESSAGE(JERR_TGA_BADCMAP, "Unsupported Targa colormap format") JMESSAGE(JERR_TGA_BADPARMS, "Invalid or unsupported Targa file") diff --git a/third-party/mozjpeg/mozjpeg/cdjpeg.c b/third-party/mozjpeg/mozjpeg/cdjpeg.c index e0e382d0cdc..304a6650179 100644 --- a/third-party/mozjpeg/mozjpeg/cdjpeg.c +++ b/third-party/mozjpeg/mozjpeg/cdjpeg.c @@ -3,8 +3,8 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2019, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -25,26 +25,37 @@ * Optional progress monitor: display a percent-done figure on stderr. */ -#ifdef PROGRESS_REPORT - METHODDEF(void) progress_monitor(j_common_ptr cinfo) { cd_progress_ptr prog = (cd_progress_ptr)cinfo->progress; - int total_passes = prog->pub.total_passes + prog->total_extra_passes; - int percent_done = - (int)(prog->pub.pass_counter * 100L / prog->pub.pass_limit); - - if (percent_done != prog->percent_done) { - prog->percent_done = percent_done; - if (total_passes > 1) { - fprintf(stderr, "\rPass %d/%d: %3d%% ", - prog->pub.completed_passes + prog->completed_extra_passes + 1, - total_passes, percent_done); - } else { - fprintf(stderr, "\r %3d%% ", percent_done); + + if (prog->max_scans != 0 && cinfo->is_decompressor) { + int scan_no = ((j_decompress_ptr)cinfo)->input_scan_number; + + if (scan_no > (int)prog->max_scans) { + fprintf(stderr, "Scan number %d exceeds maximum scans (%u)\n", scan_no, + prog->max_scans); + exit(EXIT_FAILURE); + } + } + + if (prog->report) { + int total_passes = prog->pub.total_passes + prog->total_extra_passes; + int percent_done = + (int)(prog->pub.pass_counter * 100L / prog->pub.pass_limit); + + if (percent_done != prog->percent_done) { + prog->percent_done = percent_done; + if (total_passes > 1) { + fprintf(stderr, "\rPass %d/%d: %3d%% ", + prog->pub.completed_passes + prog->completed_extra_passes + 1, + total_passes, percent_done); + } else { + fprintf(stderr, "\r %3d%% ", percent_done); + } + fflush(stderr); } - fflush(stderr); } } @@ -57,6 +68,8 @@ start_progress_monitor(j_common_ptr cinfo, cd_progress_ptr progress) progress->pub.progress_monitor = progress_monitor; progress->completed_extra_passes = 0; progress->total_extra_passes = 0; + progress->max_scans = 0; + progress->report = FALSE; progress->percent_done = -1; cinfo->progress = &progress->pub; } @@ -73,8 +86,6 @@ end_progress_monitor(j_common_ptr cinfo) } } -#endif - /* * Case-insensitive matching of possibly-abbreviated keyword switches. diff --git a/third-party/mozjpeg/mozjpeg/cdjpeg.h b/third-party/mozjpeg/mozjpeg/cdjpeg.h index 6cfd8d88e1a..79e66240ee1 100644 --- a/third-party/mozjpeg/mozjpeg/cdjpeg.h +++ b/third-party/mozjpeg/mozjpeg/cdjpeg.h @@ -3,8 +3,9 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1994-1997, Thomas G. Lane. + * Modified 2019 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2017, D. R. Commander. + * Copyright (C) 2017, 2019, 2021, D. R. Commander. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. * For conditions of distribution and use, see the accompanying README.ijg file. @@ -37,7 +38,9 @@ struct cjpeg_source_struct { JSAMPARRAY buffer; JDIMENSION buffer_height; - +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + JDIMENSION max_pixels; +#endif #if JPEG_RAW_READER // For reading JPEG JSAMPARRAY plane_pointer[4]; @@ -65,9 +68,9 @@ struct djpeg_dest_struct { void (*finish_output) (j_decompress_ptr cinfo, djpeg_dest_ptr dinfo); /* Re-calculate buffer dimensions based on output dimensions (for use with partial image decompression.) If this is NULL, then the output format - does not support partial image decompression (BMP and RLE, in particular, - cannot support partial decompression because they use an inversion buffer - to write the image in bottom-up order.) */ + does not support partial image decompression (BMP, in particular, cannot + support partial decompression because it uses an inversion buffer to write + the image in bottom-up order.) */ void (*calc_buffer_dimensions) (j_decompress_ptr cinfo, djpeg_dest_ptr dinfo); @@ -96,6 +99,9 @@ struct cdjpeg_progress_mgr { struct jpeg_progress_mgr pub; /* fields known to JPEG library */ int completed_extra_passes; /* extra passes completed */ int total_extra_passes; /* total extra */ + JDIMENSION max_scans; /* abort if the number of scans exceeds this + value and the value is non-zero */ + boolean report; /* whether or not to report progress */ /* last printed percentage stored here to avoid multiple printouts */ int percent_done; }; @@ -112,21 +118,19 @@ EXTERN(cjpeg_source_ptr) jinit_read_bmp(j_compress_ptr cinfo, EXTERN(djpeg_dest_ptr) jinit_write_bmp(j_decompress_ptr cinfo, boolean is_os2, boolean use_inversion_array); EXTERN(cjpeg_source_ptr) jinit_read_gif(j_compress_ptr cinfo); -EXTERN(djpeg_dest_ptr) jinit_write_gif(j_decompress_ptr cinfo); +EXTERN(djpeg_dest_ptr) jinit_write_gif(j_decompress_ptr cinfo, boolean is_lzw); EXTERN(cjpeg_source_ptr) jinit_read_ppm(j_compress_ptr cinfo); EXTERN(djpeg_dest_ptr) jinit_write_ppm(j_decompress_ptr cinfo); -EXTERN(cjpeg_source_ptr) jinit_read_rle(j_compress_ptr cinfo); -EXTERN(djpeg_dest_ptr) jinit_write_rle(j_decompress_ptr cinfo); EXTERN(cjpeg_source_ptr) jinit_read_targa(j_compress_ptr cinfo); EXTERN(djpeg_dest_ptr) jinit_write_targa(j_decompress_ptr cinfo); /* cjpeg support routines (in rdswitch.c) */ EXTERN(boolean) read_quant_tables(j_compress_ptr cinfo, char *filename, - boolean force_baseline); + boolean force_baseline); EXTERN(boolean) read_scan_script(j_compress_ptr cinfo, char *filename); EXTERN(boolean) set_quality_ratings(j_compress_ptr cinfo, char *arg, - boolean force_baseline); + boolean force_baseline); EXTERN(boolean) set_quant_slots(j_compress_ptr cinfo, char *arg); EXTERN(boolean) set_sample_factors(j_compress_ptr cinfo, char *arg); @@ -136,9 +140,8 @@ EXTERN(void) read_color_map(j_decompress_ptr cinfo, FILE *infile); /* common support routines (in cdjpeg.c) */ -EXTERN(void) enable_signal_catcher(j_common_ptr cinfo); EXTERN(void) start_progress_monitor(j_common_ptr cinfo, - cd_progress_ptr progress); + cd_progress_ptr progress); EXTERN(void) end_progress_monitor(j_common_ptr cinfo); EXTERN(boolean) keymatch(char *arg, const char *keyword, int minchars); EXTERN(FILE *) read_stdin(void); diff --git a/third-party/mozjpeg/mozjpeg/change.log b/third-party/mozjpeg/mozjpeg/change.log index f090d7788c7..e4d0ddc4bd7 100644 --- a/third-party/mozjpeg/mozjpeg/change.log +++ b/third-party/mozjpeg/mozjpeg/change.log @@ -6,6 +6,25 @@ reference. Please see ChangeLog.md for information specific to libjpeg-turbo. CHANGE LOG for Independent JPEG Group's JPEG software +Version 9d 12-Jan-2020 +----------------------- + +Restore GIF read and write support from libjpeg version 6a. +Thank to Wolfgang Werner (W.W.) Heinz for suggestion. + +Add jpegtran -drop option; add options to the crop extension and wipe +to fill the extra area with content from the source image region, +instead of gray out. + + +Version 9c 14-Jan-2018 +----------------------- + +jpegtran: add an option to the -wipe switch to fill the region +with the average of adjacent blocks, instead of gray out. +Thank to Caitlyn Feddock and Maddie Ziegler for inspiration. + + Version 9b 17-Jan-2016 ----------------------- @@ -13,6 +32,13 @@ Document 'f' specifier for jpegtran -crop specification. Thank to Michele Martone for suggestion. +Version 9a 19-Jan-2014 +----------------------- + +Add jpegtran -wipe option and extension for -crop. +Thank to Andrew Senior, David Clunie, and Josef Schmid for suggestion. + + Version 9 13-Jan-2013 ---------------------- @@ -138,11 +164,6 @@ Huffman tables being used. Huffman tables are checked for validity much more carefully than before. -To avoid the Unisys LZW patent, djpeg's GIF output capability has been -changed to produce "uncompressed GIFs", and cjpeg's GIF input capability -has been removed altogether. We're not happy about it either, but there -seems to be no good alternative. - The configure script now supports building libjpeg as a shared library on many flavors of Unix (all the ones that GNU libtool knows how to build shared libraries for). Use "./configure --enable-shared" to diff --git a/third-party/mozjpeg/mozjpeg/cjpeg.1 b/third-party/mozjpeg/mozjpeg/cjpeg.1 index a3e47babd46..4bc4c8f9779 100644 --- a/third-party/mozjpeg/mozjpeg/cjpeg.1 +++ b/third-party/mozjpeg/mozjpeg/cjpeg.1 @@ -1,4 +1,4 @@ -.TH CJPEG 1 "18 March 2017" +.TH CJPEG 1 "30 November 2021" .SH NAME cjpeg \- compress an image file to a JPEG file .SH SYNOPSIS @@ -16,8 +16,7 @@ cjpeg \- compress an image file to a JPEG file compresses the named image file, or the standard input if no file is named, and produces a JPEG/JFIF file on the standard output. The currently supported input file formats are: PPM (PBMPLUS color -format), PGM (PBMPLUS grayscale format), BMP, Targa, and RLE (Utah Raster -Toolkit format). (RLE is supported only if the URT library is available.) +format), PGM (PBMPLUS grayscale format), BMP, GIF, and Targa. .SH OPTIONS All switch names may be abbreviated; for example, .B \-grayscale @@ -41,11 +40,7 @@ Scale quantization tables to adjust image quality. Quality is 0 (worst) to 100 (best); default is 75. (See below for more info.) .TP .B \-grayscale -Create monochrome JPEG file from color input. Be sure to use this switch when -compressing a grayscale BMP file, because -.B cjpeg -isn't bright enough to notice whether a BMP file uses only shades of gray. -By saying +Create monochrome JPEG file from color input. By saying .BR \-grayscale, you'll get a smaller JPEG file that takes less time to process. .TP @@ -161,31 +156,40 @@ arithmetic coded JPEG is not yet widely implemented, so many decoders will be unable to view an arithmetic coded JPEG file at all. .TP .B \-dct int -Use integer DCT method (default). +Use accurate integer DCT method (default). .TP .B \-dct fast -Use fast integer DCT (less accurate). -In libjpeg-turbo, the fast method is generally about 5-15% faster than the int -method when using the x86/x86-64 SIMD extensions (results may vary with other -SIMD implementations, or when using libjpeg-turbo without SIMD extensions.) +Use less accurate integer DCT method [legacy feature]. +When the Independent JPEG Group's software was first released in 1991, the +compression time for a 1-megapixel JPEG image on a mainstream PC was measured +in minutes. Thus, the \fBfast\fR integer DCT algorithm provided noticeable +performance benefits. On modern CPUs running libjpeg-turbo, however, the +compression time for a 1-megapixel JPEG image is measured in milliseconds, and +thus the performance benefits of the \fBfast\fR algorithm are much less +noticeable. On modern x86/x86-64 CPUs that support AVX2 instructions, the +\fBfast\fR and \fBint\fR methods have similar performance. On other types of +CPUs, the \fBfast\fR method is generally about 5-15% faster than the \fBint\fR +method. + For quality levels of 90 and below, there should be little or no perceptible -difference between the two algorithms. For quality levels above 90, however, -the difference between the fast and the int methods becomes more pronounced. -With quality=97, for instance, the fast method incurs generally about a 1-3 dB -loss (in PSNR) relative to the int method, but this can be larger for some -images. Do not use the fast method with quality levels above 97. The -algorithm often degenerates at quality=98 and above and can actually produce a -more lossy image than if lower quality levels had been used. Also, in -libjpeg-turbo, the fast method is not fully accelerated for quality levels -above 97, so it will be slower than the int method. +quality difference between the two algorithms. For quality levels above 90, +however, the difference between the \fBfast\fR and \fBint\fR methods becomes +more pronounced. With quality=97, for instance, the \fBfast\fR method incurs +generally about a 1-3 dB loss in PSNR relative to the \fBint\fR method, but +this can be larger for some images. Do not use the \fBfast\fR method with +quality levels above 97. The algorithm often degenerates at quality=98 and +above and can actually produce a more lossy image than if lower quality levels +had been used. Also, in libjpeg-turbo, the \fBfast\fR method is not fully +accelerated for quality levels above 97, so it will be slower than the +\fBint\fR method. .TP .B \-dct float -Use floating-point DCT method. -The float method is mainly a legacy feature. It does not produce significantly -more accurate results than the int method, and it is much slower. The float -method may also give different results on different machines due to varying -roundoff behavior, whereas the integer methods should give the same results on -all machines. +Use floating-point DCT method [legacy feature]. +The \fBfloat\fR method does not produce significantly more accurate results +than the \fBint\fR method, and it is much slower. The \fBfloat\fR method may +also give different results on different machines due to varying roundoff +behavior, whereas the integer methods should give the same results on all +machines. .TP .BI \-icc " file" Embed ICC color management profile contained in the specified file. @@ -215,6 +219,14 @@ Compress to memory instead of a file. This feature was implemented mainly as a way of testing the in-memory destination manager (jpeg_mem_dest()), but it is also useful for benchmarking, since it reduces the I/O overhead. .TP +.BI \-report +Report compression progress. +.TP +.BI \-strict +Treat all warnings as fatal. Enabling this option will cause the compressor to +abort if an LZW-compressed GIF input image contains incomplete or corrupt image +data. +.TP .B \-verbose Enable debug printout. More .BR \-v 's @@ -341,11 +353,6 @@ This file was modified by The libjpeg-turbo Project to include only information relevant to libjpeg-turbo, to wordsmith certain sections, and to describe features not present in libjpeg. .SH ISSUES -Support for GIF input files was removed in cjpeg v6b due to concerns over -the Unisys LZW patent. Although this patent expired in 2006, cjpeg still -lacks GIF support, for these historical reasons. (Conversion of GIF files to -JPEG is usually a bad idea anyway, since GIF is a 256-color format.) -.PP Not all variants of BMP and Targa file formats are supported. .PP The diff --git a/third-party/mozjpeg/mozjpeg/cjpeg.c b/third-party/mozjpeg/mozjpeg/cjpeg.c index 99dd5c7d350..ad7b4f96c14 100644 --- a/third-party/mozjpeg/mozjpeg/cjpeg.c +++ b/third-party/mozjpeg/mozjpeg/cjpeg.c @@ -5,7 +5,7 @@ * Copyright (C) 1991-1998, Thomas G. Lane. * Modified 2003-2011 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2010, 2013-2014, 2017, D. R. Commander. + * Copyright (C) 2010, 2013-2014, 2017, 2019-2022, D. R. Commander. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. * For conditions of distribution and use, see the accompanying README file. @@ -28,24 +28,16 @@ * works regardless of which command line style is used. */ -#include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ -#include "jversion.h" /* for version message */ -#include "jconfigint.h" - -#ifndef HAVE_STDLIB_H /* should declare malloc(),free() */ -extern void *malloc(size_t size); -extern void free(void *ptr); +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE #endif -#ifdef USE_CCOMMAND /* command-line reader for Macintosh */ -#ifdef __MWERKS__ -#include /* Metrowerks needs this */ -#include /* ... and this */ -#endif -#ifdef THINK_C -#include /* Think declares it here */ -#endif +#ifdef CJPEG_FUZZER +#define JPEG_INTERNALS #endif +#include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ +#include "jversion.h" /* for version message */ +#include "jconfigint.h" /* Create the add-on message string table. */ @@ -70,9 +62,9 @@ static const char * const cdjpeg_message_table[] = { * 2) assume we can push back more than one character (works in * some C implementations, but unportable); * 3) provide our own buffering (breaks input readers that want to use - * stdio directly, such as the RLE library); + * stdio directly); * or 4) don't put back the data, and modify the input_init methods to assume - * they start reading after the start of file (also breaks RLE library). + * they start reading after the start of file. * #1 is attractive for MS-DOS but is untenable on Unix. * * The most portable solution for file types that can't be identified by their @@ -124,10 +116,6 @@ select_file_type(j_compress_ptr cinfo, FILE *infile) copy_markers = TRUE; return jinit_read_png(cinfo); #endif -#ifdef RLE_SUPPORTED - case 'R': - return jinit_read_rle(cinfo); -#endif #ifdef TARGA_SUPPORTED case 0x00: return jinit_read_targa(cinfo); @@ -158,6 +146,47 @@ static const char *progname; /* program name for error messages */ static char *icc_filename; /* for -icc switch */ static char *outfilename; /* for -outfile switch */ boolean memdst; /* for -memdst switch */ +boolean report; /* for -report switch */ +boolean strict; /* for -strict switch */ + + +#ifdef CJPEG_FUZZER + +#include + +struct my_error_mgr { + struct jpeg_error_mgr pub; + jmp_buf setjmp_buffer; +}; + +void my_error_exit(j_common_ptr cinfo) +{ + struct my_error_mgr *myerr = (struct my_error_mgr *)cinfo->err; + + longjmp(myerr->setjmp_buffer, 1); +} + +static void my_emit_message_fuzzer(j_common_ptr cinfo, int msg_level) +{ + if (msg_level < 0) + cinfo->err->num_warnings++; +} + +#define HANDLE_ERROR() { \ + if (cinfo.global_state > CSTATE_START) { \ + if (memdst && outbuffer) \ + (*cinfo.dest->term_destination) (&cinfo); \ + jpeg_abort_compress(&cinfo); \ + } \ + jpeg_destroy_compress(&cinfo); \ + if (input_file != stdin && input_file != NULL) \ + fclose(input_file); \ + if (memdst) \ + free(outbuffer); \ + return EXIT_FAILURE; \ +} + +#endif LOCAL(void) @@ -207,25 +236,28 @@ usage(void) fprintf(stderr, " -arithmetic Use arithmetic coding\n"); #endif #ifdef DCT_ISLOW_SUPPORTED - fprintf(stderr, " -dct int Use integer DCT method%s\n", + fprintf(stderr, " -dct int Use accurate integer DCT method%s\n", (JDCT_DEFAULT == JDCT_ISLOW ? " (default)" : "")); #endif #ifdef DCT_IFAST_SUPPORTED - fprintf(stderr, " -dct fast Use fast integer DCT (less accurate)%s\n", + fprintf(stderr, " -dct fast Use less accurate integer DCT method [legacy feature]%s\n", (JDCT_DEFAULT == JDCT_IFAST ? " (default)" : "")); #endif #ifdef DCT_FLOAT_SUPPORTED - fprintf(stderr, " -dct float Use floating-point DCT method%s\n", + fprintf(stderr, " -dct float Use floating-point DCT method [legacy feature]%s\n", (JDCT_DEFAULT == JDCT_FLOAT ? " (default)" : "")); #endif fprintf(stderr, " -quant-baseline Use 8-bit quantization table entries for baseline JPEG compatibility\n"); fprintf(stderr, " -quant-table N Use predefined quantization table N:\n"); fprintf(stderr, " - 0 JPEG Annex K\n"); fprintf(stderr, " - 1 Flat\n"); - fprintf(stderr, " - 2 Custom, tuned for MS-SSIM\n"); - fprintf(stderr, " - 3 ImageMagick table by N. Robidoux\n"); - fprintf(stderr, " - 4 Custom, tuned for PSNR-HVS\n"); + fprintf(stderr, " - 2 Tuned for MS-SSIM on Kodak image set\n"); + fprintf(stderr, " - 3 ImageMagick table by N. Robidoux (default)\n"); + fprintf(stderr, " - 4 Tuned for PSNR-HVS on Kodak image set\n"); fprintf(stderr, " - 5 Table from paper by Klein, Silverstein and Carney\n"); + fprintf(stderr, " - 6 Table from paper by Watson, Taylor and Borthwick\n"); + fprintf(stderr, " - 7 Table from paper by Ahumada, Watson, Peterson\n"); + fprintf(stderr, " - 8 Table from paper by Peterson, Ahumada and Watson\n"); fprintf(stderr, " -icc FILE Embed ICC profile contained in FILE\n"); fprintf(stderr, " -restart N Set restart interval in rows, or in blocks with B\n"); #ifdef INPUT_SMOOTHING_SUPPORTED @@ -236,6 +268,8 @@ usage(void) #if JPEG_LIB_VERSION >= 80 || defined(MEM_SRCDST_SUPPORTED) fprintf(stderr, " -memdst Compress to memory instead of file (useful for benchmarking)\n"); #endif + fprintf(stderr, " -report Report compression progress\n"); + fprintf(stderr, " -strict Treat all warnings as fatal\n"); fprintf(stderr, " -verbose or -debug Emit debug output\n"); fprintf(stderr, " -version Print version information and exit\n"); fprintf(stderr, "Switches for wizards:\n"); @@ -251,7 +285,7 @@ usage(void) LOCAL(int) parse_switches(j_compress_ptr cinfo, int argc, char **argv, - int last_file_arg_seen, boolean for_real) + int last_file_arg_seen, boolean for_real) /* Parse optional switches. * Returns argv[] index of first file-name argument (== argc if none). * Any file names with indexes <= last_file_arg_seen are ignored; @@ -283,6 +317,8 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, icc_filename = NULL; outfilename = NULL; memdst = FALSE; + report = FALSE; + strict = FALSE; cinfo->err->trace_level = 0; /* Scan command line options, adjust parameters */ @@ -470,6 +506,8 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, qtablefile = argv[argn]; /* We postpone actually reading the file in case -quality comes later. */ + } else if (keymatch(arg, "report", 3)) { + report = TRUE; } else if (keymatch(arg, "quant-table", 7)) { int val; if (++argn >= argc) /* advance to next argument */ @@ -485,7 +523,7 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, } else if (keymatch(arg, "quant-baseline", 7)) { /* Force quantization table to meet baseline requirements */ force_baseline = TRUE; - + } else if (keymatch(arg, "restart", 1)) { /* Restart interval in MCU rows (or in MCUs with 'b'). */ long lval; @@ -545,6 +583,9 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, usage(); cinfo->smoothing_factor = val; + } else if (keymatch(arg, "strict", 2)) { + strict = TRUE; + } else if (keymatch(arg, "targa", 1)) { /* Input file is Targa format. */ is_targa = TRUE; @@ -653,6 +694,19 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, } +METHODDEF(void) +my_emit_message(j_common_ptr cinfo, int msg_level) +{ + if (msg_level < 0) { + /* Treat warning as fatal */ + cinfo->err->error_exit(cinfo); + } else { + if (cinfo->err->trace_level >= msg_level) + cinfo->err->output_message(cinfo); + } +} + + /* * The main program. */ @@ -661,13 +715,16 @@ int main(int argc, char **argv) { struct jpeg_compress_struct cinfo; +#ifdef CJPEG_FUZZER + struct my_error_mgr myerr; + struct jpeg_error_mgr &jerr = myerr.pub; +#else struct jpeg_error_mgr jerr; -#ifdef PROGRESS_REPORT - struct cdjpeg_progress_mgr progress; #endif + struct cdjpeg_progress_mgr progress; int file_index; cjpeg_source_ptr src_mgr; - FILE *input_file; + FILE *input_file = NULL; FILE *icc_file; JOCTET *icc_profile = NULL; long icc_len = 0; @@ -676,11 +733,6 @@ main(int argc, char **argv) unsigned long outsize = 0; JDIMENSION num_scanlines; - /* On Mac, fetch a command line. */ -#ifdef USE_CCOMMAND - argc = ccommand(&argv); -#endif - progname = argv[0]; if (progname == NULL || progname[0] == 0) progname = "cjpeg"; /* in case C library doesn't provide it */ @@ -710,6 +762,9 @@ main(int argc, char **argv) file_index = parse_switches(&cinfo, argc, argv, 0, FALSE); + if (strict) + jerr.emit_message = my_emit_message; + #ifdef TWO_FILE_COMMANDLINE if (!memdst) { /* Must have either -outfile switch or explicit output file name */ @@ -785,13 +840,24 @@ main(int argc, char **argv) fclose(icc_file); } -#ifdef PROGRESS_REPORT - start_progress_monitor((j_common_ptr)&cinfo, &progress); +#ifdef CJPEG_FUZZER + jerr.error_exit = my_error_exit; + jerr.emit_message = my_emit_message_fuzzer; + if (setjmp(myerr.setjmp_buffer)) + HANDLE_ERROR() #endif + if (report) { + start_progress_monitor((j_common_ptr)&cinfo, &progress); + progress.report = report; + } + /* Figure out the input file format, and set up to read it. */ src_mgr = select_file_type(&cinfo, input_file); src_mgr->input_file = input_file; +#ifdef CJPEG_FUZZER + src_mgr->max_pixels = 1048576; +#endif /* Read the input file header to obtain file size & colorspace. */ (*src_mgr->start_input) (&cinfo, src_mgr); @@ -813,6 +879,11 @@ main(int argc, char **argv) #endif jpeg_stdio_dest(&cinfo, output_file); +#ifdef CJPEG_FUZZER + if (setjmp(myerr.setjmp_buffer)) + HANDLE_ERROR() +#endif + /* Start compressor */ jpeg_start_compress(&cinfo, TRUE); @@ -850,7 +921,7 @@ main(int argc, char **argv) } if (icc_profile != NULL) jpeg_write_icc_profile(&cinfo, icc_profile, (unsigned int)icc_len); - + /* Process data */ while (cinfo.next_scanline < cinfo.image_height) { num_scanlines = (*src_mgr->get_pixel_rows) (&cinfo, src_mgr); @@ -873,18 +944,18 @@ main(int argc, char **argv) if (output_file != stdout && output_file != NULL) fclose(output_file); -#ifdef PROGRESS_REPORT - end_progress_monitor((j_common_ptr)&cinfo); -#endif + if (report) + end_progress_monitor((j_common_ptr)&cinfo); if (memdst) { +#ifndef CJPEG_FUZZER fprintf(stderr, "Compressed size: %lu bytes\n", outsize); +#endif free(outbuffer); } free(icc_profile); /* All done. */ - exit(jerr.num_warnings ? EXIT_WARNING : EXIT_SUCCESS); - return 0; /* suppress no-return-value warnings */ + return (jerr.num_warnings ? EXIT_WARNING : EXIT_SUCCESS); } diff --git a/third-party/mozjpeg/mozjpeg/cmakescripts/BuildPackages.cmake b/third-party/mozjpeg/mozjpeg/cmakescripts/BuildPackages.cmake index 395dd9895c7..2e0170f5170 100644 --- a/third-party/mozjpeg/mozjpeg/cmakescripts/BuildPackages.cmake +++ b/third-party/mozjpeg/mozjpeg/cmakescripts/BuildPackages.cmake @@ -1,18 +1,6 @@ # This file is included from the top-level CMakeLists.txt. We just store it # here to avoid cluttering up that file. -set(PKGNAME ${CMAKE_PROJECT_NAME} CACHE STRING - "Distribution package name (default: ${CMAKE_PROJECT_NAME})") -set(PKGVENDOR "The ${CMAKE_PROJECT_NAME} Project" CACHE STRING - "Vendor name to be included in distribution package descriptions (default: The ${CMAKE_PROJECT_NAME} Project)") -set(PKGURL "http://www.${CMAKE_PROJECT_NAME}.org" CACHE STRING - "URL of project web site to be included in distribution package descriptions (default: http://www.${CMAKE_PROJECT_NAME}.org)") -set(PKGEMAIL "information@${CMAKE_PROJECT_NAME}.org" CACHE STRING - "E-mail of project maintainer to be included in distribution package descriptions (default: information@${CMAKE_PROJECT_NAME}.org") -set(PKGID "com.${CMAKE_PROJECT_NAME}.${PKGNAME}" CACHE STRING - "Globally unique package identifier (reverse DNS notation) (default: com.${CMAKE_PROJECT_NAME}.${PKGNAME})") - - ############################################################################### # Linux RPM and DEB ############################################################################### @@ -22,12 +10,21 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(RPMARCH ${CMAKE_SYSTEM_PROCESSOR}) if(CPU_TYPE STREQUAL "x86_64") set(DEBARCH amd64) -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "armv7*") - set(DEBARCH armhf) elseif(CPU_TYPE STREQUAL "arm64") set(DEBARCH ${CPU_TYPE}) elseif(CPU_TYPE STREQUAL "arm") - set(DEBARCH armel) + check_c_source_compiles(" + #if __ARM_PCS_VFP != 1 + #error \"float ABI != hard\" + #endif + int main(void) { return 0; }" HAVE_HARD_FLOAT) + if(HAVE_HARD_FLOAT) + set(RPMARCH armv7hl) + set(DEBARCH armhf) + else() + set(RPMARCH armel) + set(DEBARCH armel) + endif() elseif(CMAKE_SYSTEM_PROCESSOR_LC STREQUAL "ppc64le") set(DEBARCH ppc64el) elseif(CPU_TYPE STREQUAL "powerpc" AND BITS EQUAL 32) @@ -45,19 +42,19 @@ boolean_number(CMAKE_POSITION_INDEPENDENT_CODE) configure_file(release/makerpm.in pkgscripts/makerpm) configure_file(release/rpm.spec.in pkgscripts/rpm.spec @ONLY) -add_custom_target(rpm sh pkgscripts/makerpm +add_custom_target(rpm pkgscripts/makerpm SOURCES pkgscripts/makerpm) configure_file(release/makesrpm.in pkgscripts/makesrpm) -add_custom_target(srpm sh pkgscripts/makesrpm +add_custom_target(srpm pkgscripts/makesrpm SOURCES pkgscripts/makesrpm DEPENDS dist) configure_file(release/makedpkg.in pkgscripts/makedpkg) configure_file(release/deb-control.in pkgscripts/deb-control) -add_custom_target(deb sh pkgscripts/makedpkg +add_custom_target(deb pkgscripts/makedpkg SOURCES pkgscripts/makedpkg) endif() # Linux @@ -71,12 +68,14 @@ if(WIN32) if(MSVC) set(INST_PLATFORM "Visual C++") - set(INST_NAME ${CMAKE_PROJECT_NAME}-${VERSION}-vc) + set(INST_ID vc) + set(INST_NAME ${CMAKE_PROJECT_NAME}-${VERSION}-${INST_ID}) set(INST_REG_NAME ${CMAKE_PROJECT_NAME}) elseif(MINGW) set(INST_PLATFORM GCC) - set(INST_NAME ${CMAKE_PROJECT_NAME}-${VERSION}-gcc) - set(INST_REG_NAME ${CMAKE_PROJECT_NAME}-gcc) + set(INST_ID gcc) + set(INST_NAME ${CMAKE_PROJECT_NAME}-${VERSION}-${INST_ID}) + set(INST_REG_NAME ${CMAKE_PROJECT_NAME}-${INST_ID}) set(INST_DEFS -DGCC) endif() @@ -91,7 +90,7 @@ if(WITH_JAVA) set(INST_DEFS ${INST_DEFS} -DJAVA) endif() -if(MSVC_IDE) +if(GENERATOR_IS_MULTI_CONFIG) set(INST_DEFS ${INST_DEFS} "-DBUILDDIR=${CMAKE_CFG_INTDIR}\\") else() set(INST_DEFS ${INST_DEFS} "-DBUILDDIR=") @@ -100,64 +99,48 @@ endif() string(REGEX REPLACE "/" "\\\\" INST_DIR ${CMAKE_INSTALL_PREFIX}) configure_file(release/installer.nsi.in installer.nsi @ONLY) +# TODO: It would be nice to eventually switch to CPack and eliminate this mess, +# but not today. +configure_file(win/projectTargets.cmake.in + win/${CMAKE_PROJECT_NAME}Targets.cmake @ONLY) +configure_file(win/${INST_ID}/projectTargets-release.cmake.in + win/${CMAKE_PROJECT_NAME}Targets-release.cmake @ONLY) if(WITH_JAVA) set(JAVA_DEPEND turbojpeg-java) endif() +if(WITH_TURBOJPEG) + set(TURBOJPEG_DEPEND turbojpeg turbojpeg-static tjbench) +endif() add_custom_target(installer makensis -nocd ${INST_DEFS} installer.nsi - DEPENDS jpeg jpeg-static turbojpeg turbojpeg-static rdjpgcom wrjpgcom - cjpeg djpeg jpegtran tjbench ${JAVA_DEPEND} + DEPENDS jpeg jpeg-static rdjpgcom wrjpgcom cjpeg djpeg jpegtran + ${JAVA_DEPEND} ${TURBOJPEG_DEPEND} SOURCES installer.nsi) endif() # WIN32 -############################################################################### -# Cygwin Package -############################################################################### - -if(CYGWIN) - -configure_file(release/makecygwinpkg.in pkgscripts/makecygwinpkg) - -add_custom_target(cygwinpkg sh pkgscripts/makecygwinpkg) - -endif() # CYGWIN - - ############################################################################### # Mac DMG ############################################################################### if(APPLE) -set(DEFAULT_OSX_32BIT_BUILD ${CMAKE_SOURCE_DIR}/osxx86) -set(OSX_32BIT_BUILD ${DEFAULT_OSX_32BIT_BUILD} CACHE PATH - "Directory containing 32-bit (i386) Mac build to include in universal binaries (default: ${DEFAULT_OSX_32BIT_BUILD})") -set(DEFAULT_IOS_ARMV7_BUILD ${CMAKE_SOURCE_DIR}/iosarmv7) -set(IOS_ARMV7_BUILD ${DEFAULT_IOS_ARMV7_BUILD} CACHE PATH - "Directory containing ARMv7 iOS build to include in universal binaries (default: ${DEFAULT_IOS_ARMV7_BUILD})") -set(DEFAULT_IOS_ARMV7S_BUILD ${CMAKE_SOURCE_DIR}/iosarmv7s) -set(IOS_ARMV7S_BUILD ${DEFAULT_IOS_ARMV7S_BUILD} CACHE PATH - "Directory containing ARMv7s iOS build to include in universal binaries (default: ${DEFAULT_IOS_ARMV7S_BUILD})") -set(DEFAULT_IOS_ARMV8_BUILD ${CMAKE_SOURCE_DIR}/iosarmv8) -set(IOS_ARMV8_BUILD ${DEFAULT_IOS_ARMV8_BUILD} CACHE PATH - "Directory containing ARMv8 iOS build to include in universal binaries (default: ${DEFAULT_IOS_ARMV8_BUILD})") - -set(OSX_APP_CERT_NAME "" CACHE STRING +set(ARMV8_BUILD "" CACHE PATH + "Directory containing Armv8 iOS or macOS build to include in universal binaries") + +set(MACOS_APP_CERT_NAME "" CACHE STRING "Name of the Developer ID Application certificate (in the macOS keychain) that should be used to sign the libjpeg-turbo DMG. Leave this blank to generate an unsigned DMG.") -set(OSX_INST_CERT_NAME "" CACHE STRING +set(MACOS_INST_CERT_NAME "" CACHE STRING "Name of the Developer ID Installer certificate (in the macOS keychain) that should be used to sign the libjpeg-turbo installer package. Leave this blank to generate an unsigned package.") configure_file(release/makemacpkg.in pkgscripts/makemacpkg) configure_file(release/Distribution.xml.in pkgscripts/Distribution.xml) +configure_file(release/Welcome.rtf.in pkgscripts/Welcome.rtf) configure_file(release/uninstall.in pkgscripts/uninstall) -add_custom_target(dmg sh pkgscripts/makemacpkg - SOURCES pkgscripts/makemacpkg) - -add_custom_target(udmg sh pkgscripts/makemacpkg universal +add_custom_target(dmg pkgscripts/makemacpkg SOURCES pkgscripts/makemacpkg) endif() # APPLE @@ -174,9 +157,20 @@ add_custom_target(dist configure_file(release/maketarball.in pkgscripts/maketarball) -add_custom_target(tarball sh pkgscripts/maketarball +add_custom_target(tarball pkgscripts/maketarball SOURCES pkgscripts/maketarball) configure_file(release/libjpeg.pc.in pkgscripts/libjpeg.pc @ONLY) -configure_file(release/libturbojpeg.pc.in pkgscripts/libturbojpeg.pc @ONLY) +if(WITH_TURBOJPEG) + configure_file(release/libturbojpeg.pc.in pkgscripts/libturbojpeg.pc @ONLY) +endif() + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + pkgscripts/${CMAKE_PROJECT_NAME}ConfigVersion.cmake + VERSION ${VERSION} COMPATIBILITY AnyNewerVersion) + +configure_package_config_file(release/Config.cmake.in + pkgscripts/${CMAKE_PROJECT_NAME}Config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CMAKE_PROJECT_NAME}) diff --git a/third-party/mozjpeg/mozjpeg/cmakescripts/GNUInstallDirs.cmake b/third-party/mozjpeg/mozjpeg/cmakescripts/GNUInstallDirs.cmake index 7c411965912..6408fca7771 100644 --- a/third-party/mozjpeg/mozjpeg/cmakescripts/GNUInstallDirs.cmake +++ b/third-party/mozjpeg/mozjpeg/cmakescripts/GNUInstallDirs.cmake @@ -118,6 +118,7 @@ # absolute paths where necessary, using the same logic. #============================================================================= +# Copyright 2018 Matthias Räncker # Copyright 2016, 2019 D. R. Commander # Copyright 2016 Dmitry Marakasov # Copyright 2016 Roger Leigh @@ -259,6 +260,8 @@ if(NOT DEFINED CMAKE_INSTALL_DEFAULT_LIBDIR) else() if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") set(CMAKE_INSTALL_DEFAULT_LIBDIR "lib64") + elseif(CMAKE_C_COMPILER_ABI MATCHES "ELF X32") + set(CMAKE_INSTALL_DEFAULT_LIBDIR "libx32") endif() endif() endif() diff --git a/third-party/mozjpeg/mozjpeg/cmakescripts/PackageInfo.cmake b/third-party/mozjpeg/mozjpeg/cmakescripts/PackageInfo.cmake new file mode 100644 index 00000000000..36f6133aab9 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/cmakescripts/PackageInfo.cmake @@ -0,0 +1,13 @@ +# This file is included from the top-level CMakeLists.txt. We just store it +# here to avoid cluttering up that file. + +set(PKGNAME ${CMAKE_PROJECT_NAME} CACHE STRING + "Distribution package name (default: ${CMAKE_PROJECT_NAME})") +set(PKGVENDOR "The ${CMAKE_PROJECT_NAME} Project" CACHE STRING + "Vendor name to be included in distribution package descriptions (default: The ${CMAKE_PROJECT_NAME} Project)") +set(PKGURL "http://www.${CMAKE_PROJECT_NAME}.org" CACHE STRING + "URL of project web site to be included in distribution package descriptions (default: http://www.${CMAKE_PROJECT_NAME}.org)") +set(PKGEMAIL "information@${CMAKE_PROJECT_NAME}.org" CACHE STRING + "E-mail of project maintainer to be included in distribution package descriptions (default: information@${CMAKE_PROJECT_NAME}.org") +set(PKGID "com.${CMAKE_PROJECT_NAME}.${PKGNAME}" CACHE STRING + "Globally unique package identifier (reverse DNS notation) (default: com.${CMAKE_PROJECT_NAME}.${PKGNAME})") diff --git a/third-party/mozjpeg/mozjpeg/cmyk.h b/third-party/mozjpeg/mozjpeg/cmyk.h index 48187a8f5dc..b6ca20f2c86 100644 --- a/third-party/mozjpeg/mozjpeg/cmyk.h +++ b/third-party/mozjpeg/mozjpeg/cmyk.h @@ -17,7 +17,6 @@ #include #define JPEG_INTERNALS #include -#include "jconfigint.h" /* Fully reversible */ diff --git a/third-party/mozjpeg/mozjpeg/croptest.in b/third-party/mozjpeg/mozjpeg/croptest.in new file mode 100755 index 00000000000..7e3c2931b12 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/croptest.in @@ -0,0 +1,95 @@ +#!/bin/bash + +set -u +set -e +trap onexit INT +trap onexit TERM +trap onexit EXIT + +onexit() +{ + if [ -d $OUTDIR ]; then + rm -rf $OUTDIR + fi +} + +runme() +{ + echo \*\*\* $* + $* +} + +IMAGE=vgl_6548_0026a.bmp +WIDTH=128 +HEIGHT=95 +IMGDIR=@CMAKE_CURRENT_SOURCE_DIR@/testimages +OUTDIR=`mktemp -d /tmp/__croptest_output.XXXXXX` +EXEDIR=@CMAKE_CURRENT_BINARY_DIR@ + +if [ -d $OUTDIR ]; then + rm -rf $OUTDIR +fi +mkdir -p $OUTDIR + +exec >$EXEDIR/croptest.log + +echo "============================================================" +echo "$IMAGE ($WIDTH x $HEIGHT)" +echo "============================================================" +echo + +for PROGARG in "" -progressive; do + + cp $IMGDIR/$IMAGE $OUTDIR + basename=`basename $IMAGE .bmp` + echo "------------------------------------------------------------" + echo "Generating test images" + echo "------------------------------------------------------------" + echo + runme $EXEDIR/cjpeg $PROGARG -grayscale -outfile $OUTDIR/${basename}_GRAY.jpg $IMGDIR/${basename}.bmp + runme $EXEDIR/cjpeg $PROGARG -sample 2x2 -outfile $OUTDIR/${basename}_420.jpg $IMGDIR/${basename}.bmp + runme $EXEDIR/cjpeg $PROGARG -sample 2x1 -outfile $OUTDIR/${basename}_422.jpg $IMGDIR/${basename}.bmp + runme $EXEDIR/cjpeg $PROGARG -sample 1x2 -outfile $OUTDIR/${basename}_440.jpg $IMGDIR/${basename}.bmp + runme $EXEDIR/cjpeg $PROGARG -sample 1x1 -outfile $OUTDIR/${basename}_444.jpg $IMGDIR/${basename}.bmp + echo + + for NSARG in "" -nosmooth; do + + for COLORSARG in "" "-colors 256 -dither none -onepass"; do + + for Y in {0..16}; do + + for H in {1..16}; do + + X=$(( (Y*16)%128 )) + W=$(( WIDTH-X-7 )) + if [ $Y -le 15 ]; then + CROPSPEC="${W}x${H}+${X}+${Y}" + else + Y2=$(( HEIGHT-H )); + CROPSPEC="${W}x${H}+${X}+${Y2}" + fi + + echo "------------------------------------------------------------" + echo $PROGARG $NSARG $COLORSARG -crop $CROPSPEC + echo "------------------------------------------------------------" + echo + for samp in GRAY 420 422 440 444; do + $EXEDIR/djpeg $NSARG $COLORSARG -rgb -outfile $OUTDIR/${basename}_${samp}_full.ppm $OUTDIR/${basename}_${samp}.jpg + convert -crop $CROPSPEC $OUTDIR/${basename}_${samp}_full.ppm $OUTDIR/${basename}_${samp}_ref.ppm + runme $EXEDIR/djpeg $NSARG $COLORSARG -crop $CROPSPEC -rgb -outfile $OUTDIR/${basename}_${samp}.ppm $OUTDIR/${basename}_${samp}.jpg + runme cmp $OUTDIR/${basename}_${samp}.ppm $OUTDIR/${basename}_${samp}_ref.ppm + done + echo + + done + + done + + done + + done + +done + +echo SUCCESS! diff --git a/third-party/mozjpeg/mozjpeg/djpeg.1 b/third-party/mozjpeg/mozjpeg/djpeg.1 index e4204b26ed2..31431b9829d 100644 --- a/third-party/mozjpeg/mozjpeg/djpeg.1 +++ b/third-party/mozjpeg/mozjpeg/djpeg.1 @@ -1,4 +1,4 @@ -.TH DJPEG 1 "13 November 2017" +.TH DJPEG 1 "4 November 2020" .SH NAME djpeg \- decompress a JPEG file to an image file .SH SYNOPSIS @@ -15,8 +15,7 @@ djpeg \- decompress a JPEG file to an image file .B djpeg decompresses the named JPEG file, or the standard input if no file is named, and produces an image file on the standard output. PBMPLUS (PPM/PGM), BMP, -GIF, Targa, or RLE (Utah Raster Toolkit) output format can be selected. -(RLE is supported only if the URT library is available.) +GIF, or Targa output format can be selected. .SH OPTIONS All switch names may be abbreviated; for example, .B \-grayscale @@ -81,9 +80,20 @@ is specified, or if the JPEG file is grayscale; otherwise, 24-bit full-color format is emitted. .TP .B \-gif -Select GIF output format. Since GIF does not support more than 256 colors, +Select GIF output format (LZW-compressed). Since GIF does not support more +than 256 colors, .B \-colors 256 -is assumed (unless you specify a smaller number of colors). +is assumed (unless you specify a smaller number of colors). If you specify +.BR \-fast, +the default number of colors is 216. +.TP +.B \-gif0 +Select GIF output format (uncompressed). Since GIF does not support more than +256 colors, +.B \-colors 256 +is assumed (unless you specify a smaller number of colors). If you specify +.BR \-fast, +the default number of colors is 216. .TP .B \-os2 Select BMP output format (OS/2 1.x flavor). 8-bit colormapped format is @@ -100,9 +110,6 @@ PGM is emitted if the JPEG file is grayscale or if .B \-grayscale is specified; otherwise PPM is emitted. .TP -.B \-rle -Select RLE output format. (Requires URT library.) -.TP .B \-targa Select Targa output format. Grayscale format is emitted if the JPEG file is grayscale or if @@ -114,32 +121,40 @@ is specified; otherwise, 24-bit full-color format is emitted. Switches for advanced users: .TP .B \-dct int -Use integer DCT method (default). +Use accurate integer DCT method (default). .TP .B \-dct fast -Use fast integer DCT (less accurate). -In libjpeg-turbo, the fast method is generally about 5-15% faster than the int -method when using the x86/x86-64 SIMD extensions (results may vary with other -SIMD implementations, or when using libjpeg-turbo without SIMD extensions.) If -the JPEG image was compressed using a quality level of 85 or below, then there -should be little or no perceptible difference between the two algorithms. When -decompressing images that were compressed using quality levels above 85, -however, the difference between the fast and int methods becomes more -pronounced. With images compressed using quality=97, for instance, the fast -method incurs generally about a 4-6 dB loss (in PSNR) relative to the int -method, but this can be larger for some images. If you can avoid it, do not -use the fast method when decompressing images that were compressed using -quality levels above 97. The algorithm often degenerates for such images and -can actually produce a more lossy output image than if the JPEG image had been -compressed using lower quality levels. +Use less accurate integer DCT method [legacy feature]. +When the Independent JPEG Group's software was first released in 1991, the +decompression time for a 1-megapixel JPEG image on a mainstream PC was measured +in minutes. Thus, the \fBfast\fR integer DCT algorithm provided noticeable +performance benefits. On modern CPUs running libjpeg-turbo, however, the +decompression time for a 1-megapixel JPEG image is measured in milliseconds, +and thus the performance benefits of the \fBfast\fR algorithm are much less +noticeable. On modern x86/x86-64 CPUs that support AVX2 instructions, the +\fBfast\fR and \fBint\fR methods have similar performance. On other types of +CPUs, the \fBfast\fR method is generally about 5-15% faster than the \fBint\fR +method. + +If the JPEG image was compressed using a quality level of 85 or below, then +there should be little or no perceptible quality difference between the two +algorithms. When decompressing images that were compressed using quality +levels above 85, however, the difference between the \fBfast\fR and \fBint\fR +methods becomes more pronounced. With images compressed using quality=97, for +instance, the \fBfast\fR method incurs generally about a 4-6 dB loss in PSNR +relative to the \fBint\fR method, but this can be larger for some images. If +you can avoid it, do not use the \fBfast\fR method when decompressing images +that were compressed using quality levels above 97. The algorithm often +degenerates for such images and can actually produce a more lossy output image +than if the JPEG image had been compressed using lower quality levels. .TP .B \-dct float -Use floating-point DCT method. -The float method is mainly a legacy feature. It does not produce significantly -more accurate results than the int method, and it is much slower. The float -method may also give different results on different machines due to varying -roundoff behavior, whereas the integer methods should give the same results on -all machines. +Use floating-point DCT method [legacy feature]. +The \fBfloat\fR method does not produce significantly more accurate results +than the \fBint\fR method, and it is much slower. The \fBfloat\fR method may +also give different results on different machines due to varying roundoff +behavior, whereas the integer methods should give the same results on all +machines. .TP .B \-dither fs Use Floyd-Steinberg dithering in color quantization. @@ -190,6 +205,19 @@ number. For example, .B \-max 4m selects 4000000 bytes. If more space is needed, an error will occur. .TP +.BI \-maxscans " N" +Abort if the JPEG image contains more than +.I N +scans. This feature demonstrates a method by which applications can guard +against denial-of-service attacks instigated by specially-crafted malformed +JPEG images containing numerous scans with missing image data or image data +consisting only of "EOB runs" (a feature of progressive JPEG images that allows +potentially hundreds of thousands of adjoining zero-value pixels to be +represented using only a few bytes.) Attempting to decompress such malformed +JPEG images can cause excessive CPU activity, since the decompressor must fully +process each scan (even if the scan is corrupt) before it can proceed to the +next scan. +.TP .BI \-outfile " name" Send output image to the named file, not to standard output. .TP @@ -197,6 +225,9 @@ Send output image to the named file, not to standard output. Load input file into memory before decompressing. This feature was implemented mainly as a way of testing the in-memory source manager (jpeg_mem_src().) .TP +.BI \-report +Report decompression progress. +.TP .BI \-skip " Y0,Y1" Decompress all rows of the JPEG image except those between Y0 and Y1 (inclusive.) Note that if decompression scaling is being used, then Y0 and Y1 @@ -210,6 +241,12 @@ decompression scaling is being used, then X, Y, W, and H are relative to the scaled image dimensions. Currently this option only works with the PBMPLUS (PPM/PGM), GIF, and Targa output formats. .TP +.BI \-strict +Treat all warnings as fatal. This feature also demonstrates a method by which +applications can guard against attacks instigated by specially-crafted +malformed JPEG images. Enabling this option will cause the decompressor to +abort if the JPEG image contains incomplete or corrupt image data. +.TP .B \-verbose Enable debug printout. More .BR \-v 's @@ -253,12 +290,6 @@ is fast but much lower quality than the default behavior. .B \-dither none may give acceptable results in two-pass mode, but is seldom tolerable in one-pass mode. -.PP -If you are fortunate enough to have very fast floating point hardware, -\fB\-dct float\fR may be even faster than \fB\-dct fast\fR. But on most -machines \fB\-dct float\fR is slower than \fB\-dct int\fR; in this case it is -not worth using, because its theoretical accuracy advantage is too small to be -significant in practice. .SH ENVIRONMENT .TP .B JPEGMEM @@ -287,10 +318,3 @@ Independent JPEG Group This file was modified by The libjpeg-turbo Project to include only information relevant to libjpeg-turbo, to wordsmith certain sections, and to describe features not present in libjpeg. -.SH ISSUES -Support for compressed GIF output files was removed in djpeg v6b due to -concerns over the Unisys LZW patent. Although this patent expired in 2006, -djpeg still lacks compressed GIF support, for these historical reasons. -(Conversion of JPEG files to GIF is usually a bad idea anyway, since GIF is a -256-color format.) The uncompressed GIF files that djpeg generates are larger -than they should be, but they are readable by standard GIF decoders. diff --git a/third-party/mozjpeg/mozjpeg/djpeg.c b/third-party/mozjpeg/mozjpeg/djpeg.c index e127cf79480..c22c4cade16 100644 --- a/third-party/mozjpeg/mozjpeg/djpeg.c +++ b/third-party/mozjpeg/mozjpeg/djpeg.c @@ -3,9 +3,9 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. - * Modified 2013 by Guido Vollbeding. + * Modified 2013-2019 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2010-2011, 2013-2017, D. R. Commander. + * Copyright (C) 2010-2011, 2013-2017, 2019-2020, 2022, D. R. Commander. * Copyright (C) 2015, Google, Inc. * For conditions of distribution and use, see the accompanying README.ijg * file. @@ -28,26 +28,16 @@ * works regardless of which command line style is used. */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ #include "jversion.h" /* for version message */ #include "jconfigint.h" -#ifndef HAVE_STDLIB_H /* should declare free() */ -extern void free(void *ptr); -#endif - #include /* to declare isprint() */ -#ifdef USE_CCOMMAND /* command-line reader for Macintosh */ -#ifdef __MWERKS__ -#include /* Metrowerks needs this */ -#include /* ... and this */ -#endif -#ifdef THINK_C -#include /* Think declares it here */ -#endif -#endif - /* Create the add-on message string table. */ @@ -68,10 +58,10 @@ static const char * const cdjpeg_message_table[] = { typedef enum { FMT_BMP, /* BMP format (Windows flavor) */ - FMT_GIF, /* GIF format */ + FMT_GIF, /* GIF format (LZW-compressed) */ + FMT_GIF0, /* GIF format (uncompressed) */ FMT_OS2, /* BMP format (OS/2 flavor) */ FMT_PPM, /* PPM/PGM (PBMPLUS formats) */ - FMT_RLE, /* RLE format */ FMT_TARGA, /* Targa format */ FMT_TIFF /* TIFF format */ } IMAGE_FORMATS; @@ -94,11 +84,14 @@ static IMAGE_FORMATS requested_fmt; static const char *progname; /* program name for error messages */ static char *icc_filename; /* for -icc switch */ +JDIMENSION max_scans; /* for -maxscans switch */ static char *outfilename; /* for -outfile switch */ boolean memsrc; /* for -memsrc switch */ +boolean report; /* for -report switch */ boolean skip, crop; JDIMENSION skip_start, skip_end; JDIMENSION crop_x, crop_y, crop_width, crop_height; +boolean strict; /* for -strict switch */ #define INPUT_BUF_SIZE 4096 @@ -127,8 +120,10 @@ usage(void) (DEFAULT_FMT == FMT_BMP ? " (default)" : "")); #endif #ifdef GIF_SUPPORTED - fprintf(stderr, " -gif Select GIF output format%s\n", + fprintf(stderr, " -gif Select GIF output format (LZW-compressed)%s\n", (DEFAULT_FMT == FMT_GIF ? " (default)" : "")); + fprintf(stderr, " -gif0 Select GIF output format (uncompressed)%s\n", + (DEFAULT_FMT == FMT_GIF0 ? " (default)" : "")); #endif #ifdef BMP_SUPPORTED fprintf(stderr, " -os2 Select BMP output format (OS/2 style)%s\n", @@ -138,25 +133,21 @@ usage(void) fprintf(stderr, " -pnm Select PBMPLUS (PPM/PGM) output format%s\n", (DEFAULT_FMT == FMT_PPM ? " (default)" : "")); #endif -#ifdef RLE_SUPPORTED - fprintf(stderr, " -rle Select Utah RLE output format%s\n", - (DEFAULT_FMT == FMT_RLE ? " (default)" : "")); -#endif #ifdef TARGA_SUPPORTED fprintf(stderr, " -targa Select Targa output format%s\n", (DEFAULT_FMT == FMT_TARGA ? " (default)" : "")); #endif fprintf(stderr, "Switches for advanced users:\n"); #ifdef DCT_ISLOW_SUPPORTED - fprintf(stderr, " -dct int Use integer DCT method%s\n", + fprintf(stderr, " -dct int Use accurate integer DCT method%s\n", (JDCT_DEFAULT == JDCT_ISLOW ? " (default)" : "")); #endif #ifdef DCT_IFAST_SUPPORTED - fprintf(stderr, " -dct fast Use fast integer DCT (less accurate)%s\n", + fprintf(stderr, " -dct fast Use less accurate integer DCT method [legacy feature]%s\n", (JDCT_DEFAULT == JDCT_IFAST ? " (default)" : "")); #endif #ifdef DCT_FLOAT_SUPPORTED - fprintf(stderr, " -dct float Use floating-point DCT method%s\n", + fprintf(stderr, " -dct float Use floating-point DCT method [legacy feature]%s\n", (JDCT_DEFAULT == JDCT_FLOAT ? " (default)" : "")); #endif fprintf(stderr, " -dither fs Use F-S dithering (default)\n"); @@ -171,14 +162,16 @@ usage(void) fprintf(stderr, " -onepass Use 1-pass quantization (fast, low quality)\n"); #endif fprintf(stderr, " -maxmemory N Maximum memory to use (in kbytes)\n"); + fprintf(stderr, " -maxscans N Maximum number of scans to allow in input file\n"); fprintf(stderr, " -outfile name Specify name for output file\n"); #if JPEG_LIB_VERSION >= 80 || defined(MEM_SRCDST_SUPPORTED) fprintf(stderr, " -memsrc Load input file into memory before decompressing\n"); #endif - + fprintf(stderr, " -report Report decompression progress\n"); fprintf(stderr, " -skip Y0,Y1 Decompress all rows except those between Y0 and Y1 (inclusive)\n"); fprintf(stderr, " -crop WxH+X+Y Decompress only a rectangular subregion of the image\n"); fprintf(stderr, " [requires PBMPLUS (PPM/PGM), GIF, or Targa output format]\n"); + fprintf(stderr, " -strict Treat all warnings as fatal\n"); fprintf(stderr, " -verbose or -debug Emit debug output\n"); fprintf(stderr, " -version Print version information and exit\n"); exit(EXIT_FAILURE); @@ -203,10 +196,13 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, /* Set up default JPEG parameters. */ requested_fmt = DEFAULT_FMT; /* set default output file format */ icc_filename = NULL; + max_scans = 0; outfilename = NULL; memsrc = FALSE; + report = FALSE; skip = FALSE; crop = FALSE; + strict = FALSE; cinfo->err->trace_level = 0; /* Scan command line options, adjust parameters */ @@ -224,7 +220,7 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, arg++; /* advance past switch marker character */ if (keymatch(arg, "bmp", 1)) { - /* BMP output format. */ + /* BMP output format (Windows flavor). */ requested_fmt = FMT_BMP; } else if (keymatch(arg, "colors", 1) || keymatch(arg, "colours", 1) || @@ -295,9 +291,13 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, cinfo->do_fancy_upsampling = FALSE; } else if (keymatch(arg, "gif", 1)) { - /* GIF output format. */ + /* GIF output format (LZW-compressed). */ requested_fmt = FMT_GIF; + } else if (keymatch(arg, "gif0", 4)) { + /* GIF output format (uncompressed). */ + requested_fmt = FMT_GIF0; + } else if (keymatch(arg, "grayscale", 2) || keymatch(arg, "greyscale", 2)) { /* Force monochrome output. */ @@ -316,7 +316,9 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, if (++argn >= argc) /* advance to next argument */ usage(); icc_filename = argv[argn]; +#ifdef SAVE_MARKERS_SUPPORTED jpeg_save_markers(cinfo, JPEG_APP0 + 2, 0xFFFF); +#endif } else if (keymatch(arg, "map", 3)) { /* Quantize to a color map taken from an input file. */ @@ -351,6 +353,12 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, lval *= 1000L; cinfo->mem->max_memory_to_use = lval * 1000L; + } else if (keymatch(arg, "maxscans", 4)) { + if (++argn >= argc) /* advance to next argument */ + usage(); + if (sscanf(argv[argn], "%u", &max_scans) != 1) + usage(); + } else if (keymatch(arg, "nosmooth", 3)) { /* Suppress fancy upsampling */ cinfo->do_fancy_upsampling = FALSE; @@ -383,9 +391,8 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, /* PPM/PGM output format. */ requested_fmt = FMT_PPM; - } else if (keymatch(arg, "rle", 1)) { - /* RLE output format. */ - requested_fmt = FMT_RLE; + } else if (keymatch(arg, "report", 2)) { + report = TRUE; } else if (keymatch(arg, "scale", 2)) { /* Scale the output image by a fraction M/N. */ @@ -413,6 +420,9 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, usage(); crop = TRUE; + } else if (keymatch(arg, "strict", 2)) { + strict = TRUE; + } else if (keymatch(arg, "targa", 1)) { /* Targa output format. */ requested_fmt = FMT_TARGA; @@ -444,7 +454,7 @@ jpeg_getc(j_decompress_ptr cinfo) ERREXIT(cinfo, JERR_CANT_SUSPEND); } datasrc->bytes_in_buffer--; - return GETJOCTET(*datasrc->next_input_byte++); + return *datasrc->next_input_byte++; } @@ -499,6 +509,19 @@ print_text_marker(j_decompress_ptr cinfo) } +METHODDEF(void) +my_emit_message(j_common_ptr cinfo, int msg_level) +{ + if (msg_level < 0) { + /* Treat warning as fatal */ + cinfo->err->error_exit(cinfo); + } else { + if (cinfo->err->trace_level >= msg_level) + cinfo->err->output_message(cinfo); + } +} + + /* * The main program. */ @@ -508,9 +531,7 @@ main(int argc, char **argv) { struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; -#ifdef PROGRESS_REPORT struct cdjpeg_progress_mgr progress; -#endif int file_index; djpeg_dest_ptr dest_mgr = NULL; FILE *input_file; @@ -521,11 +542,6 @@ main(int argc, char **argv) #endif JDIMENSION num_scanlines; - /* On Mac, fetch a command line. */ -#ifdef USE_CCOMMAND - argc = ccommand(&argv); -#endif - progname = argv[0]; if (progname == NULL || progname[0] == 0) progname = "djpeg"; /* in case C library doesn't provide it */ @@ -557,6 +573,9 @@ main(int argc, char **argv) file_index = parse_switches(&cinfo, argc, argv, 0, FALSE); + if (strict) + jerr.emit_message = my_emit_message; + #ifdef TWO_FILE_COMMANDLINE /* Must have either -outfile switch or explicit output file name */ if (outfilename == NULL) { @@ -603,9 +622,11 @@ main(int argc, char **argv) output_file = write_stdout(); } -#ifdef PROGRESS_REPORT - start_progress_monitor((j_common_ptr)&cinfo, &progress); -#endif + if (report || max_scans != 0) { + start_progress_monitor((j_common_ptr)&cinfo, &progress); + progress.report = report; + progress.max_scans = max_scans; + } /* Specify data source for decompression */ #if JPEG_LIB_VERSION >= 80 || defined(MEM_SRCDST_SUPPORTED) @@ -617,7 +638,7 @@ main(int argc, char **argv) fprintf(stderr, "%s: memory allocation failure\n", progname); exit(EXIT_FAILURE); } - nbytes = JFREAD(input_file, &inbuffer[insize], INPUT_BUF_SIZE); + nbytes = fread(&inbuffer[insize], 1, INPUT_BUF_SIZE, input_file); if (nbytes < INPUT_BUF_SIZE && ferror(input_file)) { if (file_index < argc) fprintf(stderr, "%s: can't read from %s\n", progname, @@ -653,7 +674,10 @@ main(int argc, char **argv) #endif #ifdef GIF_SUPPORTED case FMT_GIF: - dest_mgr = jinit_write_gif(&cinfo); + dest_mgr = jinit_write_gif(&cinfo, TRUE); + break; + case FMT_GIF0: + dest_mgr = jinit_write_gif(&cinfo, FALSE); break; #endif #ifdef PPM_SUPPORTED @@ -661,11 +685,6 @@ main(int argc, char **argv) dest_mgr = jinit_write_ppm(&cinfo); break; #endif -#ifdef RLE_SUPPORTED - case FMT_RLE: - dest_mgr = jinit_write_rle(&cinfo); - break; -#endif #ifdef TARGA_SUPPORTED case FMT_TARGA: dest_mgr = jinit_write_targa(&cinfo); @@ -689,7 +708,7 @@ main(int argc, char **argv) * that skip_start <= skip_end. */ if (skip_end > cinfo.output_height - 1) { - fprintf(stderr, "%s: skip region exceeds image height %d\n", progname, + fprintf(stderr, "%s: skip region exceeds image height %u\n", progname, cinfo.output_height); exit(EXIT_FAILURE); } @@ -708,7 +727,12 @@ main(int argc, char **argv) dest_mgr->buffer_height); (*dest_mgr->put_pixel_rows) (&cinfo, dest_mgr, num_scanlines); } - jpeg_skip_scanlines(&cinfo, skip_end - skip_start + 1); + if ((tmp = jpeg_skip_scanlines(&cinfo, skip_end - skip_start + 1)) != + skip_end - skip_start + 1) { + fprintf(stderr, "%s: jpeg_skip_scanlines() returned %u rather than %u\n", + progname, tmp, skip_end - skip_start + 1); + exit(EXIT_FAILURE); + } while (cinfo.output_scanline < cinfo.output_height) { num_scanlines = jpeg_read_scanlines(&cinfo, dest_mgr->buffer, dest_mgr->buffer_height); @@ -724,7 +748,7 @@ main(int argc, char **argv) */ if (crop_x + crop_width > cinfo.output_width || crop_y + crop_height > cinfo.output_height) { - fprintf(stderr, "%s: crop dimensions exceed image dimensions %d x %d\n", + fprintf(stderr, "%s: crop dimensions exceed image dimensions %u x %u\n", progname, cinfo.output_width, cinfo.output_height); exit(EXIT_FAILURE); } @@ -744,13 +768,24 @@ main(int argc, char **argv) cinfo.output_height = tmp; /* Process data */ - jpeg_skip_scanlines(&cinfo, crop_y); + if ((tmp = jpeg_skip_scanlines(&cinfo, crop_y)) != crop_y) { + fprintf(stderr, "%s: jpeg_skip_scanlines() returned %u rather than %u\n", + progname, tmp, crop_y); + exit(EXIT_FAILURE); + } while (cinfo.output_scanline < crop_y + crop_height) { num_scanlines = jpeg_read_scanlines(&cinfo, dest_mgr->buffer, dest_mgr->buffer_height); (*dest_mgr->put_pixel_rows) (&cinfo, dest_mgr, num_scanlines); } - jpeg_skip_scanlines(&cinfo, cinfo.output_height - crop_y - crop_height); + if ((tmp = + jpeg_skip_scanlines(&cinfo, + cinfo.output_height - crop_y - crop_height)) != + cinfo.output_height - crop_y - crop_height) { + fprintf(stderr, "%s: jpeg_skip_scanlines() returned %u rather than %u\n", + progname, tmp, cinfo.output_height - crop_y - crop_height); + exit(EXIT_FAILURE); + } /* Normal full-image decompress */ } else { @@ -765,12 +800,11 @@ main(int argc, char **argv) } } -#ifdef PROGRESS_REPORT /* Hack: count final pass as done in case finish_output does an extra pass. * The library won't have updated completed_passes. */ - progress.pub.completed_passes = progress.pub.total_passes; -#endif + if (report || max_scans != 0) + progress.pub.completed_passes = progress.pub.total_passes; if (icc_filename != NULL) { FILE *icc_file; @@ -809,9 +843,8 @@ main(int argc, char **argv) if (output_file != stdout) fclose(output_file); -#ifdef PROGRESS_REPORT - end_progress_monitor((j_common_ptr)&cinfo); -#endif + if (report || max_scans != 0) + end_progress_monitor((j_common_ptr)&cinfo); if (memsrc) free(inbuffer); diff --git a/third-party/mozjpeg/mozjpeg/doc/html/annotated.html b/third-party/mozjpeg/mozjpeg/doc/html/annotated.html index 50286d27999..c735a5a7ecb 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/annotated.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/annotated.html @@ -1,18 +1,17 @@ - + - + + TurboJPEG: Data Structures + - @@ -22,9 +21,9 @@ - @@ -32,47 +31,29 @@
+
TurboJPEG -  2.0 +  2.1.4
- + -
- + + + + +
@@ -88,17 +69,15 @@
Here are the data structures with brief descriptions:
- - - + + +
oCtjregionCropping region
oCtjscalingfactorScaling factor
\CtjtransformLossless transform
 CtjregionCropping region
 CtjscalingfactorScaling factor
 CtjtransformLossless transform
diff --git a/third-party/mozjpeg/mozjpeg/doc/html/classes.html b/third-party/mozjpeg/mozjpeg/doc/html/classes.html index 41a28119d93..42f38f6bcd4 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/classes.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/classes.html @@ -1,18 +1,17 @@ - + - + + TurboJPEG: Data Structure Index + - @@ -22,9 +21,9 @@ - @@ -32,47 +31,29 @@
+
TurboJPEG -  2.0 +  2.1.4
- + - - + + + + +
@@ -86,21 +67,23 @@
Data Structure Index
- - - + +
  T  
-
tjscalingfactor   tjtransform   
+ + + + - + +
  t  
+
tjscalingfactor   tjtransform   
tjregion   
tjregion   
- +
diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2doc.png b/third-party/mozjpeg/mozjpeg/doc/html/doc.png similarity index 100% rename from third-party/mozjpeg/mozjpeg/doc/html/ftv2doc.png rename to third-party/mozjpeg/mozjpeg/doc/html/doc.png diff --git a/third-party/mozjpeg/mozjpeg/doc/html/doxygen.css b/third-party/mozjpeg/mozjpeg/doc/html/doxygen.css index dabaff2fd8c..f640966e203 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/doxygen.css +++ b/third-party/mozjpeg/mozjpeg/doc/html/doxygen.css @@ -1,7 +1,11 @@ -/* The standard CSS for doxygen 1.8.3.1 */ +/* The standard CSS for doxygen 1.8.20 */ body, table, div, p, dl { - font: 400 14px/19px Roboto,sans-serif; + font: 400 14px/22px Roboto,sans-serif; +} + +p.reference, p.definition { + font: 400 14px/22px Roboto,sans-serif; } /* @group Heading Levels */ @@ -11,6 +15,7 @@ h1.groupheader { } .title { + font: 400 14px/28px Roboto,sans-serif; font-size: 150%; font-weight: bold; margin: 10px 2px; @@ -48,17 +53,28 @@ dt { font-weight: bold; } -div.multicol { +ul.multicol { -moz-column-gap: 1em; -webkit-column-gap: 1em; + column-gap: 1em; -moz-column-count: 3; -webkit-column-count: 3; + column-count: 3; } -p.startli, p.startdd, p.starttd { +p.startli, p.startdd { margin-top: 2px; } +th p.starttd, th p.intertd, th p.endtd { + font-size: 100%; + font-weight: 700; +} + +p.starttd { + margin-top: 0px; +} + p.endli { margin-bottom: 0px; } @@ -71,6 +87,15 @@ p.endtd { margin-bottom: 2px; } +p.interli { +} + +p.interdd { +} + +p.intertd { +} + /* @end */ caption { @@ -125,12 +150,12 @@ a.qindex { a.qindexHL { font-weight: bold; background-color: #9CAFD4; - color: #ffffff; + color: #FFFFFF; border: 1px double #869DCA; } .contents a.qindexHL:visited { - color: #ffffff; + color: #FFFFFF; } a.el { @@ -140,11 +165,11 @@ a.el { a.elRef { } -a.code, a.code:visited { +a.code, a.code:visited, a.line, a.line:visited { color: #4665A2; } -a.codeRef, a.codeRef:visited { +a.codeRef, a.codeRef:visited, a.lineRef, a.lineRef:visited { color: #4665A2; } @@ -154,6 +179,25 @@ dl.el { margin-left: -1cm; } +ul { + overflow: hidden; /*Fixed: list item bullets overlap floating elements*/ +} + +#side-nav ul { + overflow: visible; /* reset ul rule for scroll bar in GENERATE_TREEVIEW window */ +} + +#main-nav ul { + overflow: visible; /* reset ul rule for the navigation bar drop down lists */ +} + +.fragment { + text-align: left; + direction: ltr; + overflow-x: auto; /*Fixed: fragment lines overlap floating elements*/ + overflow-y: hidden; +} + pre.fragment { border: 1px solid #C4CFE5; background-color: #FBFCFD; @@ -168,8 +212,8 @@ pre.fragment { } div.fragment { - padding: 4px; - margin: 4px; + padding: 0 0 1px 0; /*Fixed: last line underline overlap border*/ + margin: 4px 8px 4px 2px; background-color: #FBFCFD; border: 1px solid #C4CFE5; } @@ -201,6 +245,11 @@ div.line { transition-duration: 0.5s; } +div.line:after { + content:"\000A"; + white-space: pre; +} + div.line.glow { background-color: cyan; box-shadow: 0 0 10px cyan; @@ -222,10 +271,19 @@ span.lineno a:hover { background-color: #C8C8C8; } -div.ah { +.lineno { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.ah, span.ah { background-color: black; font-weight: bold; - color: #ffffff; + color: #FFFFFF; margin-bottom: 3px; margin-top: 3px; padding: 0.2em; @@ -237,7 +295,16 @@ div.ah { -webkit-box-shadow: 2px 2px 3px #999; -moz-box-shadow: rgba(0, 0, 0, 0.15) 2px 2px 2px; background-image: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#000),color-stop(0.3, #444)); - background-image: -moz-linear-gradient(center top, #eee 0%, #444 40%, #000); + background-image: -moz-linear-gradient(center top, #eee 0%, #444 40%, #000 110%); +} + +div.classindex ul { + list-style: none; + padding-left: 0; +} + +div.classindex span.ai { + display: inline-block; } div.groupHeader { @@ -292,7 +359,7 @@ img.formulaDsp { } -img.formulaInl { +img.formulaInl, img.inline { vertical-align: middle; } @@ -370,6 +437,13 @@ blockquote { padding: 0 12px 0 16px; } +blockquote.DocNodeRTL { + border-left: 0; + border-right: 2px solid #9CAFD4; + margin: 0 4px 0 24px; + padding: 0 16px 0 12px; +} + /* @end */ /* @@ -466,7 +540,7 @@ table.memberdecls { white-space: nowrap; } -.memItemRight { +.memItemRight, .memTemplItemRight { width: 100%; } @@ -482,6 +556,29 @@ table.memberdecls { /* Styles for detailed member documentation */ +.memtitle { + padding: 8px; + border-top: 1px solid #A8B8D9; + border-left: 1px solid #A8B8D9; + border-right: 1px solid #A8B8D9; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + margin-bottom: -1px; + background-image: url('nav_f.png'); + background-repeat: repeat-x; + background-color: #E2E8F2; + line-height: 1.25; + font-weight: 300; + float:left; +} + +.permalink +{ + font-size: 65%; + display: inline-block; + vertical-align: middle; +} + .memtemplate { font-size: 80%; color: #4665A2; @@ -520,7 +617,7 @@ table.memberdecls { } .memname { - font-weight: bold; + font-weight: 400; margin-left: 6px; } @@ -536,24 +633,24 @@ table.memberdecls { color: #253555; font-weight: bold; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); - background-image:url('nav_f.png'); - background-repeat:repeat-x; - background-color: #E2E8F2; + background-color: #DFE5F1; /* opera specific markup */ box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); border-top-right-radius: 4px; - border-top-left-radius: 4px; /* firefox specific markup */ -moz-box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 5px; -moz-border-radius-topright: 4px; - -moz-border-radius-topleft: 4px; /* webkit specific markup */ -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); -webkit-border-top-right-radius: 4px; - -webkit-border-top-left-radius: 4px; } +.overload { + font-family: "courier new",courier,monospace; + font-size: 65%; +} + .memdoc, dl.reflist dd { border-bottom: 1px solid #A8B8D9; border-left: 1px solid #A8B8D9; @@ -611,17 +708,17 @@ dl.reflist dd { padding-left: 0px; } -.params .paramname, .retval .paramname { +.params .paramname, .retval .paramname, .tparams .paramname, .exception .paramname { font-weight: bold; vertical-align: top; } -.params .paramtype { +.params .paramtype, .tparams .paramtype { font-style: italic; vertical-align: top; } -.params .paramdir { +.params .paramdir, .tparams .paramdir { font-family: "courier new",courier,monospace; vertical-align: top; } @@ -665,12 +762,12 @@ span.mlabel { /* @end */ -/* these are for tree view when not used as main index */ +/* these are for tree view inside a (index) page */ div.directory { margin: 10px 0px; - border-top: 1px solid #A8B8D9; - border-bottom: 1px solid #A8B8D9; + border-top: 1px solid #9CAFD4; + border-bottom: 1px solid #9CAFD4; width: 100%; } @@ -687,6 +784,7 @@ div.directory { .directory td.entry { white-space: nowrap; padding-right: 6px; + padding-top: 3px; } .directory td.entry a { @@ -728,6 +826,80 @@ div.directory { color: #3D578C; } +.arrow { + color: #9CAFD4; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; + font-size: 80%; + display: inline-block; + width: 16px; + height: 22px; +} + +.icon { + font-family: Arial, Helvetica; + font-weight: bold; + font-size: 12px; + height: 14px; + width: 16px; + display: inline-block; + background-color: #728DC1; + color: white; + text-align: center; + border-radius: 4px; + margin-left: 2px; + margin-right: 2px; +} + +.icona { + width: 24px; + height: 22px; + display: inline-block; +} + +.iconfopen { + width: 24px; + height: 18px; + margin-bottom: 4px; + background-image:url('folderopen.png'); + background-position: 0px -4px; + background-repeat: repeat-y; + vertical-align:top; + display: inline-block; +} + +.iconfclosed { + width: 24px; + height: 18px; + margin-bottom: 4px; + background-image:url('folderclosed.png'); + background-position: 0px -4px; + background-repeat: repeat-y; + vertical-align:top; + display: inline-block; +} + +.icondoc { + width: 24px; + height: 18px; + margin-bottom: 4px; + background-image:url('doc.png'); + background-position: 0px -4px; + background-repeat: repeat-y; + vertical-align:top; + display: inline-block; +} + +table.directory { + font: 400 14px Roboto,sans-serif; +} + +/* @end */ + div.dynheader { margin-top: 8px; -webkit-touch-callout: none; @@ -743,6 +915,10 @@ address { color: #2A3D61; } +table.doxtable caption { + caption-side: top; +} + table.doxtable { border-collapse:collapse; margin-top: 4px; @@ -787,7 +963,7 @@ table.fieldtable { } .fieldtable td.fieldname { - padding-top: 5px; + padding-top: 3px; } .fieldtable td.fielddoc { @@ -796,7 +972,7 @@ table.fieldtable { } .fieldtable td.fielddoc p:first-child { - margin-top: 2px; + margin-top: 0px; } .fieldtable td.fielddoc p:last-child { @@ -816,6 +992,7 @@ table.fieldtable { padding-bottom: 4px; padding-top: 5px; text-align:left; + font-weight: 400; -moz-border-radius-topleft: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-left-radius: 4px; @@ -908,6 +1085,18 @@ div.summary a white-space: nowrap; } +table.classindex +{ + margin: 10px; + white-space: nowrap; + margin-left: 3%; + margin-right: 3%; + width: 94%; + border: 0; + border-spacing: 0; + padding: 0; +} + div.ingroups { font-size: 8pt; @@ -934,72 +1123,143 @@ div.headertitle padding: 5px 5px 5px 10px; } -dl -{ - padding: 0 0 0 10px; +.PageDocRTL-title div.headertitle { + text-align: right; + direction: rtl; } -/* dl.note, dl.warning, dl.attention, dl.pre, dl.post, dl.invariant, dl.deprecated, dl.todo, dl.test, dl.bug */ -dl.section -{ +dl { + padding: 0 0 0 0; +} + +/* dl.note, dl.warning, dl.attention, dl.pre, dl.post, dl.invariant, dl.deprecated, dl.todo, dl.test, dl.bug, dl.examples */ +dl.section { margin-left: 0px; padding-left: 0px; } -dl.note -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #D0C000; +dl.section.DocNodeRTL { + margin-right: 0px; + padding-right: 0px; } -dl.warning, dl.attention -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #FF0000; +dl.note { + margin-left: -7px; + padding-left: 3px; + border-left: 4px solid; + border-color: #D0C000; } -dl.pre, dl.post, dl.invariant -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #00D000; +dl.note.DocNodeRTL { + margin-left: 0; + padding-left: 0; + border-left: 0; + margin-right: -7px; + padding-right: 3px; + border-right: 4px solid; + border-color: #D0C000; +} + +dl.warning, dl.attention { + margin-left: -7px; + padding-left: 3px; + border-left: 4px solid; + border-color: #FF0000; } -dl.deprecated -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #505050; +dl.warning.DocNodeRTL, dl.attention.DocNodeRTL { + margin-left: 0; + padding-left: 0; + border-left: 0; + margin-right: -7px; + padding-right: 3px; + border-right: 4px solid; + border-color: #FF0000; } -dl.todo -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #00C0E0; +dl.pre, dl.post, dl.invariant { + margin-left: -7px; + padding-left: 3px; + border-left: 4px solid; + border-color: #00D000; } -dl.test -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #3030E0; +dl.pre.DocNodeRTL, dl.post.DocNodeRTL, dl.invariant.DocNodeRTL { + margin-left: 0; + padding-left: 0; + border-left: 0; + margin-right: -7px; + padding-right: 3px; + border-right: 4px solid; + border-color: #00D000; } -dl.bug -{ - margin-left:-7px; - padding-left: 3px; - border-left:4px solid; - border-color: #C08050; +dl.deprecated { + margin-left: -7px; + padding-left: 3px; + border-left: 4px solid; + border-color: #505050; +} + +dl.deprecated.DocNodeRTL { + margin-left: 0; + padding-left: 0; + border-left: 0; + margin-right: -7px; + padding-right: 3px; + border-right: 4px solid; + border-color: #505050; +} + +dl.todo { + margin-left: -7px; + padding-left: 3px; + border-left: 4px solid; + border-color: #00C0E0; +} + +dl.todo.DocNodeRTL { + margin-left: 0; + padding-left: 0; + border-left: 0; + margin-right: -7px; + padding-right: 3px; + border-right: 4px solid; + border-color: #00C0E0; +} + +dl.test { + margin-left: -7px; + padding-left: 3px; + border-left: 4px solid; + border-color: #3030E0; +} + +dl.test.DocNodeRTL { + margin-left: 0; + padding-left: 0; + border-left: 0; + margin-right: -7px; + padding-right: 3px; + border-right: 4px solid; + border-color: #3030E0; +} + +dl.bug { + margin-left: -7px; + padding-left: 3px; + border-left: 4px solid; + border-color: #C08050; +} + +dl.bug.DocNodeRTL { + margin-left: 0; + padding-left: 0; + border-left: 0; + margin-right: -7px; + padding-right: 3px; + border-right: 4px solid; + border-color: #C08050; } dl.section dd { @@ -1019,6 +1279,11 @@ dl.section dd { border: 0px none; } +#projectalign +{ + vertical-align: middle; +} + #projectname { font: 300% Tahoma, Arial,sans-serif; @@ -1063,6 +1328,16 @@ dl.section dd { text-align: center; } +.plantumlgraph +{ + text-align: center; +} + +.diagraph +{ + text-align: center; +} + .caption { font-weight: bold; @@ -1083,10 +1358,12 @@ dl.citelist dt { font-weight:bold; margin-right:10px; padding:5px; + text-align:right; + width:52px; } dl.citelist dd { - margin:2px 0; + margin:2px 0 2px 72px; padding:5px 0; } @@ -1097,10 +1374,15 @@ div.toc { border-radius: 7px 7px 7px 7px; float: right; height: auto; - margin: 0 20px 10px 10px; + margin: 0 8px 10px 10px; width: 200px; } +.PageDocRTL-title div.toc { + float: left !important; + text-align: right; +} + div.toc li { background: url("bdwn.png") no-repeat scroll 0 5px transparent; font: 10px/1.2 Verdana,DejaVu Sans,Geneva,sans-serif; @@ -1109,6 +1391,12 @@ div.toc li { padding-top: 2px; } +.PageDocRTL-title div.toc li { + background-position-x: right !important; + padding-left: 0 !important; + padding-right: 10px; +} + div.toc h3 { font: bold 12px/1.2 Arial,FreeSans,sans-serif; color: #4665A2; @@ -1138,6 +1426,26 @@ div.toc li.level4 { margin-left: 45px; } +.PageDocRTL-title div.toc li.level1 { + margin-left: 0 !important; + margin-right: 0; +} + +.PageDocRTL-title div.toc li.level2 { + margin-left: 0 !important; + margin-right: 15px; +} + +.PageDocRTL-title div.toc li.level3 { + margin-left: 0 !important; + margin-right: 30px; +} + +.PageDocRTL-title div.toc li.level4 { + margin-left: 0 !important; + margin-right: 45px; +} + .inherit_header { font-weight: bold; color: gray; @@ -1163,6 +1471,177 @@ tr.heading h2 { margin-bottom: 4px; } +/* tooltip related style info */ + +.ttc { + position: absolute; + display: none; +} + +#powerTip { + cursor: default; + white-space: nowrap; + background-color: white; + border: 1px solid gray; + border-radius: 4px 4px 4px 4px; + box-shadow: 1px 1px 7px gray; + display: none; + font-size: smaller; + max-width: 80%; + opacity: 0.9; + padding: 1ex 1em 1em; + position: absolute; + z-index: 2147483647; +} + +#powerTip div.ttdoc { + color: grey; + font-style: italic; +} + +#powerTip div.ttname a { + font-weight: bold; +} + +#powerTip div.ttname { + font-weight: bold; +} + +#powerTip div.ttdeci { + color: #006318; +} + +#powerTip div { + margin: 0px; + padding: 0px; + font: 12px/16px Roboto,sans-serif; +} + +#powerTip:before, #powerTip:after { + content: ""; + position: absolute; + margin: 0px; +} + +#powerTip.n:after, #powerTip.n:before, +#powerTip.s:after, #powerTip.s:before, +#powerTip.w:after, #powerTip.w:before, +#powerTip.e:after, #powerTip.e:before, +#powerTip.ne:after, #powerTip.ne:before, +#powerTip.se:after, #powerTip.se:before, +#powerTip.nw:after, #powerTip.nw:before, +#powerTip.sw:after, #powerTip.sw:before { + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; +} + +#powerTip.n:after, #powerTip.s:after, +#powerTip.w:after, #powerTip.e:after, +#powerTip.nw:after, #powerTip.ne:after, +#powerTip.sw:after, #powerTip.se:after { + border-color: rgba(255, 255, 255, 0); +} + +#powerTip.n:before, #powerTip.s:before, +#powerTip.w:before, #powerTip.e:before, +#powerTip.nw:before, #powerTip.ne:before, +#powerTip.sw:before, #powerTip.se:before { + border-color: rgba(128, 128, 128, 0); +} + +#powerTip.n:after, #powerTip.n:before, +#powerTip.ne:after, #powerTip.ne:before, +#powerTip.nw:after, #powerTip.nw:before { + top: 100%; +} + +#powerTip.n:after, #powerTip.ne:after, #powerTip.nw:after { + border-top-color: #FFFFFF; + border-width: 10px; + margin: 0px -10px; +} +#powerTip.n:before { + border-top-color: #808080; + border-width: 11px; + margin: 0px -11px; +} +#powerTip.n:after, #powerTip.n:before { + left: 50%; +} + +#powerTip.nw:after, #powerTip.nw:before { + right: 14px; +} + +#powerTip.ne:after, #powerTip.ne:before { + left: 14px; +} + +#powerTip.s:after, #powerTip.s:before, +#powerTip.se:after, #powerTip.se:before, +#powerTip.sw:after, #powerTip.sw:before { + bottom: 100%; +} + +#powerTip.s:after, #powerTip.se:after, #powerTip.sw:after { + border-bottom-color: #FFFFFF; + border-width: 10px; + margin: 0px -10px; +} + +#powerTip.s:before, #powerTip.se:before, #powerTip.sw:before { + border-bottom-color: #808080; + border-width: 11px; + margin: 0px -11px; +} + +#powerTip.s:after, #powerTip.s:before { + left: 50%; +} + +#powerTip.sw:after, #powerTip.sw:before { + right: 14px; +} + +#powerTip.se:after, #powerTip.se:before { + left: 14px; +} + +#powerTip.e:after, #powerTip.e:before { + left: 100%; +} +#powerTip.e:after { + border-left-color: #FFFFFF; + border-width: 10px; + top: 50%; + margin-top: -10px; +} +#powerTip.e:before { + border-left-color: #808080; + border-width: 11px; + top: 50%; + margin-top: -11px; +} + +#powerTip.w:after, #powerTip.w:before { + right: 100%; +} +#powerTip.w:after { + border-right-color: #FFFFFF; + border-width: 10px; + top: 50%; + margin-top: -10px; +} +#powerTip.w:before { + border-right-color: #808080; + border-width: 11px; + top: 50%; + margin-top: -11px; +} + @media print { #top { display: none; } @@ -1182,3 +1661,72 @@ tr.heading h2 { } } +/* @group Markdown */ + +table.markdownTable { + border-collapse:collapse; + margin-top: 4px; + margin-bottom: 4px; +} + +table.markdownTable td, table.markdownTable th { + border: 1px solid #2D4068; + padding: 3px 7px 2px; +} + +table.markdownTable tr { +} + +th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone { + background-color: #374F7F; + color: #FFFFFF; + font-size: 110%; + padding-bottom: 4px; + padding-top: 5px; +} + +th.markdownTableHeadLeft, td.markdownTableBodyLeft { + text-align: left +} + +th.markdownTableHeadRight, td.markdownTableBodyRight { + text-align: right +} + +th.markdownTableHeadCenter, td.markdownTableBodyCenter { + text-align: center +} + +.DocNodeRTL { + text-align: right; + direction: rtl; +} + +.DocNodeLTR { + text-align: left; + direction: ltr; +} + +table.DocNodeRTL { + width: auto; + margin-right: 0; + margin-left: auto; +} + +table.DocNodeLTR { + width: auto; + margin-right: auto; + margin-left: 0; +} + +tt, code, kbd, samp +{ + display: inline-block; + direction:ltr; +} +/* @end */ + +u { + text-decoration: underline; +} + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/doxygen.png b/third-party/mozjpeg/mozjpeg/doc/html/doxygen.png deleted file mode 100644 index 3ff17d807fd8aa003bed8bb2a69e8f0909592fd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3779 zcmV;!4m|ORP)tMIv#Q0*~7*`IBSO7_x;@a8#Zk6_PeKR_s92J&)(m+);m9Iz3blw)z#Gi zP!9lj4$%+*>Hz@HCmM9L9|8c+0u=!H$O3?R0Kgx|#WP<6fKfC8fM-CQZT|_r@`>VO zX^Hgb|9cJqpdJA5$MCEK`F_2@2Y@s>^+;pF`~jdI0Pvr|vl4`=C)EH@1IFe7pdJ8F zH(qGi004~QnF)Ggga~8v08kGAs2hKTATxr7pwfNk|4#_AaT>w8P6TV+R2kbS$v==} zAjf`s0g#V8lB+b3)5oEI*q+{Yt$MZDruD2^;$+(_%Qn+%v0X-bJO=;@kiJ^ygLBnC z?1OVv_%aex1M@jKU|Z~$eI?PoF4Vj>fDzyo zAiLfpXY*a^Sj-S5D0S3@#V$sRW)g)_1e#$%8xdM>Jm7?!h zu0P2X=xoN>^!4DoPRgph2(2va07yfpXF+WH7EOg1GY%Zn z7~1A<(z7Q$ktEXhW_?GMpHp9l_UL18F3KOsxu81pqoBiNbFSGsof-W z6~eloMoz=4?OOnl2J268x5rOY`dCk0us(uS#Ud4yqOr@?=Q57a}tit|BhY>}~frH1sP`ScHS_d)oqH^lYy zZ%VP`#10MlE~P?cE(%(#(AUSv_T{+;t@$U}El}(1ig`vZo`Rm;+5&(AYzJ^Ae=h2X z@Re%vHwZU>|f0NI&%$*4eJweC5OROQrpPMA@*w|o z()A==l}(@bv^&>H1Ob3C=<^|hob?0+xJ?QQ3-ueQC}zy&JQNib!OqSO@-=>XzxlSF zAZ^U*1l6EEmg3r};_HY>&Jo_{dOPEFTWPmt=U&F#+0(O59^UIlHbNX+eF8UzyDR*T z(=5X$VF3!gm@RooS-&iiUYGG^`hMR(07zr_xP`d!^BH?uD>Phl8Rdifx3Af^Zr`Ku ztL+~HkVeL#bJ)7;`=>;{KNRvjmc}1}c58Sr#Treq=4{xo!ATy|c>iRSp4`dzMMVd@ zL8?uwXDY}Wqgh4mH`|$BTXpUIu6A1-cSq%hJw;@^Zr8TP=GMh*p(m(tN7@!^D~sl$ zz^tf4II4|};+irE$Fnm4NTc5%p{PRA`%}Zk`CE5?#h3|xcyQsS#iONZ z6H(@^i9td!$z~bZiJLTax$o>r(p}3o@< zyD7%(>ZYvy=6$U3e!F{Z`uSaYy`xQyl?b{}eg|G3&fz*`QH@mDUn)1%#5u`0m$%D} z?;tZ0u(mWeMV0QtzjgN!lT*pNRj;6510Wwx?Yi_=tYw|J#7@(Xe7ifDzXuK;JB;QO z#bg~K$cgm$@{QiL_3yr}y&~wuv=P=#O&Tj=Sr)aCUlYmZMcw?)T?c%0rUe1cS+o!qs_ zQ6Gp)-{)V!;=q}llyK3|^WeLKyjf%y;xHku;9(vM!j|~<7w1c*Mk-;P{T&yG) z@C-8E?QPynNQ<8f01D`2qexcVEIOU?y}MG)TAE6&VT5`rK8s(4PE;uQ92LTXUQ<>^ ztyQ@=@kRdh@ebUG^Z6NWWIL;_IGJ2ST>$t!$m$qvtj0Qmw8moN6GUV^!QKNK zHBXCtUH8)RY9++gH_TUV4^=-j$t}dD3qsN7GclJ^Zc&(j6&a_!$jCf}%c5ey`pm~1)@{yI3 zTdWyB+*X{JFw#z;PwRr5evb2!ueWF;v`B0HoUu4-(~aL=z;OXUUEtG`_$)Oxw6FKg zEzY`CyKaSBK3xt#8gA|r_|Kehn_HYVBMpEwbn9-fI*!u*eTA1ef8Mkl1=!jV4oYwWYM}i`A>_F4nhmlCIC6WLa zY%;4&@AlnaG11ejl61Jev21|r*m+?Kru3;1tFDl}#!OzUp6c>go4{C|^erwpG*&h6bspUPJag}oOkN2912Y3I?(eRc@U9>z#HPBHC?nps7H5!zP``90!Q1n80jo+B3TWXp!8Pe zwuKuLLI6l3Gv@+QH*Y}2wPLPQ1^EZhT#+Ed8q8Wo z1pTmIBxv14-{l&QVKxAyQF#8Q@NeJwWdKk>?cpiJLkJr+aZ!Me+Cfp!?FWSRf^j2k z73BRR{WSKaMkJ>1Nbx5dan5hg^_}O{Tj6u%iV%#QGz0Q@j{R^Ik)Z*+(YvY2ziBG)?AmJa|JV%4UT$k`hcOg5r9R?5>?o~JzK zJCrj&{i#hG>N7!B4kNX(%igb%kDj0fOQThC-8mtfap82PNRXr1D>lbgg)dYTQ(kbx z`Ee5kXG~Bh+BHQBf|kJEy6(ga%WfhvdQNDuOfQoe377l#ht&DrMGeIsI5C<&ai zWG$|hop2@@q5YDa)_-A?B02W;#fH!%k`daQLEItaJJ8Yf1L%8x;kg?)k)00P-lH+w z)5$QNV6r2$YtnV(4o=0^3{kmaXn*Dm0F*fU(@o)yVVjk|ln8ea6BMy%vZAhW9|wvA z8RoDkVoMEz1d>|5(k0Nw>22ZT){V<3$^C-cN+|~hKt2)){+l-?3m@-$c?-dlzQ)q- zZ)j%n^gerV{|+t}9m1_&&Ly!9$rtG4XX|WQ8`xYzGC~U@nYh~g(z9)bdAl#xH)xd5a=@|qql z|FzEil{P5(@gy!4ek05i$>`E^G~{;pnf6ftpLh$h#W?^#4UkPfa;;?bsIe&kz!+40 zI|6`F2n020)-r`pFaZ38F!S-lJM-o&inOw|66=GMeP@xQU5ghQH{~5Uh~TMTd;I9` z>YhVB`e^EVj*S7JF39ZgNf}A-0DwOcTT63ydN$I3b?yBQtUI*_fae~kPvzoD$zjX3 zoqBe#>12im4WzZ=f^4+u=!lA|#r%1`WB0-6*3BL#at`47#ebPpR|D1b)3BjT34nYY z%Ds%d?5$|{LgOIaRO{{oC&RK`O91$fqwM0(C_TALcozu*fWHb%%q&p-q{_8*2Zsi^ zh1ZCnr^UYa;4vQEtHk{~zi>wwMC5o{S=$P0X681y`SXwFH?Ewn{x-MOZynmc)JT5v zuHLwh;tLfxRrr%|k370}GofLl7thg>ACWWY&msqaVu&ry+`7+Ss>NL^%T1|z{IGMA zW-SKl=V-^{(f!Kf^#3(|T2W47d(%JVCI4JgRrT1pNz>+ietmFToNv^`gzC@&O-)+i zPQ~RwK8%C_vf%;%e>NyTp~dM5;!C|N0Q^6|CEb7Bw=Vz~$1#FA;Z*?mKSC)Hl-20s t8QyHj(g6VK0RYbl8UjE)0O0w=e*@m04r>stuEhWV002ovPDHLkV1hl;dM*F} diff --git a/third-party/mozjpeg/mozjpeg/doc/html/doxygen.svg b/third-party/mozjpeg/mozjpeg/doc/html/doxygen.svg new file mode 100644 index 00000000000..d42dad52d5d --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/doxygen.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/dynsections.js b/third-party/mozjpeg/mozjpeg/doc/html/dynsections.js index ed092c7f630..3174bd7bebb 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/dynsections.js +++ b/third-party/mozjpeg/mozjpeg/doc/html/dynsections.js @@ -1,3 +1,27 @@ +/* + @licstart The following is the entire license notice for the JavaScript code in this file. + + The MIT License (MIT) + + Copyright (C) 1997-2020 by Dimitri van Heesch + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, + sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + @licend The above is the entire license notice for the JavaScript code in this file + */ function toggleVisibility(linkObj) { var base = $(linkObj).attr('id'); @@ -15,7 +39,7 @@ function toggleVisibility(linkObj) summary.hide(); $(linkObj).removeClass('closed').addClass('opened'); $(trigger).attr('src',src.substring(0,src.length-10)+'open.png'); - } + } return false; } @@ -24,19 +48,20 @@ function updateStripes() $('table.directory tr'). removeClass('even').filter(':visible:even').addClass('even'); } + function toggleLevel(level) { - $('table.directory tr').each(function(){ + $('table.directory tr').each(function() { var l = this.id.split('_').length-1; var i = $('#img'+this.id.substring(3)); var a = $('#arr'+this.id.substring(3)); if (ldjv*C{Z|`mdau^P8_z}#X h?B8GEpdi4(BFDx$je&7RrDQEg&ePS;Wt~$(69Dh@6T1Ka diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2cl.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2cl.png deleted file mode 100644 index 132f6577bf7f085344904602815a260d29f55d9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmV;$0XqJPP)VBF;ev;toEj8_OB0EQg5eYilIj#JZG_m^33l3^k4mtzx!TVD?g)Y$ zrvwRDSqT!wLIM$dWCIa$vtxE|mzbTzu-y&$FvF6WA2a{Wr1g}`WdPT-0JzEZ0IxAv z-Z+ejZc&H;I5-pb_SUB}04j0^V)3t{`z<7asDl2Tw3w3sP%)0^8$bhEg)IOTBcRXv zFfq~3&gvJ$F-U7mpBW8z1GY~HK&7h4^YI~Orv~wLnC0PP_dAkv;nzX{9Q|8Gv=2ca z@v)c9T;D#h`TZ2X&&$ff2wedmot995de~-s3I)yauahg;7qn*?1n?F$e+PwP37}~; z1NKUk7reVK^7A;$QRW7qAx40HHUZ<|k3U%nz(Ec`#i+q9K!dgcROAlCS?`L= v>#=f?wF5ZND!1uAfQsk;KN^4&*8~0npJiJ%2dj9(00000NkvXXu0mjfWVFf_ diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2lastnode.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2lastnode.png deleted file mode 100644 index 63c605bb4c3d941c921a4b6cfa74951e946bcb48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRr!3HExu9B$%QnH>djv*C{Z|`mdau^P8_z}#X h?B8GEpdi4(BFDx$je&7RrDQEg&ePS;Wt~$(69Dh@6T1Ka diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2link.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2link.png deleted file mode 100644 index 17edabff95f7b8da13c9516a04efe05493c29501..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 746 zcmV7=@pnbNXRFEm&G8P!&WHG=d)>K?YZ1bzou)2{$)) zumDct!>4SyxL;zgaG>wy`^Hv*+}0kUfCrz~BCOViSb$_*&;{TGGn2^x9K*!Sf0=lV zpP=7O;GA0*Jm*tTYj$IoXvimpnV4S1Z5f$p*f$Db2iq2zrVGQUz~yq`ahn7ck(|CE z7Gz;%OP~J6)tEZWDzjhL9h2hdfoU2)Nd%T<5Kt;Y0XLt&<@6pQx!nw*5`@bq#?l*?3z{Hlzoc=Pr>oB5(9i6~_&-}A(4{Q$>c>%rV&E|a(r&;?i5cQB=} zYSDU5nXG)NS4HEs0it2AHe2>shCyr7`6@4*6{r@8fXRbTA?=IFVWAQJL&H5H{)DpM#{W(GL+Idzf^)uRV@oB8u$ z8v{MfJbTiiRg4bza<41NAzrl{=3fl_D+$t+^!xlQ8S}{UtY`e z;;&9UhyZqQRN%2pot{*Ei0*4~hSF_3AH2@fKU!$NSflS>{@tZpDT4`M2WRTTVH+D? z)GFlEGGHe?koB}i|1w45!BF}N_q&^HJ&-tyR{(afC6H7|aml|tBBbv}55C5DNP8p3 z)~jLEO4Z&2hZmP^i-e%(@d!(E|KRafiU8Q5u(wU((j8un3OR*Hvj+t diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2mlastnode.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2mlastnode.png deleted file mode 100644 index 0b63f6d38c4b9ec907b820192ebe9724ed6eca22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246 zcmVkw!R34#Lv2LOS^S2tZA31X++9RY}n zChwn@Z)Wz*WWHH{)HDtJnq&A2hk$b-y(>?@z0iHr41EKCGp#T5?07*qoM6N<$f(V3Pvj6}9 diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2mnode.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2mnode.png deleted file mode 100644 index 0b63f6d38c4b9ec907b820192ebe9724ed6eca22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246 zcmVkw!R34#Lv2LOS^S2tZA31X++9RY}n zChwn@Z)Wz*WWHH{)HDtJnq&A2hk$b-y(>?@z0iHr41EKCGp#T5?07*qoM6N<$f(V3Pvj6}9 diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2mo.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2mo.png deleted file mode 100644 index 4bfb80f76e65815989a9350ad79d8ce45380e2b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 403 zcmV;E0c`$>P)${!fXv7NWJ%@%u4(KapRY>T6_x;E zxE7kt!}Tiw8@d9Sd`rTGum>z#Q14vIm`wm1#-byD1muMi02@YNO5LRF0o!Y{`a!Ya z{^&p0Su|s705&2QxmqdexG+-zNKL3f@8gTQSJrKByfo+oNJ^-{|Mn||Q5SDwjQVsS zr1}7o5-QMs>gYIMD>GRw@$lT`z4r-_m{5U#cR{urD_)TOeY)(UD|qZ^&y`IVijqk~ xs(9-kWFr7E^!lgi8GsFK5kOY_{Xbgf0^etEU%fLevs?fG002ovPDHLkV1nB&vX1}& diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2node.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2node.png deleted file mode 100644 index 63c605bb4c3d941c921a4b6cfa74951e946bcb48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRr!3HExu9B$%QnH>djv*C{Z|`mdau^P8_z}#X h?B8GEpdi4(BFDx$je&7RrDQEg&ePS;Wt~$(69Dh@6T1Ka diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2ns.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2ns.png deleted file mode 100644 index 72e3d71c2892d6f00e259facebc88b45f6db2e35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 388 zcmV-~0ek+5P)f+++#cT|!CkD&4pnIkeMEUEM*>`*9>+Juji$!h-mW%M^8s9957{3nvbrz^&=u<~TAUrFROkmt%^F~Ez+-c53Lv%iH3d38!Rv?K zrb&MYAhp;Gf<}wS;9ZZq2@;!uYG;=Z>~GKE^{HD4keu}lnyqhc>kWX^tQn|warJ~h zT+rtMkdz6aHoN%z(o|&wpu@@OpJnF_z{PA)6(FHw02iHslz^(N{4*+K9)QJHR87wT iTyp>aXaF{u2lxRou|^4tux6eB0000^P)R?RzRoKvklcaQ%HF6%rK2&ZgO(-ihJ_C zzrKgp4jgO( fd_(yg|3PpEQb#9`a?Pz_00000NkvXXu0mjftR`5K diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2pnode.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2pnode.png deleted file mode 100644 index c6ee22f937a07d1dbfc27c669d11f8ed13e2f152..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 229 zcmV^P)R?RzRoKvklcaQ%HF6%rK2&ZgO(-ihJ_C zzrKgp4jgO( fd_(yg|3PpEQb#9`a?Pz_00000NkvXXu0mjftR`5K diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2vertline.png b/third-party/mozjpeg/mozjpeg/doc/html/ftv2vertline.png deleted file mode 100644 index 63c605bb4c3d941c921a4b6cfa74951e946bcb48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRr!3HExu9B$%QnH>djv*C{Z|`mdau^P8_z}#X h?B8GEpdi4(BFDx$je&7RrDQEg&ePS;Wt~$(69Dh@6T1Ka diff --git a/third-party/mozjpeg/mozjpeg/doc/html/functions.html b/third-party/mozjpeg/mozjpeg/doc/html/functions.html index 1042ae754f8..e54144211f3 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/functions.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/functions.html @@ -1,18 +1,17 @@ - + - + + TurboJPEG: Data Fields + - @@ -22,9 +21,9 @@ - @@ -32,53 +31,29 @@
+
TurboJPEG -  2.0 +  2.1.4
- + - - - + + + + +
@@ -90,7 +65,7 @@
Here is a list of all documented struct and union fields with links to the struct/union documentation for each field:
diff --git a/third-party/mozjpeg/mozjpeg/doc/html/functions_vars.html b/third-party/mozjpeg/mozjpeg/doc/html/functions_vars.html index e0a71572aae..a77429bb74c 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/functions_vars.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/functions_vars.html @@ -1,18 +1,17 @@ - + - + + TurboJPEG: Data Fields - Variables + - @@ -22,9 +21,9 @@ - @@ -32,53 +31,29 @@
+
TurboJPEG -  2.0 +  2.1.4
- + - - - + + + + +
@@ -90,7 +65,7 @@
 
diff --git a/third-party/mozjpeg/mozjpeg/doc/html/group___turbo_j_p_e_g.html b/third-party/mozjpeg/mozjpeg/doc/html/group___turbo_j_p_e_g.html index 5d67d78a2ac..dd0f9aec175 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/group___turbo_j_p_e_g.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/group___turbo_j_p_e_g.html @@ -1,18 +1,17 @@ - + - + + TurboJPEG: TurboJPEG + - @@ -22,9 +21,9 @@ - @@ -32,40 +31,29 @@
+
TurboJPEG -  2.0 +  2.1.4
- + - + + + + +
@@ -93,277 +81,283 @@

Data Structures

struct  tjscalingfactor - Scaling factor. More...
+ Scaling factor. More...
  struct  tjregion - Cropping region. More...
+ Cropping region. More...
  struct  tjtransform - Lossless transform. More...
+ Lossless transform. More...
  - + - + - + - + - + - + - + - + - + - + + + + - + - + - + - + - + - + - + - + - + - + - - + +

Macros

#define TJ_NUMSAMP
 The number of chrominance subsampling options. More...
 The number of chrominance subsampling options. More...
 
#define TJ_NUMPF
 The number of pixel formats. More...
 The number of pixel formats. More...
 
#define TJ_NUMCS
 The number of JPEG colorspaces. More...
 The number of JPEG colorspaces. More...
 
#define TJFLAG_BOTTOMUP
 The uncompressed source/destination image is stored in bottom-up (Windows, OpenGL) order, not top-down (X11) order. More...
 Rows in the packed-pixel source/destination image are stored in bottom-up (Windows, OpenGL) order rather than in top-down (X11) order. More...
 
#define TJFLAG_FASTUPSAMPLE
 When decompressing an image that was compressed using chrominance subsampling, use the fastest chrominance upsampling algorithm available in the underlying codec. More...
 When decompressing an image that was compressed using chrominance subsampling, use the fastest chrominance upsampling algorithm available. More...
 
#define TJFLAG_NOREALLOC
 Disable buffer (re)allocation. More...
 Disable JPEG buffer (re)allocation. More...
 
#define TJFLAG_FASTDCT
 Use the fastest DCT/IDCT algorithm available in the underlying codec. More...
 Use the fastest DCT/IDCT algorithm available. More...
 
#define TJFLAG_ACCURATEDCT
 Use the most accurate DCT/IDCT algorithm available in the underlying codec. More...
 Use the most accurate DCT/IDCT algorithm available. More...
 
#define TJFLAG_STOPONWARNING
 Immediately discontinue the current compression/decompression/transform operation if the underlying codec throws a warning (non-fatal error). More...
 Immediately discontinue the current compression/decompression/transform operation if a warning (non-fatal error) occurs. More...
 
#define TJFLAG_PROGRESSIVE
 Use progressive entropy coding in JPEG images generated by the compression and transform functions. More...
 Use progressive entropy coding in JPEG images generated by the compression and transform functions. More...
 
#define TJFLAG_LIMITSCANS
 Limit the number of progressive JPEG scans that the decompression and transform functions will process. More...
 
#define TJ_NUMERR
 The number of error codes. More...
 The number of error codes. More...
 
#define TJ_NUMXOP
 The number of transform operations. More...
 The number of transform operations. More...
 
#define TJXOPT_PERFECT
 This option will cause tjTransform() to return an error if the transform is not perfect. More...
 This option will cause tjTransform() to return an error if the transform is not perfect. More...
 
#define TJXOPT_TRIM
 This option will cause tjTransform() to discard any partial MCU blocks that cannot be transformed. More...
 This option will cause tjTransform() to discard any partial MCU blocks that cannot be transformed. More...
 
#define TJXOPT_CROP
 This option will enable lossless cropping. More...
 This option will enable lossless cropping. More...
 
#define TJXOPT_GRAY
 This option will discard the color data in the input image and produce a grayscale output image. More...
 This option will discard the color data in the source image and produce a grayscale destination image. More...
 
#define TJXOPT_NOOUTPUT
 This option will prevent tjTransform() from outputting a JPEG image for this particular transform (this can be used in conjunction with a custom filter to capture the transformed DCT coefficients without transcoding them.) More...
 This option will prevent tjTransform() from outputting a JPEG image for this particular transform. More...
 
#define TJXOPT_PROGRESSIVE
 This option will enable progressive entropy coding in the output image generated by this particular transform. More...
 This option will enable progressive entropy coding in the JPEG image generated by this particular transform. More...
 
#define TJXOPT_COPYNONE
 This option will prevent tjTransform() from copying any extra markers (including EXIF and ICC profile data) from the source image to the output image. More...
 This option will prevent tjTransform() from copying any extra markers (including EXIF and ICC profile data) from the source image to the destination image. More...
 
#define TJPAD(width)
 Pad the given width to the nearest 32-bit boundary. More...
 Pad the given width to the nearest multiple of 4. More...
 
#define TJSCALED(dimension, scalingFactor)
 Compute the scaled value of dimension using the given scaling factor. More...
#define TJSCALED(dimension, scalingFactor)
 Compute the scaled value of dimension using the given scaling factor. More...
 
- - - + + + - +

Typedefs

typedef struct tjtransform tjtransform
 Lossless transform. More...
 
typedef struct tjtransform tjtransform
 Lossless transform. More...
 
typedef void * tjhandle
 TurboJPEG instance handle. More...
 TurboJPEG instance handle. More...
 
- - + - - + - - + - + - - +

Enumerations

enum  TJSAMP {
+
enum  TJSAMP {
  TJSAMP_444, TJSAMP_422, TJSAMP_420, TJSAMP_GRAY, -
+
  TJSAMP_440, TJSAMP_411 -
+
}
 Chrominance subsampling options. More...
 Chrominance subsampling options. More...
 
enum  TJPF {
+
enum  TJPF {
  TJPF_RGB, TJPF_BGR, TJPF_RGBX, TJPF_BGRX, -
+
  TJPF_XBGR, TJPF_XRGB, TJPF_GRAY, TJPF_RGBA, -
+
  TJPF_BGRA, TJPF_ABGR, TJPF_ARGB, TJPF_CMYK, -
+
  TJPF_UNKNOWN -
+
}
 Pixel formats. More...
 Pixel formats. More...
 
enum  TJCS {
+
enum  TJCS {
  TJCS_RGB, TJCS_YCbCr, TJCS_GRAY, TJCS_CMYK, -
+
  TJCS_YCCK -
+
}
 JPEG colorspaces. More...
 JPEG colorspaces. More...
 
enum  TJERR { TJERR_WARNING, TJERR_FATAL }
 Error codes. More...
 Error codes. More...
 
enum  TJXOP {
+
enum  TJXOP {
  TJXOP_NONE, TJXOP_HFLIP, TJXOP_VFLIP, TJXOP_TRANSPOSE, -
+
  TJXOP_TRANSVERSE, TJXOP_ROT90, TJXOP_ROT180, TJXOP_ROT270 -
+
}
 Transform operations for tjTransform() More...
 Transform operations for tjTransform() More...
 
- + - + - - - + + + - + - + - - - + + + - + - + - + - - - + + + - + - + - + - - - + + + - + - - - + + + - + - - - + + + - + - + - + - + - + - + - + - + - + - +

Functions

DLLEXPORT tjhandle tjInitCompress (void)
 Create a TurboJPEG compressor instance. More...
 Create a TurboJPEG compressor instance. More...
 
DLLEXPORT int tjCompress2 (tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char **jpegBuf, unsigned long *jpegSize, int jpegSubsamp, int jpegQual, int flags)
 Compress an RGB, grayscale, or CMYK image into a JPEG image. More...
 Compress a packed-pixel RGB, grayscale, or CMYK image into a JPEG image. More...
 
DLLEXPORT int tjCompressFromYUV (tjhandle handle, const unsigned char *srcBuf, int width, int pad, int height, int subsamp, unsigned char **jpegBuf, unsigned long *jpegSize, int jpegQual, int flags)
 Compress a YUV planar image into a JPEG image. More...
 
DLLEXPORT int tjCompressFromYUV (tjhandle handle, const unsigned char *srcBuf, int width, int align, int height, int subsamp, unsigned char **jpegBuf, unsigned long *jpegSize, int jpegQual, int flags)
 Compress a unified planar YUV image into a JPEG image. More...
 
DLLEXPORT int tjCompressFromYUVPlanes (tjhandle handle, const unsigned char **srcPlanes, int width, const int *strides, int height, int subsamp, unsigned char **jpegBuf, unsigned long *jpegSize, int jpegQual, int flags)
 Compress a set of Y, U (Cb), and V (Cr) image planes into a JPEG image. More...
 Compress a set of Y, U (Cb), and V (Cr) image planes into a JPEG image. More...
 
DLLEXPORT unsigned long tjBufSize (int width, int height, int jpegSubsamp)
 The maximum size of the buffer (in bytes) required to hold a JPEG image with the given parameters. More...
 The maximum size of the buffer (in bytes) required to hold a JPEG image with the given parameters. More...
 
DLLEXPORT unsigned long tjBufSizeYUV2 (int width, int pad, int height, int subsamp)
 The size of the buffer (in bytes) required to hold a YUV planar image with the given parameters. More...
 
DLLEXPORT unsigned long tjBufSizeYUV2 (int width, int align, int height, int subsamp)
 The size of the buffer (in bytes) required to hold a unified planar YUV image with the given parameters. More...
 
DLLEXPORT unsigned long tjPlaneSizeYUV (int componentID, int width, int stride, int height, int subsamp)
 The size of the buffer (in bytes) required to hold a YUV image plane with the given parameters. More...
 The size of the buffer (in bytes) required to hold a YUV image plane with the given parameters. More...
 
DLLEXPORT int tjPlaneWidth (int componentID, int width, int subsamp)
 The plane width of a YUV image plane with the given parameters. More...
 The plane width of a YUV image plane with the given parameters. More...
 
DLLEXPORT int tjPlaneHeight (int componentID, int height, int subsamp)
 The plane height of a YUV image plane with the given parameters. More...
 The plane height of a YUV image plane with the given parameters. More...
 
DLLEXPORT int tjEncodeYUV3 (tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char *dstBuf, int pad, int subsamp, int flags)
 Encode an RGB or grayscale image into a YUV planar image. More...
 
DLLEXPORT int tjEncodeYUV3 (tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char *dstBuf, int align, int subsamp, int flags)
 Encode a packed-pixel RGB or grayscale image into a unified planar YUV image. More...
 
DLLEXPORT int tjEncodeYUVPlanes (tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char **dstPlanes, int *strides, int subsamp, int flags)
 Encode an RGB or grayscale image into separate Y, U (Cb), and V (Cr) image planes. More...
 Encode a packed-pixel RGB or grayscale image into separate Y, U (Cb), and V (Cr) image planes. More...
 
DLLEXPORT tjhandle tjInitDecompress (void)
 Create a TurboJPEG decompressor instance. More...
 Create a TurboJPEG decompressor instance. More...
 
DLLEXPORT int tjDecompressHeader3 (tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, int *width, int *height, int *jpegSubsamp, int *jpegColorspace)
 Retrieve information about a JPEG image without decompressing it. More...
 Retrieve information about a JPEG image without decompressing it, or prime the decompressor with quantization and Huffman tables. More...
 
DLLEXPORT tjscalingfactortjGetScalingFactors (int *numscalingfactors)
 Returns a list of fractional scaling factors that the JPEG decompressor in this implementation of TurboJPEG supports. More...
 
DLLEXPORT tjscalingfactortjGetScalingFactors (int *numScalingFactors)
 Returns a list of fractional scaling factors that the JPEG decompressor supports. More...
 
DLLEXPORT int tjDecompress2 (tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, int width, int pitch, int height, int pixelFormat, int flags)
 Decompress a JPEG image to an RGB, grayscale, or CMYK image. More...
 Decompress a JPEG image into a packed-pixel RGB, grayscale, or CMYK image. More...
 
DLLEXPORT int tjDecompressToYUV2 (tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, int width, int pad, int height, int flags)
 Decompress a JPEG image to a YUV planar image. More...
 
DLLEXPORT int tjDecompressToYUV2 (tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, int width, int align, int height, int flags)
 Decompress a JPEG image into a unified planar YUV image. More...
 
DLLEXPORT int tjDecompressToYUVPlanes (tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, unsigned char **dstPlanes, int width, int *strides, int height, int flags)
 Decompress a JPEG image into separate Y, U (Cb), and V (Cr) image planes. More...
 Decompress a JPEG image into separate Y, U (Cb), and V (Cr) image planes. More...
 
DLLEXPORT int tjDecodeYUV (tjhandle handle, const unsigned char *srcBuf, int pad, int subsamp, unsigned char *dstBuf, int width, int pitch, int height, int pixelFormat, int flags)
 Decode a YUV planar image into an RGB or grayscale image. More...
 
DLLEXPORT int tjDecodeYUV (tjhandle handle, const unsigned char *srcBuf, int align, int subsamp, unsigned char *dstBuf, int width, int pitch, int height, int pixelFormat, int flags)
 Decode a unified planar YUV image into a packed-pixel RGB or grayscale image. More...
 
DLLEXPORT int tjDecodeYUVPlanes (tjhandle handle, const unsigned char **srcPlanes, const int *strides, int subsamp, unsigned char *dstBuf, int width, int pitch, int height, int pixelFormat, int flags)
 Decode a set of Y, U (Cb), and V (Cr) image planes into an RGB or grayscale image. More...
 Decode a set of Y, U (Cb), and V (Cr) image planes into a packed-pixel RGB or grayscale image. More...
 
DLLEXPORT tjhandle tjInitTransform (void)
 Create a new TurboJPEG transformer instance. More...
 Create a new TurboJPEG transformer instance. More...
 
DLLEXPORT int tjTransform (tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, int n, unsigned char **dstBufs, unsigned long *dstSizes, tjtransform *transforms, int flags)
 Losslessly transform a JPEG image into another JPEG image. More...
 Losslessly transform a JPEG image into another JPEG image. More...
 
DLLEXPORT int tjDestroy (tjhandle handle)
 Destroy a TurboJPEG compressor, decompressor, or transformer instance. More...
 Destroy a TurboJPEG compressor, decompressor, or transformer instance. More...
 
DLLEXPORT unsigned char * tjAlloc (int bytes)
 Allocate an image buffer for use with TurboJPEG. More...
 Allocate a byte buffer for use with TurboJPEG. More...
 
DLLEXPORT unsigned char * tjLoadImage (const char *filename, int *width, int align, int *height, int *pixelFormat, int flags)
 Load an uncompressed image from disk into memory. More...
 Load a packed-pixel image from disk into memory. More...
 
DLLEXPORT int tjSaveImage (const char *filename, unsigned char *buffer, int width, int pitch, int height, int pixelFormat, int flags)
 Save an uncompressed image from memory to disk. More...
 Save a packed-pixel image from memory to disk. More...
 
DLLEXPORT void tjFree (unsigned char *buffer)
 Free an image buffer previously allocated by TurboJPEG. More...
 Free a byte buffer previously allocated by TurboJPEG. More...
 
DLLEXPORT char * tjGetErrorStr2 (tjhandle handle)
 Returns a descriptive error message explaining why the last command failed. More...
 Returns a descriptive error message explaining why the last command failed. More...
 
DLLEXPORT int tjGetErrorCode (tjhandle handle)
 Returns a code indicating the severity of the last error. More...
 Returns a code indicating the severity of the last error. More...
 
- + - + - + - + - + - + - +

Variables

static const int tjMCUWidth [TJ_NUMSAMP]
 MCU block width (in pixels) for a given level of chrominance subsampling. More...
 MCU block width (in pixels) for a given level of chrominance subsampling. More...
 
static const int tjMCUHeight [TJ_NUMSAMP]
 MCU block height (in pixels) for a given level of chrominance subsampling. More...
 MCU block height (in pixels) for a given level of chrominance subsampling. More...
 
static const int tjRedOffset [TJ_NUMPF]
 Red offset (in bytes) for a given pixel format. More...
 Red offset (in bytes) for a given pixel format. More...
 
static const int tjGreenOffset [TJ_NUMPF]
 Green offset (in bytes) for a given pixel format. More...
 Green offset (in bytes) for a given pixel format. More...
 
static const int tjBlueOffset [TJ_NUMPF]
 Blue offset (in bytes) for a given pixel format. More...
 Blue offset (in bytes) for a given pixel format. More...
 
static const int tjAlphaOffset [TJ_NUMPF]
 Alpha offset (in bytes) for a given pixel format. More...
 Alpha offset (in bytes) for a given pixel format. More...
 
static const int tjPixelSize [TJ_NUMPF]
 Pixel size (in bytes) for a given pixel format. More...
 Pixel size (in bytes) for a given pixel format. More...
 

Detailed Description

TurboJPEG API.

This API provides an interface for generating, decoding, and transforming planar YUV and JPEG images in memory.

-

YUV Image Format Notes

+

+YUV Image Format Notes

Technically, the JPEG format uses the YCbCr colorspace (which is technically not a colorspace but a color transform), but per the convention of the digital video community, the TurboJPEG API uses "YUV" to refer to an image format consisting of Y, Cb, and Cr image planes.

-

Each plane is simply a 2D array of bytes, each byte representing the value of one of the components (Y, Cb, or Cr) at a particular location in the image. The width and height of each plane are determined by the image width, height, and level of chrominance subsampling. The luminance plane width is the image width padded to the nearest multiple of the horizontal subsampling factor (2 in the case of 4:2:0 and 4:2:2, 4 in the case of 4:1:1, 1 in the case of 4:4:4 or grayscale.) Similarly, the luminance plane height is the image height padded to the nearest multiple of the vertical subsampling factor (2 in the case of 4:2:0 or 4:4:0, 1 in the case of 4:4:4 or grayscale.) This is irrespective of any additional padding that may be specified as an argument to the various YUV functions. The chrominance plane width is equal to the luminance plane width divided by the horizontal subsampling factor, and the chrominance plane height is equal to the luminance plane height divided by the vertical subsampling factor.

-

For example, if the source image is 35 x 35 pixels and 4:2:2 subsampling is used, then the luminance plane would be 36 x 35 bytes, and each of the chrominance planes would be 18 x 35 bytes. If you specify a line padding of 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, and each of the chrominance planes would be 20 x 35 bytes.

+

Each plane is simply a 2D array of bytes, each byte representing the value of one of the components (Y, Cb, or Cr) at a particular location in the image. The width and height of each plane are determined by the image width, height, and level of chrominance subsampling. The luminance plane width is the image width padded to the nearest multiple of the horizontal subsampling factor (1 in the case of 4:4:4, grayscale, or 4:4:0; 2 in the case of 4:2:2 or 4:2:0; 4 in the case of 4:1:1.) Similarly, the luminance plane height is the image height padded to the nearest multiple of the vertical subsampling factor (1 in the case of 4:4:4, 4:2:2, grayscale, or 4:1:1; 2 in the case of 4:2:0 or 4:4:0.) This is irrespective of any additional padding that may be specified as an argument to the various YUV functions. The chrominance plane width is equal to the luminance plane width divided by the horizontal subsampling factor, and the chrominance plane height is equal to the luminance plane height divided by the vertical subsampling factor.

+

For example, if the source image is 35 x 35 pixels and 4:2:2 subsampling is used, then the luminance plane would be 36 x 35 bytes, and each of the chrominance planes would be 18 x 35 bytes. If you specify a row alignment of 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, and each of the chrominance planes would be 20 x 35 bytes.

Macro Definition Documentation

- + +

◆ TJ_NUMCS

+
@@ -377,7 +371,9 @@

Macro Definition Documentation

- + +

◆ TJ_NUMERR

+
@@ -391,7 +387,9 @@

Macro Definition Documentation

- + +

◆ TJ_NUMPF

+
@@ -405,7 +403,9 @@

Macro Definition Documentation

- + +

◆ TJ_NUMSAMP

+
@@ -419,7 +419,9 @@

Macro Definition Documentation

- + +

◆ TJ_NUMXOP

+
@@ -433,7 +435,9 @@

Macro Definition Documentation

- + +

◆ TJFLAG_ACCURATEDCT

+
@@ -443,12 +447,14 @@

Macro Definition Documentation

-

Use the most accurate DCT/IDCT algorithm available in the underlying codec.

-

The default if this flag is not specified is implementation-specific. For example, the implementation of TurboJPEG for libjpeg[-turbo] uses the fast algorithm by default when compressing, because this has been shown to have only a very slight effect on accuracy, but it uses the accurate algorithm when decompressing, because this has been shown to have a larger effect.

+

Use the most accurate DCT/IDCT algorithm available.

+

The default if this flag is not specified is implementation-specific. For example, the implementation of the TurboJPEG API in libjpeg-turbo uses the fast algorithm by default when compressing, because this has been shown to have only a very slight effect on accuracy, but it uses the accurate algorithm when decompressing, because this has been shown to have a larger effect.

- + +

◆ TJFLAG_BOTTOMUP

+
@@ -458,11 +464,13 @@

Macro Definition Documentation

-

The uncompressed source/destination image is stored in bottom-up (Windows, OpenGL) order, not top-down (X11) order.

+

Rows in the packed-pixel source/destination image are stored in bottom-up (Windows, OpenGL) order rather than in top-down (X11) order.

- + +

◆ TJFLAG_FASTDCT

+
@@ -472,12 +480,14 @@

Macro Definition Documentation

-

Use the fastest DCT/IDCT algorithm available in the underlying codec.

-

The default if this flag is not specified is implementation-specific. For example, the implementation of TurboJPEG for libjpeg[-turbo] uses the fast algorithm by default when compressing, because this has been shown to have only a very slight effect on accuracy, but it uses the accurate algorithm when decompressing, because this has been shown to have a larger effect.

+

Use the fastest DCT/IDCT algorithm available.

+

The default if this flag is not specified is implementation-specific. For example, the implementation of the TurboJPEG API in libjpeg-turbo uses the fast algorithm by default when compressing, because this has been shown to have only a very slight effect on accuracy, but it uses the accurate algorithm when decompressing, because this has been shown to have a larger effect.

- + +

◆ TJFLAG_FASTUPSAMPLE

+
@@ -487,12 +497,31 @@

Macro Definition Documentation

-

When decompressing an image that was compressed using chrominance subsampling, use the fastest chrominance upsampling algorithm available in the underlying codec.

+

When decompressing an image that was compressed using chrominance subsampling, use the fastest chrominance upsampling algorithm available.

The default is to use smooth upsampling, which creates a smooth transition between neighboring chrominance components in order to reduce upsampling artifacts in the decompressed image.

- + +

◆ TJFLAG_LIMITSCANS

+ +
+
+ + + + +
#define TJFLAG_LIMITSCANS
+
+ +

Limit the number of progressive JPEG scans that the decompression and transform functions will process.

+

If a progressive JPEG image contains an unreasonably large number of scans, then this flag will cause the decompression and transform functions to return an error. The primary purpose of this is to allow security-critical applications to guard against an exploit of the progressive JPEG format described in this report.

+ +
+
+ +

◆ TJFLAG_NOREALLOC

+
@@ -502,12 +531,14 @@

Macro Definition Documentation

-

Disable buffer (re)allocation.

-

If passed to one of the JPEG compression or transform functions, this flag will cause those functions to generate an error if the JPEG image buffer is invalid or too small rather than attempting to allocate or reallocate that buffer. This reproduces the behavior of earlier versions of TurboJPEG.

+

Disable JPEG buffer (re)allocation.

+

If passed to one of the JPEG compression or transform functions, this flag will cause those functions to generate an error if the JPEG destination buffer is invalid or too small, rather than attempt to allocate or reallocate that buffer.

- + +

◆ TJFLAG_PROGRESSIVE

+
@@ -522,7 +553,9 @@

Macro Definition Documentation

- + +

◆ TJFLAG_STOPONWARNING

+
@@ -532,12 +565,14 @@

Macro Definition Documentation

-

Immediately discontinue the current compression/decompression/transform operation if the underlying codec throws a warning (non-fatal error).

+

Immediately discontinue the current compression/decompression/transform operation if a warning (non-fatal error) occurs.

The default behavior is to allow the operation to complete unless a fatal error is encountered.

- + +

◆ TJPAD

+
@@ -551,11 +586,13 @@

Macro Definition Documentation

-

Pad the given width to the nearest 32-bit boundary.

+

Pad the given width to the nearest multiple of 4.

- + +

◆ TJSCALED

+
@@ -584,7 +621,9 @@

Macro Definition Documentation

- + +

◆ TJXOPT_COPYNONE

+
@@ -594,11 +633,13 @@

Macro Definition Documentation

-

This option will prevent tjTransform() from copying any extra markers (including EXIF and ICC profile data) from the source image to the output image.

+

This option will prevent tjTransform() from copying any extra markers (including EXIF and ICC profile data) from the source image to the destination image.

- + +

◆ TJXOPT_CROP

+
@@ -613,7 +654,9 @@

Macro Definition Documentation

- + +

◆ TJXOPT_GRAY

+
@@ -623,11 +666,13 @@

Macro Definition Documentation

-

This option will discard the color data in the input image and produce a grayscale output image.

+

This option will discard the color data in the source image and produce a grayscale destination image.

- + +

◆ TJXOPT_NOOUTPUT

+
@@ -637,11 +682,14 @@

Macro Definition Documentation

-

This option will prevent tjTransform() from outputting a JPEG image for this particular transform (this can be used in conjunction with a custom filter to capture the transformed DCT coefficients without transcoding them.)

+

This option will prevent tjTransform() from outputting a JPEG image for this particular transform.

+

(This can be used in conjunction with a custom filter to capture the transformed DCT coefficients without transcoding them.)

- + +

◆ TJXOPT_PERFECT

+
@@ -656,7 +704,9 @@

Macro Definition Documentation

- + +

◆ TJXOPT_PROGRESSIVE

+
@@ -666,12 +716,14 @@

Macro Definition Documentation

-

This option will enable progressive entropy coding in the output image generated by this particular transform.

-

Progressive entropy coding will generally improve compression relative to baseline entropy coding (the default), but it will reduce compression and decompression performance considerably.

+

This option will enable progressive entropy coding in the JPEG image generated by this particular transform.

+

Progressive entropy coding will generally improve compression relative to baseline entropy coding (the default), but it will reduce decompression performance considerably.

- + +

◆ TJXOPT_TRIM

+
@@ -686,7 +738,9 @@

Macro Definition Documentation

Typedef Documentation

- + +

◆ tjhandle

+
@@ -700,12 +754,14 @@

Typedef Documentation

- + +

◆ tjtransform

+
- +
typedef struct tjtransform tjtransformtypedef struct tjtransform tjtransform
@@ -715,7 +771,9 @@

Typedef Documentation

Enumeration Type Documentation

- + +

◆ TJCS

+
@@ -727,31 +785,28 @@

Enumeration Type Documentation

JPEG colorspaces.

- - - - -
Enumerator
TJCS_RGB  -

RGB colorspace.

-

When compressing the JPEG image, the R, G, and B components in the source image are reordered into image planes, but no colorspace conversion or subsampling is performed. RGB JPEG images can be decompressed to any of the extended RGB pixel formats or grayscale, but they cannot be decompressed to YUV images.

+
Enumerator
TJCS_RGB 

RGB colorspace.

+

When compressing the JPEG image, the R, G, and B components in the source image are reordered into image planes, but no colorspace conversion or subsampling is performed. RGB JPEG images can be decompressed to packed-pixel images with any of the extended RGB or grayscale pixel formats, but they cannot be decompressed to planar YUV images.

TJCS_YCbCr  -

YCbCr colorspace.

-

YCbCr is not an absolute colorspace but rather a mathematical transformation of RGB designed solely for storage and transmission. YCbCr images must be converted to RGB before they can actually be displayed. In the YCbCr colorspace, the Y (luminance) component represents the black & white portion of the original image, and the Cb and Cr (chrominance) components represent the color portion of the original image. Originally, the analog equivalent of this transformation allowed the same signal to drive both black & white and color televisions, but JPEG images use YCbCr primarily because it allows the color data to be optionally subsampled for the purposes of reducing bandwidth or disk space. YCbCr is the most common JPEG colorspace, and YCbCr JPEG images can be compressed from and decompressed to any of the extended RGB pixel formats or grayscale, or they can be decompressed to YUV planar images.

+
TJCS_YCbCr 

YCbCr colorspace.

+

YCbCr is not an absolute colorspace but rather a mathematical transformation of RGB designed solely for storage and transmission. YCbCr images must be converted to RGB before they can actually be displayed. In the YCbCr colorspace, the Y (luminance) component represents the black & white portion of the original image, and the Cb and Cr (chrominance) components represent the color portion of the original image. Originally, the analog equivalent of this transformation allowed the same signal to drive both black & white and color televisions, but JPEG images use YCbCr primarily because it allows the color data to be optionally subsampled for the purposes of reducing network or disk usage. YCbCr is the most common JPEG colorspace, and YCbCr JPEG images can be compressed from and decompressed to packed-pixel images with any of the extended RGB or grayscale pixel formats. YCbCr JPEG images can also be compressed from and decompressed to planar YUV images.

TJCS_GRAY  -

Grayscale colorspace.

-

The JPEG image retains only the luminance data (Y component), and any color data from the source image is discarded. Grayscale JPEG images can be compressed from and decompressed to any of the extended RGB pixel formats or grayscale, or they can be decompressed to YUV planar images.

+
TJCS_GRAY 

Grayscale colorspace.

+

The JPEG image retains only the luminance data (Y component), and any color data from the source image is discarded. Grayscale JPEG images can be compressed from and decompressed to packed-pixel images with any of the extended RGB or grayscale pixel formats, or they can be compressed from and decompressed to planar YUV images.

TJCS_CMYK  -

CMYK colorspace.

-

When compressing the JPEG image, the C, M, Y, and K components in the source image are reordered into image planes, but no colorspace conversion or subsampling is performed. CMYK JPEG images can only be decompressed to CMYK pixels.

+
TJCS_CMYK 

CMYK colorspace.

+

When compressing the JPEG image, the C, M, Y, and K components in the source image are reordered into image planes, but no colorspace conversion or subsampling is performed. CMYK JPEG images can only be decompressed to packed-pixel images with the CMYK pixel format.

TJCS_YCCK  -

YCCK colorspace.

-

YCCK (AKA "YCbCrK") is not an absolute colorspace but rather a mathematical transformation of CMYK designed solely for storage and transmission. It is to CMYK as YCbCr is to RGB. CMYK pixels can be reversibly transformed into YCCK, and as with YCbCr, the chrominance components in the YCCK pixels can be subsampled without incurring major perceptual loss. YCCK JPEG images can only be compressed from and decompressed to CMYK pixels.

+
TJCS_YCCK 

YCCK colorspace.

+

YCCK (AKA "YCbCrK") is not an absolute colorspace but rather a mathematical transformation of CMYK designed solely for storage and transmission. It is to CMYK as YCbCr is to RGB. CMYK pixels can be reversibly transformed into YCCK, and as with YCbCr, the chrominance components in the YCCK pixels can be subsampled without incurring major perceptual loss. YCCK JPEG images can only be compressed from and decompressed to packed-pixel images with the CMYK pixel format.

- + +

◆ TJERR

+
@@ -763,17 +818,17 @@

Enumeration Type Documentation

Error codes.

- -
Enumerator
TJERR_WARNING  -

The error was non-fatal and recoverable, but the image may still be corrupt.

+
Enumerator
TJERR_WARNING 

The error was non-fatal and recoverable, but the destination image may still be corrupt.

TJERR_FATAL  -

The error was fatal and non-recoverable.

+
TJERR_FATAL 

The error was fatal and non-recoverable.

- + +

◆ TJPF

+
@@ -785,63 +840,52 @@

Enumeration Type Documentation

Pixel formats.

- - - - - - - - - - - - -
Enumerator
TJPF_RGB  -

RGB pixel format.

+
Enumerator
TJPF_RGB 

RGB pixel format.

The red, green, and blue components in the image are stored in 3-byte pixels in the order R, G, B from lowest to highest byte address within each pixel.

TJPF_BGR  -

BGR pixel format.

+
TJPF_BGR 

BGR pixel format.

The red, green, and blue components in the image are stored in 3-byte pixels in the order B, G, R from lowest to highest byte address within each pixel.

TJPF_RGBX  -

RGBX pixel format.

+
TJPF_RGBX 

RGBX pixel format.

The red, green, and blue components in the image are stored in 4-byte pixels in the order R, G, B from lowest to highest byte address within each pixel. The X component is ignored when compressing and undefined when decompressing.

TJPF_BGRX  -

BGRX pixel format.

+
TJPF_BGRX 

BGRX pixel format.

The red, green, and blue components in the image are stored in 4-byte pixels in the order B, G, R from lowest to highest byte address within each pixel. The X component is ignored when compressing and undefined when decompressing.

TJPF_XBGR  -

XBGR pixel format.

+
TJPF_XBGR 

XBGR pixel format.

The red, green, and blue components in the image are stored in 4-byte pixels in the order R, G, B from highest to lowest byte address within each pixel. The X component is ignored when compressing and undefined when decompressing.

TJPF_XRGB  -

XRGB pixel format.

+
TJPF_XRGB 

XRGB pixel format.

The red, green, and blue components in the image are stored in 4-byte pixels in the order B, G, R from highest to lowest byte address within each pixel. The X component is ignored when compressing and undefined when decompressing.

TJPF_GRAY  -

Grayscale pixel format.

+
TJPF_GRAY 

Grayscale pixel format.

Each 1-byte pixel represents a luminance (brightness) level from 0 to 255.

TJPF_RGBA  -

RGBA pixel format.

+
TJPF_RGBA 

RGBA pixel format.

This is the same as TJPF_RGBX, except that when decompressing, the X component is guaranteed to be 0xFF, which can be interpreted as an opaque alpha channel.

TJPF_BGRA  -

BGRA pixel format.

+
TJPF_BGRA 

BGRA pixel format.

This is the same as TJPF_BGRX, except that when decompressing, the X component is guaranteed to be 0xFF, which can be interpreted as an opaque alpha channel.

TJPF_ABGR  -

ABGR pixel format.

+
TJPF_ABGR 

ABGR pixel format.

This is the same as TJPF_XBGR, except that when decompressing, the X component is guaranteed to be 0xFF, which can be interpreted as an opaque alpha channel.

TJPF_ARGB  -

ARGB pixel format.

+
TJPF_ARGB 

ARGB pixel format.

This is the same as TJPF_XRGB, except that when decompressing, the X component is guaranteed to be 0xFF, which can be interpreted as an opaque alpha channel.

TJPF_CMYK  -

CMYK pixel format.

-

Unlike RGB, which is an additive color model used primarily for display, CMYK (Cyan/Magenta/Yellow/Key) is a subtractive color model used primarily for printing. In the CMYK color model, the value of each color component typically corresponds to an amount of cyan, magenta, yellow, or black ink that is applied to a white background. In order to convert between CMYK and RGB, it is necessary to use a color management system (CMS.) A CMS will attempt to map colors within the printer's gamut to perceptually similar colors in the display's gamut and vice versa, but the mapping is typically not 1:1 or reversible, nor can it be defined with a simple formula. Thus, such a conversion is out of scope for a codec library. However, the TurboJPEG API allows for compressing CMYK pixels into a YCCK JPEG image (see TJCS_YCCK) and decompressing YCCK JPEG images into CMYK pixels.

+
TJPF_CMYK 

CMYK pixel format.

+

Unlike RGB, which is an additive color model used primarily for display, CMYK (Cyan/Magenta/Yellow/Key) is a subtractive color model used primarily for printing. In the CMYK color model, the value of each color component typically corresponds to an amount of cyan, magenta, yellow, or black ink that is applied to a white background. In order to convert between CMYK and RGB, it is necessary to use a color management system (CMS.) A CMS will attempt to map colors within the printer's gamut to perceptually similar colors in the display's gamut and vice versa, but the mapping is typically not 1:1 or reversible, nor can it be defined with a simple formula. Thus, such a conversion is out of scope for a codec library. However, the TurboJPEG API allows for compressing packed-pixel CMYK images into YCCK JPEG images (see TJCS_YCCK) and decompressing YCCK JPEG images into packed-pixel CMYK images.

TJPF_UNKNOWN  -

Unknown pixel format.

-

Currently this is only used by tjLoadImage().

+
TJPF_UNKNOWN 

Unknown pixel format.

+

Currently this is only used by tjLoadImage().

- + +

◆ TJSAMP

+
@@ -852,31 +896,25 @@

Enumeration Type Documentation

Chrominance subsampling options.

-

When pixels are converted from RGB to YCbCr (see TJCS_YCbCr) or from CMYK to YCCK (see TJCS_YCCK) as part of the JPEG compression process, some of the Cb and Cr (chrominance) components can be discarded or averaged together to produce a smaller image with little perceptible loss of image clarity (the human eye is more sensitive to small changes in brightness than to small changes in color.) This is called "chrominance subsampling".

+

When pixels are converted from RGB to YCbCr (see TJCS_YCbCr) or from CMYK to YCCK (see TJCS_YCCK) as part of the JPEG compression process, some of the Cb and Cr (chrominance) components can be discarded or averaged together to produce a smaller image with little perceptible loss of image clarity. (The human eye is more sensitive to small changes in brightness than to small changes in color.) This is called "chrominance subsampling".

- - - - - - @@ -884,7 +922,9 @@

Enumeration Type Documentation

- + +

◆ TJXOP

+
Enumerator
TJSAMP_444  -

4:4:4 chrominance subsampling (no chrominance subsampling).

+
Enumerator
TJSAMP_444 

4:4:4 chrominance subsampling (no chrominance subsampling).

The JPEG or YUV image will contain one chrominance component for every pixel in the source image.

TJSAMP_422  -

4:2:2 chrominance subsampling.

+
TJSAMP_422 

4:2:2 chrominance subsampling.

The JPEG or YUV image will contain one chrominance component for every 2x1 block of pixels in the source image.

TJSAMP_420  -

4:2:0 chrominance subsampling.

+
TJSAMP_420 

4:2:0 chrominance subsampling.

The JPEG or YUV image will contain one chrominance component for every 2x2 block of pixels in the source image.

TJSAMP_GRAY  -

Grayscale.

+
TJSAMP_GRAY 

Grayscale.

The JPEG or YUV image will contain no chrominance components.

TJSAMP_440  -

4:4:0 chrominance subsampling.

+
TJSAMP_440 

4:4:0 chrominance subsampling.

The JPEG or YUV image will contain one chrominance component for every 1x2 block of pixels in the source image.

Note
4:4:0 subsampling is not fully accelerated in libjpeg-turbo.
TJSAMP_411  -

4:1:1 chrominance subsampling.

+
TJSAMP_411 

4:1:1 chrominance subsampling.

The JPEG or YUV image will contain one chrominance component for every 4x1 block of pixels in the source image. JPEG images compressed with 4:1:1 subsampling will be almost exactly the same size as those compressed with 4:2:0 subsampling, and in the aggregate, both subsampling methods produce approximately the same perceptual quality. However, 4:1:1 is better able to reproduce sharp horizontal features.

Note
4:1:1 subsampling is not fully accelerated in libjpeg-turbo.
@@ -896,33 +936,25 @@

Enumeration Type Documentation

Transform operations for tjTransform()

- - - - - - - -
Enumerator
TJXOP_NONE  -

Do not transform the position of the image pixels.

+
Enumerator
TJXOP_NONE 

Do not transform the position of the image pixels.

TJXOP_HFLIP  -

Flip (mirror) image horizontally.

+
TJXOP_HFLIP 

Flip (mirror) image horizontally.

This transform is imperfect if there are any partial MCU blocks on the right edge (see TJXOPT_PERFECT.)

TJXOP_VFLIP  -

Flip (mirror) image vertically.

+
TJXOP_VFLIP 

Flip (mirror) image vertically.

This transform is imperfect if there are any partial MCU blocks on the bottom edge (see TJXOPT_PERFECT.)

TJXOP_TRANSPOSE  -

Transpose image (flip/mirror along upper left to lower right axis.) This transform is always perfect.

+
TJXOP_TRANSPOSE 

Transpose image (flip/mirror along upper left to lower right axis.) This transform is always perfect.

TJXOP_TRANSVERSE  -

Transverse transpose image (flip/mirror along upper right to lower left axis.) This transform is imperfect if there are any partial MCU blocks in the image (see TJXOPT_PERFECT.)

+
TJXOP_TRANSVERSE 

Transverse transpose image (flip/mirror along upper right to lower left axis.) This transform is imperfect if there are any partial MCU blocks in the image (see TJXOPT_PERFECT.)

TJXOP_ROT90  -

Rotate image clockwise by 90 degrees.

+
TJXOP_ROT90 

Rotate image clockwise by 90 degrees.

This transform is imperfect if there are any partial MCU blocks on the bottom edge (see TJXOPT_PERFECT.)

TJXOP_ROT180  -

Rotate image 180 degrees.

+
TJXOP_ROT180 

Rotate image 180 degrees.

This transform is imperfect if there are any partial MCU blocks in the image (see TJXOPT_PERFECT.)

TJXOP_ROT270  -

Rotate image counter-clockwise by 90 degrees.

+
TJXOP_ROT270 

Rotate image counter-clockwise by 90 degrees.

This transform is imperfect if there are any partial MCU blocks on the right edge (see TJXOPT_PERFECT.)

@@ -930,7 +962,9 @@

Enumeration Type Documentation

Function Documentation

- + +

◆ tjAlloc()

+
@@ -944,8 +978,8 @@

Function Documentation

-

Allocate an image buffer for use with TurboJPEG.

-

You should always use this function to allocate the JPEG destination buffer(s) for the compression and transform functions unless you are disabling automatic buffer (re)allocation (by setting TJFLAG_NOREALLOC.)

+

Allocate a byte buffer for use with TurboJPEG.

+

You should always use this function to allocate the JPEG destination buffer(s) for the compression and transform functions unless you are disabling automatic buffer (re)allocation (by setting TJFLAG_NOREALLOC.)

Parameters
@@ -953,11 +987,13 @@

Function Documentation

Returns
a pointer to a newly-allocated buffer with the specified number of bytes.
-
See Also
tjFree()
+
See also
tjFree()
- + +

◆ tjBufSize()

+
bytesthe number of bytes to allocate
@@ -988,7 +1024,7 @@

Function Documentation

The maximum size of the buffer (in bytes) required to hold a JPEG image with the given parameters.

-

The number of bytes returned by this function is larger than the size of the uncompressed source image. The reason for this is that the JPEG format uses 16-bit coefficients, and it is thus possible for a very high-quality JPEG image with very high-frequency content to expand rather than compress when converted to the JPEG format. Such images represent a very rare corner case, but since there is no way to predict the size of a JPEG image prior to compression, the corner case has to be handled.

+

The number of bytes returned by this function is larger than the size of the uncompressed source image. The reason for this is that the JPEG format uses 16-bit coefficients, so it is possible for a very high-quality source image with very high-frequency content to expand rather than compress when converted to the JPEG format. Such images represent very rare corner cases, but since there is no way to predict the size of a JPEG image prior to compression, the corner cases have to be handled.

Parameters
@@ -1001,7 +1037,9 @@

Function Documentation

- + +

◆ tjBufSizeYUV2()

+
widthwidth (in pixels) of the image
@@ -1015,7 +1053,7 @@

Function Documentation

- + @@ -1037,11 +1075,11 @@

Function Documentation

int pad, align,
-

The size of the buffer (in bytes) required to hold a YUV planar image with the given parameters.

+

The size of the buffer (in bytes) required to hold a unified planar YUV image with the given parameters.

Parameters
- +
widthwidth (in pixels) of the image
padthe width of each line in each plane of the image is padded to the nearest multiple of this number of bytes (must be a power of 2.)
alignrow alignment (in bytes) of the image (must be a power of 2.) Setting this parameter to n specifies that each row in each plane of the image will be padded to the nearest multiple of n bytes (1 = unpadded.)
heightheight (in pixels) of the image
subsamplevel of chrominance subsampling in the image (see Chrominance subsampling options.)
@@ -1051,7 +1089,9 @@

Function Documentation

- + +

◆ tjCompress2()

+
@@ -1129,22 +1169,22 @@

Function Documentation

-

Compress an RGB, grayscale, or CMYK image into a JPEG image.

+

Compress a packed-pixel RGB, grayscale, or CMYK image into a JPEG image.

Parameters
- + - + - - +If you choose option 1, then *jpegSize should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check *jpegBuf upon return from this function, as it may have changed. + @@ -1155,7 +1195,9 @@

Function Documentation

- + +

◆ tjCompressFromYUV()

+
handlea handle to a TurboJPEG compressor or transformer instance
srcBufpointer to an image buffer containing RGB, grayscale, or CMYK pixels to be compressed
srcBufpointer to a buffer containing a packed-pixel RGB, grayscale, or CMYK source image to be compressed
widthwidth (in pixels) of the source image
pitchbytes per line in the source image. Normally, this should be width * tjPixelSize[pixelFormat] if the image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each line of the image is padded to the nearest 32-bit boundary, as is the case for Windows bitmaps. You can also be clever and use this parameter to skip lines, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
pitchbytes per row in the source image. Normally this should be width * tjPixelSize[pixelFormat], if the image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each row of the image is padded to the nearest multiple of 4 bytes, as is the case for Windows bitmaps. You can also be clever and use this parameter to skip rows, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
heightheight (in pixels) of the source image
pixelFormatpixel format of the source image (see Pixel formats.)
jpegBufaddress of a pointer to an image buffer that will receive the JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to accommodate the size of the JPEG image. Thus, you can choose to:
    -
  1. pre-allocate the JPEG buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. +
jpegBufaddress of a pointer to a byte buffer that will receive the JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to accommodate the size of the JPEG image. Thus, you can choose to:
    +
  1. pre-allocate the JPEG buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. set *jpegBuf to NULL to tell TurboJPEG to allocate the buffer for you, or
  3. -
  4. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize(). This should ensure that the buffer never has to be re-allocated (setting TJFLAG_NOREALLOC guarantees that it won't be.)
  5. +
  6. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize(). This should ensure that the buffer never has to be re-allocated. (Setting TJFLAG_NOREALLOC guarantees that it won't be.)
-If you choose option 1, *jpegSize should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check *jpegBuf upon return from this function, as it may have changed.
jpegSizepointer to an unsigned long variable that holds the size of the JPEG image buffer. If *jpegBuf points to a pre-allocated buffer, then *jpegSize should be set to the size of the buffer. Upon return, *jpegSize will contain the size of the JPEG image (in bytes.) If *jpegBuf points to a JPEG image buffer that is being reused from a previous call to one of the JPEG compression functions, then *jpegSize is ignored.
jpegSizepointer to an unsigned long variable that holds the size of the JPEG buffer. If *jpegBuf points to a pre-allocated buffer, then *jpegSize should be set to the size of the buffer. Upon return, *jpegSize will contain the size of the JPEG image (in bytes.) If *jpegBuf points to a JPEG buffer that is being reused from a previous call to one of the JPEG compression functions, then *jpegSize is ignored.
jpegSubsampthe level of chrominance subsampling to be used when generating the JPEG image (see Chrominance subsampling options.)
jpegQualthe image quality of the generated JPEG image (1 = worst, 100 = best)
flagsthe bitwise OR of one or more of the flags
@@ -1181,7 +1223,7 @@

Function Documentation

- + @@ -1227,22 +1269,22 @@

Function Documentation

int pad, align,
-

Compress a YUV planar image into a JPEG image.

+

Compress a unified planar YUV image into a JPEG image.

Parameters
- - - - + + + + - - +If you choose option 1, then *jpegSize should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check *jpegBuf upon return from this function, as it may have changed. +
handlea handle to a TurboJPEG compressor or transformer instance
srcBufpointer to an image buffer containing a YUV planar image to be compressed. The size of this buffer should match the value returned by tjBufSizeYUV2() for the given image width, height, padding, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) image planes should be stored sequentially in the source buffer (refer to YUV Image Format Notes.)
widthwidth (in pixels) of the source image. If the width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed within TurboJPEG.
padthe line padding used in the source image. For instance, if each line in each plane of the YUV image is padded to the nearest multiple of 4 bytes, then pad should be set to 4.
heightheight (in pixels) of the source image. If the height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed within TurboJPEG.
srcBufpointer to a buffer containing a unified planar YUV source image to be compressed. The size of this buffer should match the value returned by tjBufSizeYUV2() for the given image width, height, row alignment, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) image planes should be stored sequentially in the buffer. (Refer to YUV Image Format Notes.)
widthwidth (in pixels) of the source image. If the width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed.
alignrow alignment (in bytes) of the source image (must be a power of 2.) Setting this parameter to n indicates that each row in each plane of the source image is padded to the nearest multiple of n bytes (1 = unpadded.)
heightheight (in pixels) of the source image. If the height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed.
subsampthe level of chrominance subsampling used in the source image (see Chrominance subsampling options.)
jpegBufaddress of a pointer to an image buffer that will receive the JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to accommodate the size of the JPEG image. Thus, you can choose to:
    -
  1. pre-allocate the JPEG buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. +
jpegBufaddress of a pointer to a byte buffer that will receive the JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to accommodate the size of the JPEG image. Thus, you can choose to:
    +
  1. pre-allocate the JPEG buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. set *jpegBuf to NULL to tell TurboJPEG to allocate the buffer for you, or
  3. -
  4. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize(). This should ensure that the buffer never has to be re-allocated (setting TJFLAG_NOREALLOC guarantees that it won't be.)
  5. +
  6. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize(). This should ensure that the buffer never has to be re-allocated. (Setting TJFLAG_NOREALLOC guarantees that it won't be.)
-If you choose option 1, *jpegSize should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check *jpegBuf upon return from this function, as it may have changed.
jpegSizepointer to an unsigned long variable that holds the size of the JPEG image buffer. If *jpegBuf points to a pre-allocated buffer, then *jpegSize should be set to the size of the buffer. Upon return, *jpegSize will contain the size of the JPEG image (in bytes.) If *jpegBuf points to a JPEG image buffer that is being reused from a previous call to one of the JPEG compression functions, then *jpegSize is ignored.
jpegSizepointer to an unsigned long variable that holds the size of the JPEG buffer. If *jpegBuf points to a pre-allocated buffer, then *jpegSize should be set to the size of the buffer. Upon return, *jpegSize will contain the size of the JPEG image (in bytes.) If *jpegBuf points to a JPEG buffer that is being reused from a previous call to one of the JPEG compression functions, then *jpegSize is ignored.
jpegQualthe image quality of the generated JPEG image (1 = worst, 100 = best)
flagsthe bitwise OR of one or more of the flags
@@ -1252,7 +1294,9 @@

Function Documentation

- + +

◆ tjCompressFromYUVPlanes()

+
@@ -1328,18 +1372,18 @@

Function Documentation

Parameters
- - - - + + + + - - +If you choose option 1, then *jpegSize should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check *jpegBuf upon return from this function, as it may have changed. +
handlea handle to a TurboJPEG compressor or transformer instance
srcPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if compressing a grayscale image) that contain a YUV image to be compressed. These planes can be contiguous or non-contiguous in memory. The size of each plane should match the value returned by tjPlaneSizeYUV() for the given image width, height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
widthwidth (in pixels) of the source image. If the width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed within TurboJPEG.
stridesan array of integers, each specifying the number of bytes per line in the corresponding plane of the YUV source image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective plane widths. You can adjust the strides in order to specify an arbitrary amount of line padding in each plane or to create a JPEG image from a subregion of a larger YUV planar image.
heightheight (in pixels) of the source image. If the height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed within TurboJPEG.
srcPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if compressing a grayscale image) that contain a YUV source image to be compressed. These planes can be contiguous or non-contiguous in memory. The size of each plane should match the value returned by tjPlaneSizeYUV() for the given image width, height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
widthwidth (in pixels) of the source image. If the width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed.
stridesan array of integers, each specifying the number of bytes per row in the corresponding plane of the YUV source image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective plane widths. You can adjust the strides in order to specify an arbitrary amount of row padding in each plane or to create a JPEG image from a subregion of a larger planar YUV image.
heightheight (in pixels) of the source image. If the height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed.
subsampthe level of chrominance subsampling used in the source image (see Chrominance subsampling options.)
jpegBufaddress of a pointer to an image buffer that will receive the JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to accommodate the size of the JPEG image. Thus, you can choose to:
    -
  1. pre-allocate the JPEG buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. +
jpegBufaddress of a pointer to a byte buffer that will receive the JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to accommodate the size of the JPEG image. Thus, you can choose to:
    +
  1. pre-allocate the JPEG buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. set *jpegBuf to NULL to tell TurboJPEG to allocate the buffer for you, or
  3. -
  4. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize(). This should ensure that the buffer never has to be re-allocated (setting TJFLAG_NOREALLOC guarantees that it won't be.)
  5. +
  6. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize(). This should ensure that the buffer never has to be re-allocated. (Setting TJFLAG_NOREALLOC guarantees that it won't be.)
-If you choose option 1, *jpegSize should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check *jpegBuf upon return from this function, as it may have changed.
jpegSizepointer to an unsigned long variable that holds the size of the JPEG image buffer. If *jpegBuf points to a pre-allocated buffer, then *jpegSize should be set to the size of the buffer. Upon return, *jpegSize will contain the size of the JPEG image (in bytes.) If *jpegBuf points to a JPEG image buffer that is being reused from a previous call to one of the JPEG compression functions, then *jpegSize is ignored.
jpegSizepointer to an unsigned long variable that holds the size of the JPEG buffer. If *jpegBuf points to a pre-allocated buffer, then *jpegSize should be set to the size of the buffer. Upon return, *jpegSize will contain the size of the JPEG image (in bytes.) If *jpegBuf points to a JPEG buffer that is being reused from a previous call to one of the JPEG compression functions, then *jpegSize is ignored.
jpegQualthe image quality of the generated JPEG image (1 = worst, 100 = best)
flagsthe bitwise OR of one or more of the flags
@@ -1349,7 +1393,9 @@

Function Documentation

- + +

◆ tjDecodeYUV()

+
@@ -1369,7 +1415,7 @@

Function Documentation

- + @@ -1421,17 +1467,17 @@

Function Documentation

int pad, align,
-

Decode a YUV planar image into an RGB or grayscale image.

-

This function uses the accelerated color conversion routines in the underlying codec but does not execute any of the other steps in the JPEG decompression process.

+

Decode a unified planar YUV image into a packed-pixel RGB or grayscale image.

+

This function performs color conversion (which is accelerated in the libjpeg-turbo implementation) but does not execute any of the other steps in the JPEG decompression process.

Parameters
- - + + - + - + @@ -1442,7 +1488,9 @@

Function Documentation

- + +

◆ tjDecodeYUVPlanes()

+
handlea handle to a TurboJPEG decompressor or transformer instance
srcBufpointer to an image buffer containing a YUV planar image to be decoded. The size of this buffer should match the value returned by tjBufSizeYUV2() for the given image width, height, padding, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) image planes should be stored sequentially in the source buffer (refer to YUV Image Format Notes.)
padUse this parameter to specify that the width of each line in each plane of the YUV source image is padded to the nearest multiple of this number of bytes (must be a power of 2.)
srcBufpointer to a buffer containing a unified planar YUV source image to be decoded. The size of this buffer should match the value returned by tjBufSizeYUV2() for the given image width, height, row alignment, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) image planes should be stored sequentially in the source buffer. (Refer to YUV Image Format Notes.)
alignrow alignment (in bytes) of the YUV source image (must be a power of 2.) Setting this parameter to n indicates that each row in each plane of the YUV source image is padded to the nearest multiple of n bytes (1 = unpadded.)
subsampthe level of chrominance subsampling used in the YUV source image (see Chrominance subsampling options.)
dstBufpointer to an image buffer that will receive the decoded image. This buffer should normally be pitch * height bytes in size, but the dstBuf pointer can also be used to decode into a specific region of a larger buffer.
dstBufpointer to a buffer that will receive the packed-pixel decoded image. This buffer should normally be pitch * height bytes in size, but the dstBuf pointer can also be used to decode into a specific region of a larger buffer.
widthwidth (in pixels) of the source and destination images
pitchbytes per line in the destination image. Normally, this should be width * tjPixelSize[pixelFormat] if the destination image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each line of the destination image should be padded to the nearest 32-bit boundary, as is the case for Windows bitmaps. You can also be clever and use the pitch parameter to skip lines, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
pitchbytes per row in the destination image. Normally this should be set to width * tjPixelSize[pixelFormat], if the destination image should be unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each row of the destination image should be padded to the nearest multiple of 4 bytes, as is the case for Windows bitmaps. You can also be clever and use the pitch parameter to skip rows, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
heightheight (in pixels) of the source and destination images
pixelFormatpixel format of the destination image (see Pixel formats.)
flagsthe bitwise OR of one or more of the flags
@@ -1514,17 +1562,17 @@

Function Documentation

-

Decode a set of Y, U (Cb), and V (Cr) image planes into an RGB or grayscale image.

-

This function uses the accelerated color conversion routines in the underlying codec but does not execute any of the other steps in the JPEG decompression process.

+

Decode a set of Y, U (Cb), and V (Cr) image planes into a packed-pixel RGB or grayscale image.

+

This function performs color conversion (which is accelerated in the libjpeg-turbo implementation) but does not execute any of the other steps in the JPEG decompression process.

Parameters
- - + + - + - + @@ -1535,7 +1583,9 @@

Function Documentation

- + +

◆ tjDecompress2()

+
handlea handle to a TurboJPEG decompressor or transformer instance
srcPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if decoding a grayscale image) that contain a YUV image to be decoded. These planes can be contiguous or non-contiguous in memory. The size of each plane should match the value returned by tjPlaneSizeYUV() for the given image width, height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
stridesan array of integers, each specifying the number of bytes per line in the corresponding plane of the YUV source image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective plane widths. You can adjust the strides in order to specify an arbitrary amount of line padding in each plane or to decode a subregion of a larger YUV planar image.
srcPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if decoding a grayscale image) that contain a YUV image to be decoded. These planes can be contiguous or non-contiguous in memory. The size of each plane should match the value returned by tjPlaneSizeYUV() for the given image width, height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
stridesan array of integers, each specifying the number of bytes per row in the corresponding plane of the YUV source image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective plane widths. You can adjust the strides in order to specify an arbitrary amount of row padding in each plane or to decode a subregion of a larger planar YUV image.
subsampthe level of chrominance subsampling used in the YUV source image (see Chrominance subsampling options.)
dstBufpointer to an image buffer that will receive the decoded image. This buffer should normally be pitch * height bytes in size, but the dstBuf pointer can also be used to decode into a specific region of a larger buffer.
dstBufpointer to a buffer that will receive the packed-pixel decoded image. This buffer should normally be pitch * height bytes in size, but the dstBuf pointer can also be used to decode into a specific region of a larger buffer.
widthwidth (in pixels) of the source and destination images
pitchbytes per line in the destination image. Normally, this should be width * tjPixelSize[pixelFormat] if the destination image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each line of the destination image should be padded to the nearest 32-bit boundary, as is the case for Windows bitmaps. You can also be clever and use the pitch parameter to skip lines, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
pitchbytes per row in the destination image. Normally this should be set to width * tjPixelSize[pixelFormat], if the destination image should be unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each row of the destination image should be padded to the nearest multiple of 4 bytes, as is the case for Windows bitmaps. You can also be clever and use the pitch parameter to skip rows, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
heightheight (in pixels) of the source and destination images
pixelFormatpixel format of the destination image (see Pixel formats.)
flagsthe bitwise OR of one or more of the flags
@@ -1601,15 +1651,15 @@

Function Documentation

-

Decompress a JPEG image to an RGB, grayscale, or CMYK image.

+

Decompress a JPEG image into a packed-pixel RGB, grayscale, or CMYK image.

Parameters
- + - + - + @@ -1620,7 +1670,9 @@

Function Documentation

- + +

◆ tjDecompressHeader3()

+
handlea handle to a TurboJPEG decompressor or transformer instance
jpegBufpointer to a buffer containing the JPEG image to decompress
jpegBufpointer to a byte buffer containing the JPEG image to decompress
jpegSizesize of the JPEG image (in bytes)
dstBufpointer to an image buffer that will receive the decompressed image. This buffer should normally be pitch * scaledHeight bytes in size, where scaledHeight can be determined by calling TJSCALED() with the JPEG image height and one of the scaling factors returned by tjGetScalingFactors(). The dstBuf pointer may also be used to decompress into a specific region of a larger buffer.
dstBufpointer to a buffer that will receive the packed-pixel decompressed image. This buffer should normally be pitch * scaledHeight bytes in size, where scaledHeight can be determined by calling TJSCALED() with the JPEG image height and one of the scaling factors returned by tjGetScalingFactors(). The dstBuf pointer may also be used to decompress into a specific region of a larger buffer.
widthdesired width (in pixels) of the destination image. If this is different than the width of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired width. If width is set to 0, then only the height will be considered when determining the scaled image size.
pitchbytes per line in the destination image. Normally, this is scaledWidth * tjPixelSize[pixelFormat] if the decompressed image is unpadded, else TJPAD(scaledWidth * tjPixelSize[pixelFormat]) if each line of the decompressed image is padded to the nearest 32-bit boundary, as is the case for Windows bitmaps. (NOTE: scaledWidth can be determined by calling TJSCALED() with the JPEG image width and one of the scaling factors returned by tjGetScalingFactors().) You can also be clever and use the pitch parameter to skip lines, etc. Setting this parameter to 0 is the equivalent of setting it to scaledWidth * tjPixelSize[pixelFormat].
pitchbytes per row in the destination image. Normally this should be set to scaledWidth * tjPixelSize[pixelFormat], if the destination image should be unpadded, or TJPAD(scaledWidth * tjPixelSize[pixelFormat]) if each row of the destination image should be padded to the nearest multiple of 4 bytes, as is the case for Windows bitmaps. (NOTE: scaledWidth can be determined by calling TJSCALED() with the JPEG image width and one of the scaling factors returned by tjGetScalingFactors().) You can also be clever and use the pitch parameter to skip rows, etc. Setting this parameter to 0 is the equivalent of setting it to scaledWidth * tjPixelSize[pixelFormat].
heightdesired height (in pixels) of the destination image. If this is different than the height of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired height. If height is set to 0, then only the width will be considered when determining the scaled image size.
pixelFormatpixel format of the destination image (see Pixel formats.)
flagsthe bitwise OR of one or more of the flags
@@ -1674,16 +1726,16 @@

Function Documentation

-

Retrieve information about a JPEG image without decompressing it.

+

Retrieve information about a JPEG image without decompressing it, or prime the decompressor with quantization and Huffman tables.

Parameters
- - - - - - + + + + + +
handlea handle to a TurboJPEG decompressor or transformer instance
jpegBufpointer to a buffer containing a JPEG image
jpegSizesize of the JPEG image (in bytes)
widthpointer to an integer variable that will receive the width (in pixels) of the JPEG image
heightpointer to an integer variable that will receive the height (in pixels) of the JPEG image
jpegSubsamppointer to an integer variable that will receive the level of chrominance subsampling used when the JPEG image was compressed (see Chrominance subsampling options.)
jpegColorspacepointer to an integer variable that will receive one of the JPEG colorspace constants, indicating the colorspace of the JPEG image (see JPEG colorspaces.)
jpegBufpointer to a byte buffer containing a JPEG image or an "abbreviated table specification" (AKA "tables-only") datastream. Passing a tables-only datastream to this function primes the decompressor with quantization and Huffman tables that can be used when decompressing subsequent "abbreviated image" datastreams. This is useful, for instance, when decompressing video streams in which all frames share the same quantization and Huffman tables.
jpegSizesize of the JPEG image or tables-only datastream (in bytes)
widthpointer to an integer variable that will receive the width (in pixels) of the JPEG image. If jpegBuf points to a tables-only datastream, then width is ignored.
heightpointer to an integer variable that will receive the height (in pixels) of the JPEG image. If jpegBuf points to a tables-only datastream, then height is ignored.
jpegSubsamppointer to an integer variable that will receive the level of chrominance subsampling used when the JPEG image was compressed (see Chrominance subsampling options.) If jpegBuf points to a tables-only datastream, then jpegSubsamp is ignored.
jpegColorspacepointer to an integer variable that will receive one of the JPEG colorspace constants, indicating the colorspace of the JPEG image (see JPEG colorspaces.) If jpegBuf points to a tables-only datastream, then jpegColorspace is ignored.
@@ -1691,7 +1743,9 @@

Function Documentation

- + +

◆ tjDecompressToYUV2()

+
@@ -1729,7 +1783,7 @@

Function Documentation

- + @@ -1751,17 +1805,17 @@

Function Documentation

int pad, align,
-

Decompress a JPEG image to a YUV planar image.

-

This function performs JPEG decompression but leaves out the color conversion step, so a planar YUV image is generated instead of an RGB image.

+

Decompress a JPEG image into a unified planar YUV image.

+

This function performs JPEG decompression but leaves out the color conversion step, so a planar YUV image is generated instead of a packed-pixel image.

Parameters
- + - - - - + + + +
handlea handle to a TurboJPEG decompressor or transformer instance
jpegBufpointer to a buffer containing the JPEG image to decompress
jpegBufpointer to a byte buffer containing the JPEG image to decompress
jpegSizesize of the JPEG image (in bytes)
dstBufpointer to an image buffer that will receive the YUV image. Use tjBufSizeYUV2() to determine the appropriate size for this buffer based on the image width, height, padding, and level of subsampling. The Y, U (Cb), and V (Cr) image planes will be stored sequentially in the buffer (refer to YUV Image Format Notes.)
widthdesired width (in pixels) of the YUV image. If this is different than the width of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired width. If width is set to 0, then only the height will be considered when determining the scaled image size. If the scaled width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed within TurboJPEG.
padthe width of each line in each plane of the YUV image will be padded to the nearest multiple of this number of bytes (must be a power of 2.) To generate images suitable for X Video, pad should be set to 4.
heightdesired height (in pixels) of the YUV image. If this is different than the height of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired height. If height is set to 0, then only the width will be considered when determining the scaled image size. If the scaled height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed within TurboJPEG.
dstBufpointer to a buffer that will receive the unified planar YUV decompressed image. Use tjBufSizeYUV2() to determine the appropriate size for this buffer based on the scaled image width, scaled image height, row alignment, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) image planes will be stored sequentially in the buffer. (Refer to YUV Image Format Notes.)
widthdesired width (in pixels) of the YUV image. If this is different than the width of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired width. If width is set to 0, then only the height will be considered when determining the scaled image size. If the scaled width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed.
alignrow alignment (in bytes) of the YUV image (must be a power of 2.) Setting this parameter to n will cause each row in each plane of the YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.) To generate images suitable for X Video, align should be set to 4.
heightdesired height (in pixels) of the YUV image. If this is different than the height of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired height. If height is set to 0, then only the width will be considered when determining the scaled image size. If the scaled height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed.
flagsthe bitwise OR of one or more of the flags
@@ -1770,7 +1824,9 @@

Function Documentation

- + +

◆ tjDecompressToYUVPlanes()

+
@@ -1831,16 +1887,16 @@

Function Documentation

Decompress a JPEG image into separate Y, U (Cb), and V (Cr) image planes.

-

This function performs JPEG decompression but leaves out the color conversion step, so a planar YUV image is generated instead of an RGB image.

+

This function performs JPEG decompression but leaves out the color conversion step, so a planar YUV image is generated instead of a packed-pixel image.

Parameters
- + - - - - + + + +
handlea handle to a TurboJPEG decompressor or transformer instance
jpegBufpointer to a buffer containing the JPEG image to decompress
jpegBufpointer to a byte buffer containing the JPEG image to decompress
jpegSizesize of the JPEG image (in bytes)
dstPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if decompressing a grayscale image) that will receive the YUV image. These planes can be contiguous or non-contiguous in memory. Use tjPlaneSizeYUV() to determine the appropriate size for each plane based on the scaled image width, scaled image height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
widthdesired width (in pixels) of the YUV image. If this is different than the width of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired width. If width is set to 0, then only the height will be considered when determining the scaled image size. If the scaled width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed within TurboJPEG.
stridesan array of integers, each specifying the number of bytes per line in the corresponding plane of the output image. Setting the stride for any plane to 0 is the same as setting it to the scaled plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective scaled plane widths. You can adjust the strides in order to add an arbitrary amount of line padding to each plane or to decompress the JPEG image into a subregion of a larger YUV planar image.
heightdesired height (in pixels) of the YUV image. If this is different than the height of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired height. If height is set to 0, then only the width will be considered when determining the scaled image size. If the scaled height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed within TurboJPEG.
dstPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if decompressing a grayscale image) that will receive the decompressed image. These planes can be contiguous or non-contiguous in memory. Use tjPlaneSizeYUV() to determine the appropriate size for each plane based on the scaled image width, scaled image height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
widthdesired width (in pixels) of the YUV image. If this is different than the width of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired width. If width is set to 0, then only the height will be considered when determining the scaled image size. If the scaled width is not an even multiple of the MCU block width (see tjMCUWidth), then an intermediate buffer copy will be performed.
stridesan array of integers, each specifying the number of bytes per row in the corresponding plane of the YUV image. Setting the stride for any plane to 0 is the same as setting it to the scaled plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective scaled plane widths. You can adjust the strides in order to add an arbitrary amount of row padding to each plane or to decompress the JPEG image into a subregion of a larger planar YUV image.
heightdesired height (in pixels) of the YUV image. If this is different than the height of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired height. If height is set to 0, then only the width will be considered when determining the scaled image size. If the scaled height is not an even multiple of the MCU block height (see tjMCUHeight), then an intermediate buffer copy will be performed.
flagsthe bitwise OR of one or more of the flags
@@ -1849,7 +1905,9 @@

Function Documentation

- + +

◆ tjDestroy()

+
@@ -1874,7 +1932,9 @@

Function Documentation

- + +

◆ tjEncodeYUV3()

+
@@ -1924,7 +1984,7 @@

Function Documentation

- + @@ -1946,18 +2006,18 @@

Function Documentation

int pad, align,
-

Encode an RGB or grayscale image into a YUV planar image.

-

This function uses the accelerated color conversion routines in the underlying codec but does not execute any of the other steps in the JPEG compression process.

+

Encode a packed-pixel RGB or grayscale image into a unified planar YUV image.

+

This function performs color conversion (which is accelerated in the libjpeg-turbo implementation) but does not execute any of the other steps in the JPEG compression process.

Parameters
- + - + - - + +
handlea handle to a TurboJPEG compressor or transformer instance
srcBufpointer to an image buffer containing RGB or grayscale pixels to be encoded
srcBufpointer to a buffer containing a packed-pixel RGB or grayscale source image to be encoded
widthwidth (in pixels) of the source image
pitchbytes per line in the source image. Normally, this should be width * tjPixelSize[pixelFormat] if the image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each line of the image is padded to the nearest 32-bit boundary, as is the case for Windows bitmaps. You can also be clever and use this parameter to skip lines, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
pitchbytes per row in the source image. Normally this should be width * tjPixelSize[pixelFormat], if the image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each row of the image is padded to the nearest multiple of 4 bytes, as is the case for Windows bitmaps. You can also be clever and use this parameter to skip rows, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
heightheight (in pixels) of the source image
pixelFormatpixel format of the source image (see Pixel formats.)
dstBufpointer to an image buffer that will receive the YUV image. Use tjBufSizeYUV2() to determine the appropriate size for this buffer based on the image width, height, padding, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) image planes will be stored sequentially in the buffer (refer to YUV Image Format Notes.)
padthe width of each line in each plane of the YUV image will be padded to the nearest multiple of this number of bytes (must be a power of 2.) To generate images suitable for X Video, pad should be set to 4.
dstBufpointer to a buffer that will receive the unified planar YUV image. Use tjBufSizeYUV2() to determine the appropriate size for this buffer based on the image width, height, row alignment, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) image planes will be stored sequentially in the buffer. (Refer to YUV Image Format Notes.)
alignrow alignment (in bytes) of the YUV image (must be a power of 2.) Setting this parameter to n will cause each row in each plane of the YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.) To generate images suitable for X Video, align should be set to 4.
subsampthe level of chrominance subsampling to be used when generating the YUV image (see Chrominance subsampling options.) To generate images suitable for X Video, subsamp should be set to TJSAMP_420. This produces an image compatible with the I420 (AKA "YUV420P") format.
flagsthe bitwise OR of one or more of the flags
@@ -1967,7 +2027,9 @@

Function Documentation

- + +

◆ tjEncodeYUVPlanes()

+
@@ -2039,18 +2101,18 @@

Function Documentation

-

Encode an RGB or grayscale image into separate Y, U (Cb), and V (Cr) image planes.

-

This function uses the accelerated color conversion routines in the underlying codec but does not execute any of the other steps in the JPEG compression process.

+

Encode a packed-pixel RGB or grayscale image into separate Y, U (Cb), and V (Cr) image planes.

+

This function performs color conversion (which is accelerated in the libjpeg-turbo implementation) but does not execute any of the other steps in the JPEG compression process.

Parameters
- + - + - - + +
handlea handle to a TurboJPEG compressor or transformer instance
srcBufpointer to an image buffer containing RGB or grayscale pixels to be encoded
srcBufpointer to a buffer containing a packed-pixel RGB or grayscale source image to be encoded
widthwidth (in pixels) of the source image
pitchbytes per line in the source image. Normally, this should be width * tjPixelSize[pixelFormat] if the image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each line of the image is padded to the nearest 32-bit boundary, as is the case for Windows bitmaps. You can also be clever and use this parameter to skip lines, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
pitchbytes per row in the source image. Normally this should be width * tjPixelSize[pixelFormat], if the image is unpadded, or TJPAD(width * tjPixelSize[pixelFormat]) if each row of the image is padded to the nearest multiple of 4 bytes, as is the case for Windows bitmaps. You can also be clever and use this parameter to skip rows, etc. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
heightheight (in pixels) of the source image
pixelFormatpixel format of the source image (see Pixel formats.)
dstPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if generating a grayscale image) that will receive the encoded image. These planes can be contiguous or non-contiguous in memory. Use tjPlaneSizeYUV() to determine the appropriate size for each plane based on the image width, height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
stridesan array of integers, each specifying the number of bytes per line in the corresponding plane of the output image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective plane widths. You can adjust the strides in order to add an arbitrary amount of line padding to each plane or to encode an RGB or grayscale image into a subregion of a larger YUV planar image.
dstPlanesan array of pointers to Y, U (Cb), and V (Cr) image planes (or just a Y plane, if generating a grayscale image) that will receive the encoded image. These planes can be contiguous or non-contiguous in memory. Use tjPlaneSizeYUV() to determine the appropriate size for each plane based on the image width, height, strides, and level of chrominance subsampling. Refer to YUV Image Format Notes for more details.
stridesan array of integers, each specifying the number of bytes per row in the corresponding plane of the YUV image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see YUV Image Format Notes.) If strides is NULL, then the strides for all planes will be set to their respective plane widths. You can adjust the strides in order to add an arbitrary amount of row padding to each plane or to encode an RGB or grayscale image into a subregion of a larger planar YUV image.
subsampthe level of chrominance subsampling to be used when generating the YUV image (see Chrominance subsampling options.) To generate images suitable for X Video, subsamp should be set to TJSAMP_420. This produces an image compatible with the I420 (AKA "YUV420P") format.
flagsthe bitwise OR of one or more of the flags
@@ -2060,7 +2122,9 @@

Function Documentation

- + +

◆ tjFree()

+
@@ -2074,19 +2138,21 @@

Function Documentation

-

Free an image buffer previously allocated by TurboJPEG.

-

You should always use this function to free JPEG destination buffer(s) that were automatically (re)allocated by the compression and transform functions or that were manually allocated using tjAlloc().

+

Free a byte buffer previously allocated by TurboJPEG.

+

You should always use this function to free JPEG destination buffer(s) that were automatically (re)allocated by the compression and transform functions or that were manually allocated using tjAlloc().

Parameters
bufferaddress of the buffer to free. If the address is NULL, then this function has no effect.
-
See Also
tjAlloc()
+
See also
tjAlloc()
- + +

◆ tjGetErrorCode()

+
@@ -2112,7 +2178,9 @@

Function Documentation

- + +

◆ tjGetErrorStr2()

+
@@ -2129,7 +2197,7 @@

Function Documentation

Returns a descriptive error message explaining why the last command failed.

Parameters
- +
handlea handle to a TurboJPEG compressor, decompressor, or transformer instance, or NULL if the error was generated by a global function (but note that retrieving the error message for a global function is not thread-safe.)
handlea handle to a TurboJPEG compressor, decompressor, or transformer instance, or NULL if the error was generated by a global function (but note that retrieving the error message for a global function is thread-safe only on platforms that support thread-local storage.)
@@ -2137,7 +2205,9 @@

Function Documentation

- + +

◆ tjGetScalingFactors()

+
@@ -2145,16 +2215,16 @@

Function Documentation

- +
DLLEXPORT tjscalingfactor* tjGetScalingFactors ( int * numscalingfactors)numScalingFactors)
-

Returns a list of fractional scaling factors that the JPEG decompressor in this implementation of TurboJPEG supports.

+

Returns a list of fractional scaling factors that the JPEG decompressor supports.

Parameters
- +
numscalingfactorspointer to an integer variable that will receive the number of elements in the list
numScalingFactorspointer to an integer variable that will receive the number of elements in the list
@@ -2162,7 +2232,9 @@

Function Documentation

- + +

◆ tjInitCompress()

+
@@ -2181,7 +2253,9 @@

Function Documentation

- + +

◆ tjInitDecompress()

+
@@ -2200,7 +2274,9 @@

Function Documentation

- + +

◆ tjInitTransform()

+
@@ -2219,7 +2295,9 @@

Function Documentation

- + +

◆ tjLoadImage()

+
@@ -2267,29 +2345,31 @@

Function Documentation

-

Load an uncompressed image from disk into memory.

+

Load a packed-pixel image from disk into memory.

Parameters
- - - - - + + + +
filenamename of a file containing an uncompressed image in Windows BMP or PBMPLUS (PPM/PGM) format
widthpointer to an integer variable that will receive the width (in pixels) of the uncompressed image
alignrow alignment of the image buffer to be returned (must be a power of 2.) For instance, setting this parameter to 4 will cause all rows in the image buffer to be padded to the nearest 32-bit boundary, and setting this parameter to 1 will cause all rows in the image buffer to be unpadded.
heightpointer to an integer variable that will receive the height (in pixels) of the uncompressed image
pixelFormatpointer to an integer variable that specifies or will receive the pixel format of the uncompressed image buffer. The behavior of tjLoadImage() will vary depending on the value of *pixelFormat passed to the function:
    -
  • TJPF_UNKNOWN : The uncompressed image buffer returned by the function will use the most optimal pixel format for the file type, and *pixelFormat will contain the ID of this pixel format upon successful return from the function.
  • -
  • TJPF_GRAY : Only PGM files and 8-bit BMP files with a grayscale colormap can be loaded.
  • -
  • TJPF_CMYK : The RGB or grayscale pixels stored in the file will be converted using a quick & dirty algorithm that is suitable only for testing purposes (proper conversion between CMYK and other formats requires a color management system.)
  • -
  • Other pixel formats : The uncompressed image buffer will use the specified pixel format, and pixel format conversion will be performed if necessary.
  • +
filenamename of a file containing a packed-pixel image in Windows BMP or PBMPLUS (PPM/PGM) format
widthpointer to an integer variable that will receive the width (in pixels) of the packed-pixel image
alignrow alignment of the packed-pixel buffer to be returned (must be a power of 2.) Setting this parameter to n will cause all rows in the buffer to be padded to the nearest multiple of n bytes (1 = unpadded.)
heightpointer to an integer variable that will receive the height (in pixels) of the packed-pixel image
pixelFormatpointer to an integer variable that specifies or will receive the pixel format of the packed-pixel buffer. The behavior of tjLoadImage() will vary depending on the value of *pixelFormat passed to the function:
    +
  • TJPF_UNKNOWN : The packed-pixel buffer returned by this function will use the most optimal pixel format for the file type, and *pixelFormat will contain the ID of that pixel format upon successful return from this function.
  • +
  • TJPF_GRAY : Only PGM files and 8-bit-per-pixel BMP files with a grayscale colormap can be loaded.
  • +
  • TJPF_CMYK : The RGB or grayscale pixels stored in the file will be converted using a quick & dirty algorithm that is suitable only for testing purposes. (Proper conversion between CMYK and other formats requires a color management system.)
  • +
  • Other pixel formats : The packed-pixel buffer will use the specified pixel format, and pixel format conversion will be performed if necessary.
flagsthe bitwise OR of one or more of the flags.
-
Returns
a pointer to a newly-allocated buffer containing the uncompressed image, converted to the chosen pixel format and with the chosen row alignment, or NULL if an error occurred (see tjGetErrorStr2().) This buffer should be freed using tjFree().
+
Returns
a pointer to a newly-allocated buffer containing the packed-pixel image, converted to the chosen pixel format and with the chosen row alignment, or NULL if an error occurred (see tjGetErrorStr2().) This buffer should be freed using tjFree().
- + +

◆ tjPlaneHeight()

+
@@ -2333,7 +2413,9 @@

Function Documentation

- + +

◆ tjPlaneSizeYUV()

+
@@ -2380,7 +2462,7 @@

Function Documentation

- +
componentIDID number of the image plane (0 = Y, 1 = U/Cb, 2 = V/Cr)
widthwidth (in pixels) of the YUV image. NOTE: this is the width of the whole image, not the plane width.
stridebytes per line in the image plane. Setting this to 0 is the equivalent of setting it to the plane width.
stridebytes per row in the image plane. Setting this to 0 is the equivalent of setting it to the plane width.
heightheight (in pixels) of the YUV image. NOTE: this is the height of the whole image, not the plane height.
subsamplevel of chrominance subsampling in the image (see Chrominance subsampling options.)
@@ -2390,7 +2472,9 @@

Function Documentation

- + +

◆ tjPlaneWidth()

+
@@ -2434,7 +2518,9 @@

Function Documentation

- + +

◆ tjSaveImage()

+
@@ -2488,15 +2574,15 @@

Function Documentation

-

Save an uncompressed image from memory to disk.

+

Save a packed-pixel image from memory to disk.

Parameters
- - - - - - + + + + + +
filenamename of a file to which to save the uncompressed image. The image will be stored in Windows BMP or PBMPLUS (PPM/PGM) format, depending on the file extension.
bufferpointer to an image buffer containing RGB, grayscale, or CMYK pixels to be saved
widthwidth (in pixels) of the uncompressed image
pitchbytes per line in the image buffer. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
heightheight (in pixels) of the uncompressed image
pixelFormatpixel format of the image buffer (see Pixel formats.) If this parameter is set to TJPF_GRAY, then the image will be stored in PGM or 8-bit (indexed color) BMP format. Otherwise, the image will be stored in PPM or 24-bit BMP format. If this parameter is set to TJPF_CMYK, then the CMYK pixels will be converted to RGB using a quick & dirty algorithm that is suitable only for testing (proper conversion between CMYK and other formats requires a color management system.)
filenamename of a file to which to save the packed-pixel image. The image will be stored in Windows BMP or PBMPLUS (PPM/PGM) format, depending on the file extension.
bufferpointer to a buffer containing a packed-pixel RGB, grayscale, or CMYK image to be saved
widthwidth (in pixels) of the packed-pixel image
pitchbytes per row in the packed-pixel image. Setting this parameter to 0 is the equivalent of setting it to width * tjPixelSize[pixelFormat].
heightheight (in pixels) of the packed-pixel image
pixelFormatpixel format of the packed-pixel image (see Pixel formats.) If this parameter is set to TJPF_GRAY, then the image will be stored in PGM or 8-bit-per-pixel (indexed color) BMP format. Otherwise, the image will be stored in PPM or 24-bit-per-pixel BMP format. If this parameter is set to TJPF_CMYK, then the CMYK pixels will be converted to RGB using a quick & dirty algorithm that is suitable only for testing purposes. (Proper conversion between CMYK and other formats requires a color management system.)
flagsthe bitwise OR of one or more of the flags.
@@ -2505,7 +2591,9 @@

Function Documentation

- + +

◆ tjTransform()

+
@@ -2570,17 +2658,17 @@

Function Documentation

Parameters
- + - - - +If you choose option 1, then dstSizes[i] should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check dstBufs[i] upon return from this function, as it may have changed. + +
handlea handle to a TurboJPEG transformer instance
jpegBufpointer to a buffer containing the JPEG source image to transform
jpegBufpointer to a byte buffer containing the JPEG source image to transform
jpegSizesize of the JPEG source image (in bytes)
nthe number of transformed JPEG images to generate
dstBufspointer to an array of n image buffers. dstBufs[i] will receive a JPEG image that has been transformed using the parameters in transforms[i]. TurboJPEG has the ability to reallocate the JPEG buffer to accommodate the size of the JPEG image. Thus, you can choose to:
    -
  1. pre-allocate the JPEG buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. +
dstBufspointer to an array of n byte buffers. dstBufs[i] will receive a JPEG image that has been transformed using the parameters in transforms[i]. TurboJPEG has the ability to reallocate the JPEG destination buffer to accommodate the size of the transformed JPEG image. Thus, you can choose to:
    +
  1. pre-allocate the JPEG destination buffer with an arbitrary size using tjAlloc() and let TurboJPEG grow the buffer as needed,
  2. set dstBufs[i] to NULL to tell TurboJPEG to allocate the buffer for you, or
  3. -
  4. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize() with the transformed or cropped width and height. Under normal circumstances, this should ensure that the buffer never has to be re-allocated (setting TJFLAG_NOREALLOC guarantees that it won't be.) Note, however, that there are some rare cases (such as transforming images with a large amount of embedded EXIF or ICC profile data) in which the output image will be larger than the worst-case size, and TJFLAG_NOREALLOC cannot be used in those cases.
  5. +
  6. pre-allocate the buffer to a "worst case" size determined by calling tjBufSize() with the transformed or cropped width and height and the level of subsampling used in the source image. Under normal circumstances, this should ensure that the buffer never has to be re-allocated. (Setting TJFLAG_NOREALLOC guarantees that it won't be.) Note, however, that there are some rare cases (such as transforming images with a large amount of embedded EXIF or ICC profile data) in which the transformed JPEG image will be larger than the worst-case size, and TJFLAG_NOREALLOC cannot be used in those cases.
-If you choose option 1, dstSizes[i] should be set to the size of your pre-allocated buffer. In any case, unless you have set TJFLAG_NOREALLOC, you should always check dstBufs[i] upon return from this function, as it may have changed.
dstSizespointer to an array of n unsigned long variables that will receive the actual sizes (in bytes) of each transformed JPEG image. If dstBufs[i] points to a pre-allocated buffer, then dstSizes[i] should be set to the size of the buffer. Upon return, dstSizes[i] will contain the size of the JPEG image (in bytes.)
transformspointer to an array of n tjtransform structures, each of which specifies the transform parameters and/or cropping region for the corresponding transformed output image.
dstSizespointer to an array of n unsigned long variables that will receive the actual sizes (in bytes) of each transformed JPEG image. If dstBufs[i] points to a pre-allocated buffer, then dstSizes[i] should be set to the size of the buffer. Upon return, dstSizes[i] will contain the size of the transformed JPEG image (in bytes.)
transformspointer to an array of n tjtransform structures, each of which specifies the transform parameters and/or cropping region for the corresponding transformed JPEG image.
flagsthe bitwise OR of one or more of the flags
@@ -2590,7 +2678,9 @@

Function Documentation

Variable Documentation

- + +

◆ tjAlphaOffset

+
@@ -2609,11 +2699,13 @@

Variable Documentation

Alpha offset (in bytes) for a given pixel format.

-

This specifies the number of bytes that the Alpha component is offset from the start of the pixel. For instance, if a pixel of format TJ_BGRA is stored in char pixel[], then the alpha component will be pixel[tjAlphaOffset[TJ_BGRA]]. This will be -1 if the pixel format does not have an alpha component.

+

This specifies the number of bytes that the alpha component is offset from the start of the pixel. For instance, if a pixel of format TJPF_BGRA is stored in unsigned char pixel[], then the alpha component will be pixel[tjAlphaOffset[TJPF_BGRA]]. This will be -1 if the pixel format does not have an alpha component.

- + +

◆ tjBlueOffset

+
@@ -2632,11 +2724,13 @@

Variable Documentation

Blue offset (in bytes) for a given pixel format.

-

This specifies the number of bytes that the Blue component is offset from the start of the pixel. For instance, if a pixel of format TJ_BGRX is stored in char pixel[], then the blue component will be pixel[tjBlueOffset[TJ_BGRX]]. This will be -1 if the pixel format does not have a blue component.

+

This specifies the number of bytes that the blue component is offset from the start of the pixel. For instance, if a pixel of format TJPF_BGRX is stored in unsigned char pixel[], then the blue component will be pixel[tjBlueOffset[TJPF_BGRX]]. This will be -1 if the pixel format does not have a blue component.

- + +

◆ tjGreenOffset

+
@@ -2655,11 +2749,13 @@

Variable Documentation

Green offset (in bytes) for a given pixel format.

-

This specifies the number of bytes that the green component is offset from the start of the pixel. For instance, if a pixel of format TJ_BGRX is stored in char pixel[], then the green component will be pixel[tjGreenOffset[TJ_BGRX]]. This will be -1 if the pixel format does not have a green component.

+

This specifies the number of bytes that the green component is offset from the start of the pixel. For instance, if a pixel of format TJPF_BGRX is stored in unsigned char pixel[], then the green component will be pixel[tjGreenOffset[TJPF_BGRX]]. This will be -1 if the pixel format does not have a green component.

- + +

◆ tjMCUHeight

+
@@ -2678,8 +2774,7 @@

Variable Documentation

MCU block height (in pixels) for a given level of chrominance subsampling.

-

MCU block sizes:

-
    +

    MCU block sizes:

    • 8x8 for no subsampling or grayscale
    • 16x8 for 4:2:2
    • 8x16 for 4:4:0
    • @@ -2689,7 +2784,9 @@

      Variable Documentation

- + +

◆ tjMCUWidth

+
@@ -2708,8 +2805,7 @@

Variable Documentation

MCU block width (in pixels) for a given level of chrominance subsampling.

-

MCU block sizes:

-
    +

    MCU block sizes:

    • 8x8 for no subsampling or grayscale
    • 16x8 for 4:2:2
    • 8x16 for 4:4:0
    • @@ -2719,7 +2815,9 @@

      Variable Documentation

- + +

◆ tjPixelSize

+
@@ -2741,7 +2839,9 @@

Variable Documentation

- + +

◆ tjRedOffset

+
@@ -2760,16 +2860,14 @@

Variable Documentation

Red offset (in bytes) for a given pixel format.

-

This specifies the number of bytes that the red component is offset from the start of the pixel. For instance, if a pixel of format TJ_BGRX is stored in char pixel[], then the red component will be pixel[tjRedOffset[TJ_BGRX]]. This will be -1 if the pixel format does not have a red component.

+

This specifies the number of bytes that the red component is offset from the start of the pixel. For instance, if a pixel of format TJPF_BGRX is stored in unsigned char pixel[], then the red component will be pixel[tjRedOffset[TJPF_BGRX]]. This will be -1 if the pixel format does not have a red component.

diff --git a/third-party/mozjpeg/mozjpeg/doc/html/index.html b/third-party/mozjpeg/mozjpeg/doc/html/index.html index a60f4d07a62..a6f272a393e 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/index.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/index.html @@ -1,18 +1,17 @@ - + - + +TurboJPEG: Main Page + - @@ -22,9 +21,9 @@
- @@ -32,40 +31,29 @@
+
TurboJPEG -  2.0 +  2.1.4
- + - + + + +
+
@@ -82,9 +70,7 @@
diff --git a/third-party/mozjpeg/mozjpeg/doc/html/jquery.js b/third-party/mozjpeg/mozjpeg/doc/html/jquery.js index 63939e76dd2..103c32d79b7 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/jquery.js +++ b/third-party/mozjpeg/mozjpeg/doc/html/jquery.js @@ -1,8 +1,35 @@ -/*! jQuery v1.7.1 jquery.com | jquery.org/license */ -(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; -f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")), -f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() -{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c) -{if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"
","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0a;a++)for(i in o[a])n=o[a][i],o[a].hasOwnProperty(i)&&void 0!==n&&(e[i]=t.isPlainObject(n)?t.isPlainObject(e[i])?t.widget.extend({},e[i],n):t.widget.extend({},n):n);return e},t.widget.bridge=function(e,i){var n=i.prototype.widgetFullName||e;t.fn[e]=function(o){var a="string"==typeof o,r=s.call(arguments,1),h=this;return a?this.length||"instance"!==o?this.each(function(){var i,s=t.data(this,n);return"instance"===o?(h=s,!1):s?t.isFunction(s[o])&&"_"!==o.charAt(0)?(i=s[o].apply(s,r),i!==s&&void 0!==i?(h=i&&i.jquery?h.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+o+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+o+"'")}):h=void 0:(r.length&&(o=t.widget.extend.apply(null,[o].concat(r))),this.each(function(){var e=t.data(this,n);e?(e.option(o||{}),e._init&&e._init()):t.data(this,n,new i(o,this))})),h}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+o.eventNamespace,c=h[2];c?n.on(l,c,r):i.on(l,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(u.test(t[0])?e/100:1),parseFloat(t[1])*(u.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o=Math.max,a=Math.abs,r=/left|center|right/,h=/top|center|bottom/,l=/[\+\-]\d+(\.[\d]+)?%?/,c=/^\w+/,u=/%$/,d=t.fn.position;t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("
"),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widthi?"left":e>0?"right":"center",vertical:0>r?"top":s>0?"bottom":"middle"};l>p&&p>a(e+i)&&(u.horizontal="center"),c>f&&f>a(s+r)&&(u.vertical="middle"),u.important=o(a(e),a(i))>o(a(s),a(r))?"horizontal":"vertical",n.using.call(this,t,u)}),h.offset(t.extend(D,{using:r}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,h=n-r,l=r+e.collisionWidth-a-n;e.collisionWidth>a?h>0&&0>=l?(i=t.left+h+e.collisionWidth-a-n,t.left+=h-i):t.left=l>0&&0>=h?n:h>l?n+a-e.collisionWidth:n:h>0?t.left+=h:l>0?t.left-=l:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,h=n-r,l=r+e.collisionHeight-a-n;e.collisionHeight>a?h>0&&0>=l?(i=t.top+h+e.collisionHeight-a-n,t.top+=h-i):t.top=l>0&&0>=h?n:h>l?n+a-e.collisionHeight:n:h>0?t.top+=h:l>0?t.top-=l:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,r=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=t.left-e.collisionPosition.marginLeft,c=l-h,u=l+e.collisionWidth-r-h,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-r-o,(0>i||a(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-h,(s>0||u>a(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,r=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=t.top-e.collisionPosition.marginTop,c=l-h,u=l+e.collisionHeight-r-h,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,m=-2*e.offset[1];0>c?(s=t.top+p+f+m+e.collisionHeight-r-o,(0>s||a(c)>s)&&(t.top+=p+f+m)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+m-h,(i>0||u>a(i))&&(t.top+=p+f+m))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}}),t.ui.focusable=function(i,s){var n,o,a,r,h,l=i.nodeName.toLowerCase();return"area"===l?(n=i.parentNode,o=n.name,i.href&&o&&"map"===n.nodeName.toLowerCase()?(a=t("img[usemap='#"+o+"']"),a.length>0&&a.is(":visible")):!1):(/^(input|select|textarea|button|object)$/.test(l)?(r=!i.disabled,r&&(h=t(i).closest("fieldset")[0],h&&(r=!h.disabled))):r="a"===l?i.href||s:s,r&&t(i).is(":visible")&&e(t(i)))},t.extend(t.expr[":"],{focusable:function(e){return t.ui.focusable(e,null!=t.attr(e,"tabindex"))}}),t.ui.focusable,t.fn.form=function(){return"string"==typeof this[0].form?this.closest("form"):t(this[0].form)},t.ui.formResetMixin={_formResetHandler:function(){var e=t(this);setTimeout(function(){var i=e.data("ui-form-reset-instances");t.each(i,function(){this.refresh()})})},_bindFormResetHandler:function(){if(this.form=this.element.form(),this.form.length){var t=this.form.data("ui-form-reset-instances")||[];t.length||this.form.on("reset.ui-form-reset",this._formResetHandler),t.push(this),this.form.data("ui-form-reset-instances",t)}},_unbindFormResetHandler:function(){if(this.form.length){var e=this.form.data("ui-form-reset-instances");e.splice(t.inArray(this,e),1),e.length?this.form.data("ui-form-reset-instances",e):this.form.removeData("ui-form-reset-instances").off("reset.ui-form-reset")}}},"1.7"===t.fn.jquery.substring(0,3)&&(t.each(["Width","Height"],function(e,i){function s(e,i,s,o){return t.each(n,function(){i-=parseFloat(t.css(e,"padding"+this))||0,s&&(i-=parseFloat(t.css(e,"border"+this+"Width"))||0),o&&(i-=parseFloat(t.css(e,"margin"+this))||0)}),i}var n="Width"===i?["Left","Right"]:["Top","Bottom"],o=i.toLowerCase(),a={innerWidth:t.fn.innerWidth,innerHeight:t.fn.innerHeight,outerWidth:t.fn.outerWidth,outerHeight:t.fn.outerHeight};t.fn["inner"+i]=function(e){return void 0===e?a["inner"+i].call(this):this.each(function(){t(this).css(o,s(this,e)+"px")})},t.fn["outer"+i]=function(e,n){return"number"!=typeof e?a["outer"+i].call(this,e):this.each(function(){t(this).css(o,s(this,e,!0,n)+"px")})}}),t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.ui.escapeSelector=function(){var t=/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g;return function(e){return e.replace(t,"\\$1")}}(),t.fn.labels=function(){var e,i,s,n,o;return this[0].labels&&this[0].labels.length?this.pushStack(this[0].labels):(n=this.eq(0).parents("label"),s=this.attr("id"),s&&(e=this.eq(0).parents().last(),o=e.add(e.length?e.siblings():this.siblings()),i="label[for='"+t.ui.escapeSelector(s)+"']",n=n.add(o.find(i).addBack(i))),this.pushStack(n))},t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.extend(t.expr[":"],{tabbable:function(e){var i=t.attr(e,"tabindex"),s=null!=i;return(!s||i>=0)&&t.ui.focusable(e,s)}}),t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase());var n=!1;t(document).on("mouseup",function(){n=!1}),t.widget("ui.mouse",{version:"1.12.1",options:{cancel:"input, textarea, button, select, option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.on("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).on("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.off("."+this.widgetName),this._mouseMoveDelegate&&this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(e){if(!n){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(e),this._mouseDownEvent=e;var i=this,s=1===e.which,o="string"==typeof this.options.cancel&&e.target.nodeName?t(e.target).closest(this.options.cancel).length:!1;return s&&!o&&this._mouseCapture(e)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(e)!==!1,!this._mouseStarted)?(e.preventDefault(),!0):(!0===t.data(e.target,this.widgetName+".preventClickEvent")&&t.removeData(e.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return i._mouseMove(t)},this._mouseUpDelegate=function(t){return i._mouseUp(t)},this.document.on("mousemove."+this.widgetName,this._mouseMoveDelegate).on("mouseup."+this.widgetName,this._mouseUpDelegate),e.preventDefault(),n=!0,!0)):!0}},_mouseMove:function(e){if(this._mouseMoved){if(t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button)return this._mouseUp(e);if(!e.which)if(e.originalEvent.altKey||e.originalEvent.ctrlKey||e.originalEvent.metaKey||e.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(e)}return(e.which||e.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer),this.ignoreMissingWhich=!1,n=!1,e.preventDefault()},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),t.ui.plugin={add:function(e,i,s){var n,o=t.ui[e].prototype;for(n in s)o.plugins[n]=o.plugins[n]||[],o.plugins[n].push([i,s[n]])},call:function(t,e,i,s){var n,o=t.plugins[e];if(o&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(n=0;o.length>n;n++)t.options[o[n][0]]&&o[n][1].apply(t.element,i)}},t.widget("ui.resizable",t.ui.mouse,{version:"1.12.1",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,classes:{"ui-resizable-se":"ui-icon ui-icon-gripsmall-diagonal-se"},containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_num:function(t){return parseFloat(t)||0},_isNumber:function(t){return!isNaN(parseFloat(t))},_hasScroll:function(e,i){if("hidden"===t(e).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",n=!1;return e[s]>0?!0:(e[s]=1,n=e[s]>0,e[s]=0,n)},_create:function(){var e,i=this.options,s=this;this._addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!i.aspectRatio,aspectRatio:i.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:i.helper||i.ghost||i.animate?i.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/^(canvas|textarea|input|select|button|img)$/i)&&(this.element.wrap(t("
").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,e={marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom"),marginLeft:this.originalElement.css("marginLeft")},this.element.css(e),this.originalElement.css("margin",0),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css(e),this._proportionallyResize()),this._setupHandles(),i.autoHide&&t(this.element).on("mouseenter",function(){i.disabled||(s._removeClass("ui-resizable-autohide"),s._handles.show())}).on("mouseleave",function(){i.disabled||s.resizing||(s._addClass("ui-resizable-autohide"),s._handles.hide())}),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeData("resizable").removeData("ui-resizable").off(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_setOption:function(t,e){switch(this._super(t,e),t){case"handles":this._removeHandles(),this._setupHandles();break;default:}},_setupHandles:function(){var e,i,s,n,o,a=this.options,r=this;if(this.handles=a.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this._handles=t(),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),s=this.handles.split(","),this.handles={},i=0;s.length>i;i++)e=t.trim(s[i]),n="ui-resizable-"+e,o=t("
"),this._addClass(o,"ui-resizable-handle "+n),o.css({zIndex:a.zIndex}),this.handles[e]=".ui-resizable-"+e,this.element.append(o);this._renderAxis=function(e){var i,s,n,o;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String?this.handles[i]=this.element.children(this.handles[i]).first().show():(this.handles[i].jquery||this.handles[i].nodeType)&&(this.handles[i]=t(this.handles[i]),this._on(this.handles[i],{mousedown:r._mouseDown})),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i)&&(s=t(this.handles[i],this.element),o=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,o),this._proportionallyResize()),this._handles=this._handles.add(this.handles[i])},this._renderAxis(this.element),this._handles=this._handles.add(this.element.find(".ui-resizable-handle")),this._handles.disableSelection(),this._handles.on("mouseover",function(){r.resizing||(this.className&&(o=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),r.axis=o&&o[1]?o[1]:"se")}),a.autoHide&&(this._handles.hide(),this._addClass("ui-resizable-autohide"))},_removeHandles:function(){this._handles.remove()},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(e){var i,s,n,o=this.options,a=this.element;return this.resizing=!0,this._renderProxy(),i=this._num(this.helper.css("left")),s=this._num(this.helper.css("top")),o.containment&&(i+=t(o.containment).scrollLeft()||0,s+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:i,top:s},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:a.width(),height:a.height()},this.originalSize=this._helper?{width:a.outerWidth(),height:a.outerHeight()}:{width:a.width(),height:a.height()},this.sizeDiff={width:a.outerWidth()-a.width(),height:a.outerHeight()-a.height()},this.originalPosition={left:i,top:s},this.originalMousePosition={left:e.pageX,top:e.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,n=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===n?this.axis+"-resize":n),this._addClass("ui-resizable-resizing"),this._propagate("start",e),!0},_mouseDrag:function(e){var i,s,n=this.originalMousePosition,o=this.axis,a=e.pageX-n.left||0,r=e.pageY-n.top||0,h=this._change[o];return this._updatePrevProperties(),h?(i=h.apply(this,[e,a,r]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),s=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(s)||(this._updatePrevProperties(),this._trigger("resize",e,this.ui()),this._applyChanges()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,o,a,r,h,l=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&this._hasScroll(i[0],"left")?0:c.sizeDiff.height,o=s?0:c.sizeDiff.width,a={width:c.helper.width()-o,height:c.helper.height()-n},r=parseFloat(c.element.css("left"))+(c.position.left-c.originalPosition.left)||null,h=parseFloat(c.element.css("top"))+(c.position.top-c.originalPosition.top)||null,l.animate||this.element.css(t.extend(a,{top:h,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!l.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this._removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var t={};return this.position.top!==this.prevPosition.top&&(t.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(t.left=this.position.left+"px"),this.size.width!==this.prevSize.width&&(t.width=this.size.width+"px"),this.size.height!==this.prevSize.height&&(t.height=this.size.height+"px"),this.helper.css(t),t},_updateVirtualBoundaries:function(t){var e,i,s,n,o,a=this.options;o={minWidth:this._isNumber(a.minWidth)?a.minWidth:0,maxWidth:this._isNumber(a.maxWidth)?a.maxWidth:1/0,minHeight:this._isNumber(a.minHeight)?a.minHeight:0,maxHeight:this._isNumber(a.maxHeight)?a.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,s=o.minWidth/this.aspectRatio,i=o.maxHeight*this.aspectRatio,n=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),s>o.minHeight&&(o.minHeight=s),o.maxWidth>i&&(o.maxWidth=i),o.maxHeight>n&&(o.maxHeight=n)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),this._isNumber(t.left)&&(this.position.left=t.left),this._isNumber(t.top)&&(this.position.top=t.top),this._isNumber(t.height)&&(this.size.height=t.height),this._isNumber(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,i=this.size,s=this.axis;return this._isNumber(t.height)?t.width=t.height*this.aspectRatio:this._isNumber(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===s&&(t.left=e.left+(i.width-t.width),t.top=null),"nw"===s&&(t.top=e.top+(i.height-t.height),t.left=e.left+(i.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,i=this.axis,s=this._isNumber(t.width)&&e.maxWidth&&e.maxWidtht.width,a=this._isNumber(t.height)&&e.minHeight&&e.minHeight>t.height,r=this.originalPosition.left+this.originalSize.width,h=this.originalPosition.top+this.originalSize.height,l=/sw|nw|w/.test(i),c=/nw|ne|n/.test(i);return o&&(t.width=e.minWidth),a&&(t.height=e.minHeight),s&&(t.width=e.maxWidth),n&&(t.height=e.maxHeight),o&&l&&(t.left=r-e.minWidth),s&&l&&(t.left=r-e.maxWidth),a&&c&&(t.top=h-e.minHeight),n&&c&&(t.top=h-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_getPaddingPlusBorderDimensions:function(t){for(var e=0,i=[],s=[t.css("borderTopWidth"),t.css("borderRightWidth"),t.css("borderBottomWidth"),t.css("borderLeftWidth")],n=[t.css("paddingTop"),t.css("paddingRight"),t.css("paddingBottom"),t.css("paddingLeft")];4>e;e++)i[e]=parseFloat(s[e])||0,i[e]+=parseFloat(n[e])||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var t,e=0,i=this.helper||this.element;this._proportionallyResizeElements.length>e;e++)t=this._proportionallyResizeElements[e],this.outerDimensions||(this.outerDimensions=this._getPaddingPlusBorderDimensions(t)),t.css({height:i.height()-this.outerDimensions.height||0,width:i.width()-this.outerDimensions.width||0})},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("
"),this._addClass(this.helper,this._helper),this.helper.css({width:this.element.outerWidth(),height:this.element.outerHeight(),position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element +},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).resizable("instance"),s=i.options,n=i._proportionallyResizeElements,o=n.length&&/textarea/i.test(n[0].nodeName),a=o&&i._hasScroll(n[0],"left")?0:i.sizeDiff.height,r=o?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-a},l=parseFloat(i.element.css("left"))+(i.position.left-i.originalPosition.left)||null,c=parseFloat(i.element.css("top"))+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(h,c&&l?{top:c,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseFloat(i.element.css("width")),height:parseFloat(i.element.css("height")),top:parseFloat(i.element.css("top")),left:parseFloat(i.element.css("left"))};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var e,i,s,n,o,a,r,h=t(this).resizable("instance"),l=h.options,c=h.element,u=l.containment,d=u instanceof t?u.get(0):/parent/.test(u)?c.parent().get(0):u;d&&(h.containerElement=t(d),/document/.test(u)||u===document?(h.containerOffset={left:0,top:0},h.containerPosition={left:0,top:0},h.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(e=t(d),i=[],t(["Top","Right","Left","Bottom"]).each(function(t,s){i[t]=h._num(e.css("padding"+s))}),h.containerOffset=e.offset(),h.containerPosition=e.position(),h.containerSize={height:e.innerHeight()-i[3],width:e.innerWidth()-i[1]},s=h.containerOffset,n=h.containerSize.height,o=h.containerSize.width,a=h._hasScroll(d,"left")?d.scrollWidth:o,r=h._hasScroll(d)?d.scrollHeight:n,h.parentData={element:d,left:s.left,top:s.top,width:a,height:r}))},resize:function(e){var i,s,n,o,a=t(this).resizable("instance"),r=a.options,h=a.containerOffset,l=a.position,c=a._aspectRatio||e.shiftKey,u={top:0,left:0},d=a.containerElement,p=!0;d[0]!==document&&/static/.test(d.css("position"))&&(u=h),l.left<(a._helper?h.left:0)&&(a.size.width=a.size.width+(a._helper?a.position.left-h.left:a.position.left-u.left),c&&(a.size.height=a.size.width/a.aspectRatio,p=!1),a.position.left=r.helper?h.left:0),l.top<(a._helper?h.top:0)&&(a.size.height=a.size.height+(a._helper?a.position.top-h.top:a.position.top),c&&(a.size.width=a.size.height*a.aspectRatio,p=!1),a.position.top=a._helper?h.top:0),n=a.containerElement.get(0)===a.element.parent().get(0),o=/relative|absolute/.test(a.containerElement.css("position")),n&&o?(a.offset.left=a.parentData.left+a.position.left,a.offset.top=a.parentData.top+a.position.top):(a.offset.left=a.element.offset().left,a.offset.top=a.element.offset().top),i=Math.abs(a.sizeDiff.width+(a._helper?a.offset.left-u.left:a.offset.left-h.left)),s=Math.abs(a.sizeDiff.height+(a._helper?a.offset.top-u.top:a.offset.top-h.top)),i+a.size.width>=a.parentData.width&&(a.size.width=a.parentData.width-i,c&&(a.size.height=a.size.width/a.aspectRatio,p=!1)),s+a.size.height>=a.parentData.height&&(a.size.height=a.parentData.height-s,c&&(a.size.width=a.size.height*a.aspectRatio,p=!1)),p||(a.position.left=a.prevPosition.left,a.position.top=a.prevPosition.top,a.size.width=a.prevSize.width,a.size.height=a.prevSize.height)},stop:function(){var e=t(this).resizable("instance"),i=e.options,s=e.containerOffset,n=e.containerPosition,o=e.containerElement,a=t(e.helper),r=a.offset(),h=a.outerWidth()-e.sizeDiff.width,l=a.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l}),e._helper&&!i.animate&&/static/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).resizable("instance"),i=e.options;t(i.alsoResize).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseFloat(e.width()),height:parseFloat(e.height()),left:parseFloat(e.css("left")),top:parseFloat(e.css("top"))})})},resize:function(e,i){var s=t(this).resizable("instance"),n=s.options,o=s.originalSize,a=s.originalPosition,r={height:s.size.height-o.height||0,width:s.size.width-o.width||0,top:s.position.top-a.top||0,left:s.position.left-a.left||0};t(n.alsoResize).each(function(){var e=t(this),s=t(this).data("ui-resizable-alsoresize"),n={},o=e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(s[e]||0)+(r[e]||0);i&&i>=0&&(n[e]=i||null)}),e.css(n)})},stop:function(){t(this).removeData("ui-resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).resizable("instance"),i=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:i.height,width:i.width,margin:0,left:0,top:0}),e._addClass(e.ghost,"ui-resizable-ghost"),t.uiBackCompat!==!1&&"string"==typeof e.options.ghost&&e.ghost.addClass(this.options.ghost),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).resizable("instance");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).resizable("instance");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e,i=t(this).resizable("instance"),s=i.options,n=i.size,o=i.originalSize,a=i.originalPosition,r=i.axis,h="number"==typeof s.grid?[s.grid,s.grid]:s.grid,l=h[0]||1,c=h[1]||1,u=Math.round((n.width-o.width)/l)*l,d=Math.round((n.height-o.height)/c)*c,p=o.width+u,f=o.height+d,m=s.maxWidth&&p>s.maxWidth,g=s.maxHeight&&f>s.maxHeight,_=s.minWidth&&s.minWidth>p,v=s.minHeight&&s.minHeight>f;s.grid=h,_&&(p+=l),v&&(f+=c),m&&(p-=l),g&&(f-=c),/^(se|s|e)$/.test(r)?(i.size.width=p,i.size.height=f):/^(ne)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.top=a.top-d):/^(sw)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.left=a.left-u):((0>=f-c||0>=p-l)&&(e=i._getPaddingPlusBorderDimensions(this)),f-c>0?(i.size.height=f,i.position.top=a.top-d):(f=c-e.height,i.size.height=f,i.position.top=a.top+o.height-f),p-l>0?(i.size.width=p,i.position.left=a.left-u):(p=l-e.width,i.size.width=p,i.position.left=a.left+o.width-p))}}),t.ui.resizable});/** + * Copyright (c) 2007 Ariel Flesler - aflesler ○ gmail • com | https://github.com/flesler + * Licensed under MIT + * @author Ariel Flesler + * @version 2.1.2 + */ +;(function(f){"use strict";"function"===typeof define&&define.amd?define(["jquery"],f):"undefined"!==typeof module&&module.exports?module.exports=f(require("jquery")):f(jQuery)})(function($){"use strict";function n(a){return!a.nodeName||-1!==$.inArray(a.nodeName.toLowerCase(),["iframe","#document","html","body"])}function h(a){return $.isFunction(a)||$.isPlainObject(a)?a:{top:a,left:a}}var p=$.scrollTo=function(a,d,b){return $(window).scrollTo(a,d,b)};p.defaults={axis:"xy",duration:0,limit:!0};$.fn.scrollTo=function(a,d,b){"object"=== typeof d&&(b=d,d=0);"function"===typeof b&&(b={onAfter:b});"max"===a&&(a=9E9);b=$.extend({},p.defaults,b);d=d||b.duration;var u=b.queue&&1=f[g]?0:Math.min(f[g],n));!a&&1-1){targetElements.on(evt+EVENT_NAMESPACE,function elementToggle(event){$.powerTip.toggle(this,event)})}else{targetElements.on(evt+EVENT_NAMESPACE,function elementOpen(event){$.powerTip.show(this,event)})}});$.each(options.closeEvents,function(idx,evt){if($.inArray(evt,options.openEvents)<0){targetElements.on(evt+EVENT_NAMESPACE,function elementClose(event){$.powerTip.hide(this,!isMouseEvent(event))})}});targetElements.on("keydown"+EVENT_NAMESPACE,function elementKeyDown(event){if(event.keyCode===27){$.powerTip.hide(this,true)}})}return targetElements};$.fn.powerTip.defaults={fadeInTime:200,fadeOutTime:100,followMouse:false,popupId:"powerTip",popupClass:null,intentSensitivity:7,intentPollInterval:100,closeDelay:100,placement:"n",smartPlacement:false,offset:10,mouseOnToPopup:false,manual:false,openEvents:["mouseenter","focus"],closeEvents:["mouseleave","blur"]};$.fn.powerTip.smartPlacementLists={n:["n","ne","nw","s"],e:["e","ne","se","w","nw","sw","n","s","e"],s:["s","se","sw","n"],w:["w","nw","sw","e","ne","se","n","s","w"],nw:["nw","w","sw","n","s","se","nw"],ne:["ne","e","se","n","s","sw","ne"],sw:["sw","w","nw","s","n","ne","sw"],se:["se","e","ne","s","n","nw","se"],"nw-alt":["nw-alt","n","ne-alt","sw-alt","s","se-alt","w","e"],"ne-alt":["ne-alt","n","nw-alt","se-alt","s","sw-alt","e","w"],"sw-alt":["sw-alt","s","se-alt","nw-alt","n","ne-alt","w","e"],"se-alt":["se-alt","s","sw-alt","ne-alt","n","nw-alt","e","w"]};$.powerTip={show:function apiShowTip(element,event){if(isMouseEvent(event)){trackMouse(event);session.previousX=event.pageX;session.previousY=event.pageY;$(element).data(DATA_DISPLAYCONTROLLER).show()}else{$(element).first().data(DATA_DISPLAYCONTROLLER).show(true,true)}return element},reposition:function apiResetPosition(element){$(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition();return element},hide:function apiCloseTip(element,immediate){var displayController;immediate=element?immediate:true;if(element){displayController=$(element).first().data(DATA_DISPLAYCONTROLLER)}else if(session.activeHover){displayController=session.activeHover.data(DATA_DISPLAYCONTROLLER)}if(displayController){displayController.hide(immediate)}return element},toggle:function apiToggle(element,event){if(session.activeHover&&session.activeHover.is(element)){$.powerTip.hide(element,!isMouseEvent(event))}else{$.powerTip.show(element,event)}return element}};$.powerTip.showTip=$.powerTip.show;$.powerTip.closeTip=$.powerTip.hide;function CSSCoordinates(){var me=this;me.top="auto";me.left="auto";me.right="auto";me.bottom="auto";me.set=function(property,value){if($.isNumeric(value)){me[property]=Math.round(value)}}}function DisplayController(element,options,tipController){var hoverTimer=null,myCloseDelay=null;function openTooltip(immediate,forceOpen){cancelTimer();if(!element.data(DATA_HASACTIVEHOVER)){if(!immediate){session.tipOpenImminent=true;hoverTimer=setTimeout(function intentDelay(){hoverTimer=null;checkForIntent()},options.intentPollInterval)}else{if(forceOpen){element.data(DATA_FORCEDOPEN,true)}closeAnyDelayed();tipController.showTip(element)}}else{cancelClose()}}function closeTooltip(disableDelay){if(myCloseDelay){myCloseDelay=session.closeDelayTimeout=clearTimeout(myCloseDelay);session.delayInProgress=false}cancelTimer();session.tipOpenImminent=false;if(element.data(DATA_HASACTIVEHOVER)){element.data(DATA_FORCEDOPEN,false);if(!disableDelay){session.delayInProgress=true;session.closeDelayTimeout=setTimeout(function closeDelay(){session.closeDelayTimeout=null;tipController.hideTip(element);session.delayInProgress=false;myCloseDelay=null},options.closeDelay);myCloseDelay=session.closeDelayTimeout}else{tipController.hideTip(element)}}}function checkForIntent(){var xDifference=Math.abs(session.previousX-session.currentX),yDifference=Math.abs(session.previousY-session.currentY),totalDifference=xDifference+yDifference;if(totalDifference",{id:options.popupId});if($body.length===0){$body=$("body")}$body.append(tipElement);session.tooltips=session.tooltips?session.tooltips.add(tipElement):tipElement}if(options.followMouse){if(!tipElement.data(DATA_HASMOUSEMOVE)){$document.on("mousemove"+EVENT_NAMESPACE,positionTipOnCursor);$window.on("scroll"+EVENT_NAMESPACE,positionTipOnCursor);tipElement.data(DATA_HASMOUSEMOVE,true)}}function beginShowTip(element){element.data(DATA_HASACTIVEHOVER,true);tipElement.queue(function queueTipInit(next){showTip(element);next()})}function showTip(element){var tipContent;if(!element.data(DATA_HASACTIVEHOVER)){return}if(session.isTipOpen){if(!session.isClosing){hideTip(session.activeHover)}tipElement.delay(100).queue(function queueTipAgain(next){showTip(element);next()});return}element.trigger("powerTipPreRender");tipContent=getTooltipContent(element);if(tipContent){tipElement.empty().append(tipContent)}else{return}element.trigger("powerTipRender");session.activeHover=element;session.isTipOpen=true;tipElement.data(DATA_MOUSEONTOTIP,options.mouseOnToPopup);tipElement.addClass(options.popupClass);if(!options.followMouse||element.data(DATA_FORCEDOPEN)){positionTipOnElement(element);session.isFixedTipOpen=true}else{positionTipOnCursor()}if(!element.data(DATA_FORCEDOPEN)&&!options.followMouse){$document.on("click"+EVENT_NAMESPACE,function documentClick(event){var target=event.target;if(target!==element[0]){if(options.mouseOnToPopup){if(target!==tipElement[0]&&!$.contains(tipElement[0],target)){$.powerTip.hide()}}else{$.powerTip.hide()}}})}if(options.mouseOnToPopup&&!options.manual){tipElement.on("mouseenter"+EVENT_NAMESPACE,function tipMouseEnter(){if(session.activeHover){session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel()}});tipElement.on("mouseleave"+EVENT_NAMESPACE,function tipMouseLeave(){if(session.activeHover){session.activeHover.data(DATA_DISPLAYCONTROLLER).hide()}})}tipElement.fadeIn(options.fadeInTime,function fadeInCallback(){if(!session.desyncTimeout){session.desyncTimeout=setInterval(closeDesyncedTip,500)}element.trigger("powerTipOpen")})}function hideTip(element){session.isClosing=true;session.isTipOpen=false;session.desyncTimeout=clearInterval(session.desyncTimeout);element.data(DATA_HASACTIVEHOVER,false);element.data(DATA_FORCEDOPEN,false);$document.off("click"+EVENT_NAMESPACE);tipElement.off(EVENT_NAMESPACE);tipElement.fadeOut(options.fadeOutTime,function fadeOutCallback(){var coords=new CSSCoordinates;session.activeHover=null;session.isClosing=false;session.isFixedTipOpen=false;tipElement.removeClass();coords.set("top",session.currentY+options.offset);coords.set("left",session.currentX+options.offset);tipElement.css(coords);element.trigger("powerTipClose")})}function positionTipOnCursor(){var tipWidth,tipHeight,coords,collisions,collisionCount;if(!session.isFixedTipOpen&&(session.isTipOpen||session.tipOpenImminent&&tipElement.data(DATA_HASMOUSEMOVE))){tipWidth=tipElement.outerWidth();tipHeight=tipElement.outerHeight();coords=new CSSCoordinates;coords.set("top",session.currentY+options.offset);coords.set("left",session.currentX+options.offset);collisions=getViewportCollisions(coords,tipWidth,tipHeight);if(collisions!==Collision.none){collisionCount=countFlags(collisions);if(collisionCount===1){if(collisions===Collision.right){coords.set("left",session.scrollLeft+session.windowWidth-tipWidth)}else if(collisions===Collision.bottom){coords.set("top",session.scrollTop+session.windowHeight-tipHeight)}}else{coords.set("left",session.currentX-tipWidth-options.offset);coords.set("top",session.currentY-tipHeight-options.offset)}}tipElement.css(coords)}}function positionTipOnElement(element){var priorityList,finalPlacement;if(options.smartPlacement||options.followMouse&&element.data(DATA_FORCEDOPEN)){priorityList=$.fn.powerTip.smartPlacementLists[options.placement];$.each(priorityList,function(idx,pos){var collisions=getViewportCollisions(placeTooltip(element,pos),tipElement.outerWidth(),tipElement.outerHeight());finalPlacement=pos;return collisions!==Collision.none})}else{placeTooltip(element,options.placement);finalPlacement=options.placement}tipElement.removeClass("w nw sw e ne se n s w se-alt sw-alt ne-alt nw-alt");tipElement.addClass(finalPlacement)}function placeTooltip(element,placement){var iterationCount=0,tipWidth,tipHeight,coords=new CSSCoordinates;coords.set("top",0);coords.set("left",0);tipElement.css(coords);do{tipWidth=tipElement.outerWidth();tipHeight=tipElement.outerHeight();coords=placementCalculator.compute(element,placement,tipWidth,tipHeight,options.offset);tipElement.css(coords)}while(++iterationCount<=5&&(tipWidth!==tipElement.outerWidth()||tipHeight!==tipElement.outerHeight()));return coords}function closeDesyncedTip(){var isDesynced=false,hasDesyncableCloseEvent=$.grep(["mouseleave","mouseout","blur","focusout"],function(eventType){return $.inArray(eventType,options.closeEvents)!==-1}).length>0;if(session.isTipOpen&&!session.isClosing&&!session.delayInProgress&&hasDesyncableCloseEvent){if(session.activeHover.data(DATA_HASACTIVEHOVER)===false||session.activeHover.is(":disabled")){isDesynced=true}else if(!isMouseOver(session.activeHover)&&!session.activeHover.is(":focus")&&!session.activeHover.data(DATA_FORCEDOPEN)){if(tipElement.data(DATA_MOUSEONTOTIP)){if(!isMouseOver(tipElement)){isDesynced=true}}else{isDesynced=true}}if(isDesynced){hideTip(session.activeHover)}}}this.showTip=beginShowTip;this.hideTip=hideTip;this.resetPosition=positionTipOnElement}function isSvgElement(element){return Boolean(window.SVGElement&&element[0]instanceof SVGElement)}function isMouseEvent(event){return Boolean(event&&$.inArray(event.type,MOUSE_EVENTS)>-1&&typeof event.pageX==="number")}function initTracking(){if(!session.mouseTrackingActive){session.mouseTrackingActive=true;getViewportDimensions();$(getViewportDimensions);$document.on("mousemove"+EVENT_NAMESPACE,trackMouse);$window.on("resize"+EVENT_NAMESPACE,trackResize);$window.on("scroll"+EVENT_NAMESPACE,trackScroll)}}function getViewportDimensions(){session.scrollLeft=$window.scrollLeft();session.scrollTop=$window.scrollTop();session.windowWidth=$window.width();session.windowHeight=$window.height()}function trackResize(){session.windowWidth=$window.width();session.windowHeight=$window.height()}function trackScroll(){var x=$window.scrollLeft(),y=$window.scrollTop();if(x!==session.scrollLeft){session.currentX+=x-session.scrollLeft;session.scrollLeft=x}if(y!==session.scrollTop){session.currentY+=y-session.scrollTop;session.scrollTop=y}}function trackMouse(event){session.currentX=event.pageX;session.currentY=event.pageY}function isMouseOver(element){var elementPosition=element.offset(),elementBox=element[0].getBoundingClientRect(),elementWidth=elementBox.right-elementBox.left,elementHeight=elementBox.bottom-elementBox.top;return session.currentX>=elementPosition.left&&session.currentX<=elementPosition.left+elementWidth&&session.currentY>=elementPosition.top&&session.currentY<=elementPosition.top+elementHeight}function getTooltipContent(element){var tipText=element.data(DATA_POWERTIP),tipObject=element.data(DATA_POWERTIPJQ),tipTarget=element.data(DATA_POWERTIPTARGET),targetElement,content;if(tipText){if($.isFunction(tipText)){tipText=tipText.call(element[0])}content=tipText}else if(tipObject){if($.isFunction(tipObject)){tipObject=tipObject.call(element[0])}if(tipObject.length>0){content=tipObject.clone(true,true)}}else if(tipTarget){targetElement=$("#"+tipTarget);if(targetElement.length>0){content=targetElement.html()}}return content}function getViewportCollisions(coords,elementWidth,elementHeight){var viewportTop=session.scrollTop,viewportLeft=session.scrollLeft,viewportBottom=viewportTop+session.windowHeight,viewportRight=viewportLeft+session.windowWidth,collisions=Collision.none;if(coords.topviewportBottom||Math.abs(coords.bottom-session.windowHeight)>viewportBottom){collisions|=Collision.bottom}if(coords.leftviewportRight){collisions|=Collision.left}if(coords.left+elementWidth>viewportRight||coords.right1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);/*! SmartMenus jQuery Plugin - v1.1.0 - September 17, 2017 + * http://www.smartmenus.org/ + * Copyright Vasil Dinkov, Vadikom Web Ltd. http://vadikom.com; Licensed MIT */(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof module&&"object"==typeof module.exports?module.exports=t(require("jquery")):t(jQuery)})(function($){function initMouseDetection(t){var e=".smartmenus_mouse";if(mouseDetectionEnabled||t)mouseDetectionEnabled&&t&&($(document).off(e),mouseDetectionEnabled=!1);else{var i=!0,s=null,o={mousemove:function(t){var e={x:t.pageX,y:t.pageY,timeStamp:(new Date).getTime()};if(s){var o=Math.abs(s.x-e.x),a=Math.abs(s.y-e.y);if((o>0||a>0)&&2>=o&&2>=a&&300>=e.timeStamp-s.timeStamp&&(mouse=!0,i)){var n=$(t.target).closest("a");n.is("a")&&$.each(menuTrees,function(){return $.contains(this.$root[0],n[0])?(this.itemEnter({currentTarget:n[0]}),!1):void 0}),i=!1}}s=e}};o[touchEvents?"touchstart":"pointerover pointermove pointerout MSPointerOver MSPointerMove MSPointerOut"]=function(t){isTouchEvent(t.originalEvent)&&(mouse=!1)},$(document).on(getEventsNS(o,e)),mouseDetectionEnabled=!0}}function isTouchEvent(t){return!/^(4|mouse)$/.test(t.pointerType)}function getEventsNS(t,e){e||(e="");var i={};for(var s in t)i[s.split(" ").join(e+" ")+e]=t[s];return i}var menuTrees=[],mouse=!1,touchEvents="ontouchstart"in window,mouseDetectionEnabled=!1,requestAnimationFrame=window.requestAnimationFrame||function(t){return setTimeout(t,1e3/60)},cancelAnimationFrame=window.cancelAnimationFrame||function(t){clearTimeout(t)},canAnimate=!!$.fn.animate;return $.SmartMenus=function(t,e){this.$root=$(t),this.opts=e,this.rootId="",this.accessIdPrefix="",this.$subArrow=null,this.activatedItems=[],this.visibleSubMenus=[],this.showTimeout=0,this.hideTimeout=0,this.scrollTimeout=0,this.clickActivated=!1,this.focusActivated=!1,this.zIndexInc=0,this.idInc=0,this.$firstLink=null,this.$firstSub=null,this.disabled=!1,this.$disableOverlay=null,this.$touchScrollingSub=null,this.cssTransforms3d="perspective"in t.style||"webkitPerspective"in t.style,this.wasCollapsible=!1,this.init()},$.extend($.SmartMenus,{hideAll:function(){$.each(menuTrees,function(){this.menuHideAll()})},destroy:function(){for(;menuTrees.length;)menuTrees[0].destroy();initMouseDetection(!0)},prototype:{init:function(t){var e=this;if(!t){menuTrees.push(this),this.rootId=((new Date).getTime()+Math.random()+"").replace(/\D/g,""),this.accessIdPrefix="sm-"+this.rootId+"-",this.$root.hasClass("sm-rtl")&&(this.opts.rightToLeftSubMenus=!0);var i=".smartmenus";this.$root.data("smartmenus",this).attr("data-smartmenus-id",this.rootId).dataSM("level",1).on(getEventsNS({"mouseover focusin":$.proxy(this.rootOver,this),"mouseout focusout":$.proxy(this.rootOut,this),keydown:$.proxy(this.rootKeyDown,this)},i)).on(getEventsNS({mouseenter:$.proxy(this.itemEnter,this),mouseleave:$.proxy(this.itemLeave,this),mousedown:$.proxy(this.itemDown,this),focus:$.proxy(this.itemFocus,this),blur:$.proxy(this.itemBlur,this),click:$.proxy(this.itemClick,this)},i),"a"),i+=this.rootId,this.opts.hideOnClick&&$(document).on(getEventsNS({touchstart:$.proxy(this.docTouchStart,this),touchmove:$.proxy(this.docTouchMove,this),touchend:$.proxy(this.docTouchEnd,this),click:$.proxy(this.docClick,this)},i)),$(window).on(getEventsNS({"resize orientationchange":$.proxy(this.winResize,this)},i)),this.opts.subIndicators&&(this.$subArrow=$("").addClass("sub-arrow"),this.opts.subIndicatorsText&&this.$subArrow.html(this.opts.subIndicatorsText)),initMouseDetection()}if(this.$firstSub=this.$root.find("ul").each(function(){e.menuInit($(this))}).eq(0),this.$firstLink=this.$root.find("a").eq(0),this.opts.markCurrentItem){var s=/(index|default)\.[^#\?\/]*/i,o=/#.*/,a=window.location.href.replace(s,""),n=a.replace(o,"");this.$root.find("a").each(function(){var t=this.href.replace(s,""),i=$(this);(t==a||t==n)&&(i.addClass("current"),e.opts.markCurrentTree&&i.parentsUntil("[data-smartmenus-id]","ul").each(function(){$(this).dataSM("parent-a").addClass("current")}))})}this.wasCollapsible=this.isCollapsible()},destroy:function(t){if(!t){var e=".smartmenus";this.$root.removeData("smartmenus").removeAttr("data-smartmenus-id").removeDataSM("level").off(e),e+=this.rootId,$(document).off(e),$(window).off(e),this.opts.subIndicators&&(this.$subArrow=null)}this.menuHideAll();var i=this;this.$root.find("ul").each(function(){var t=$(this);t.dataSM("scroll-arrows")&&t.dataSM("scroll-arrows").remove(),t.dataSM("shown-before")&&((i.opts.subMenusMinWidth||i.opts.subMenusMaxWidth)&&t.css({width:"",minWidth:"",maxWidth:""}).removeClass("sm-nowrap"),t.dataSM("scroll-arrows")&&t.dataSM("scroll-arrows").remove(),t.css({zIndex:"",top:"",left:"",marginLeft:"",marginTop:"",display:""})),0==(t.attr("id")||"").indexOf(i.accessIdPrefix)&&t.removeAttr("id")}).removeDataSM("in-mega").removeDataSM("shown-before").removeDataSM("scroll-arrows").removeDataSM("parent-a").removeDataSM("level").removeDataSM("beforefirstshowfired").removeAttr("role").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeAttr("aria-expanded"),this.$root.find("a.has-submenu").each(function(){var t=$(this);0==t.attr("id").indexOf(i.accessIdPrefix)&&t.removeAttr("id")}).removeClass("has-submenu").removeDataSM("sub").removeAttr("aria-haspopup").removeAttr("aria-controls").removeAttr("aria-expanded").closest("li").removeDataSM("sub"),this.opts.subIndicators&&this.$root.find("span.sub-arrow").remove(),this.opts.markCurrentItem&&this.$root.find("a.current").removeClass("current"),t||(this.$root=null,this.$firstLink=null,this.$firstSub=null,this.$disableOverlay&&(this.$disableOverlay.remove(),this.$disableOverlay=null),menuTrees.splice($.inArray(this,menuTrees),1))},disable:function(t){if(!this.disabled){if(this.menuHideAll(),!t&&!this.opts.isPopup&&this.$root.is(":visible")){var e=this.$root.offset();this.$disableOverlay=$('
').css({position:"absolute",top:e.top,left:e.left,width:this.$root.outerWidth(),height:this.$root.outerHeight(),zIndex:this.getStartZIndex(!0),opacity:0}).appendTo(document.body)}this.disabled=!0}},docClick:function(t){return this.$touchScrollingSub?(this.$touchScrollingSub=null,void 0):((this.visibleSubMenus.length&&!$.contains(this.$root[0],t.target)||$(t.target).closest("a").length)&&this.menuHideAll(),void 0)},docTouchEnd:function(){if(this.lastTouch){if(!(!this.visibleSubMenus.length||void 0!==this.lastTouch.x2&&this.lastTouch.x1!=this.lastTouch.x2||void 0!==this.lastTouch.y2&&this.lastTouch.y1!=this.lastTouch.y2||this.lastTouch.target&&$.contains(this.$root[0],this.lastTouch.target))){this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=0);var t=this;this.hideTimeout=setTimeout(function(){t.menuHideAll()},350)}this.lastTouch=null}},docTouchMove:function(t){if(this.lastTouch){var e=t.originalEvent.touches[0];this.lastTouch.x2=e.pageX,this.lastTouch.y2=e.pageY}},docTouchStart:function(t){var e=t.originalEvent.touches[0];this.lastTouch={x1:e.pageX,y1:e.pageY,target:e.target}},enable:function(){this.disabled&&(this.$disableOverlay&&(this.$disableOverlay.remove(),this.$disableOverlay=null),this.disabled=!1)},getClosestMenu:function(t){for(var e=$(t).closest("ul");e.dataSM("in-mega");)e=e.parent().closest("ul");return e[0]||null},getHeight:function(t){return this.getOffset(t,!0)},getOffset:function(t,e){var i;"none"==t.css("display")&&(i={position:t[0].style.position,visibility:t[0].style.visibility},t.css({position:"absolute",visibility:"hidden"}).show());var s=t[0].getBoundingClientRect&&t[0].getBoundingClientRect(),o=s&&(e?s.height||s.bottom-s.top:s.width||s.right-s.left);return o||0===o||(o=e?t[0].offsetHeight:t[0].offsetWidth),i&&t.hide().css(i),o},getStartZIndex:function(t){var e=parseInt(this[t?"$root":"$firstSub"].css("z-index"));return!t&&isNaN(e)&&(e=parseInt(this.$root.css("z-index"))),isNaN(e)?1:e},getTouchPoint:function(t){return t.touches&&t.touches[0]||t.changedTouches&&t.changedTouches[0]||t},getViewport:function(t){var e=t?"Height":"Width",i=document.documentElement["client"+e],s=window["inner"+e];return s&&(i=Math.min(i,s)),i},getViewportHeight:function(){return this.getViewport(!0)},getViewportWidth:function(){return this.getViewport()},getWidth:function(t){return this.getOffset(t)},handleEvents:function(){return!this.disabled&&this.isCSSOn()},handleItemEvents:function(t){return this.handleEvents()&&!this.isLinkInMegaMenu(t)},isCollapsible:function(){return"static"==this.$firstSub.css("position")},isCSSOn:function(){return"inline"!=this.$firstLink.css("display")},isFixed:function(){var t="fixed"==this.$root.css("position");return t||this.$root.parentsUntil("body").each(function(){return"fixed"==$(this).css("position")?(t=!0,!1):void 0}),t},isLinkInMegaMenu:function(t){return $(this.getClosestMenu(t[0])).hasClass("mega-menu")},isTouchMode:function(){return!mouse||this.opts.noMouseOver||this.isCollapsible()},itemActivate:function(t,e){var i=t.closest("ul"),s=i.dataSM("level");if(s>1&&(!this.activatedItems[s-2]||this.activatedItems[s-2][0]!=i.dataSM("parent-a")[0])){var o=this;$(i.parentsUntil("[data-smartmenus-id]","ul").get().reverse()).add(i).each(function(){o.itemActivate($(this).dataSM("parent-a"))})}if((!this.isCollapsible()||e)&&this.menuHideSubMenus(this.activatedItems[s-1]&&this.activatedItems[s-1][0]==t[0]?s:s-1),this.activatedItems[s-1]=t,this.$root.triggerHandler("activate.smapi",t[0])!==!1){var a=t.dataSM("sub");a&&(this.isTouchMode()||!this.opts.showOnClick||this.clickActivated)&&this.menuShow(a)}},itemBlur:function(t){var e=$(t.currentTarget);this.handleItemEvents(e)&&this.$root.triggerHandler("blur.smapi",e[0])},itemClick:function(t){var e=$(t.currentTarget);if(this.handleItemEvents(e)){if(this.$touchScrollingSub&&this.$touchScrollingSub[0]==e.closest("ul")[0])return this.$touchScrollingSub=null,t.stopPropagation(),!1;if(this.$root.triggerHandler("click.smapi",e[0])===!1)return!1;var i=$(t.target).is(".sub-arrow"),s=e.dataSM("sub"),o=s?2==s.dataSM("level"):!1,a=this.isCollapsible(),n=/toggle$/.test(this.opts.collapsibleBehavior),r=/link$/.test(this.opts.collapsibleBehavior),h=/^accordion/.test(this.opts.collapsibleBehavior);if(s&&!s.is(":visible")){if((!r||!a||i)&&(this.opts.showOnClick&&o&&(this.clickActivated=!0),this.itemActivate(e,h),s.is(":visible")))return this.focusActivated=!0,!1}else if(a&&(n||i))return this.itemActivate(e,h),this.menuHide(s),n&&(this.focusActivated=!1),!1;return this.opts.showOnClick&&o||e.hasClass("disabled")||this.$root.triggerHandler("select.smapi",e[0])===!1?!1:void 0}},itemDown:function(t){var e=$(t.currentTarget);this.handleItemEvents(e)&&e.dataSM("mousedown",!0)},itemEnter:function(t){var e=$(t.currentTarget);if(this.handleItemEvents(e)){if(!this.isTouchMode()){this.showTimeout&&(clearTimeout(this.showTimeout),this.showTimeout=0);var i=this;this.showTimeout=setTimeout(function(){i.itemActivate(e)},this.opts.showOnClick&&1==e.closest("ul").dataSM("level")?1:this.opts.showTimeout)}this.$root.triggerHandler("mouseenter.smapi",e[0])}},itemFocus:function(t){var e=$(t.currentTarget);this.handleItemEvents(e)&&(!this.focusActivated||this.isTouchMode()&&e.dataSM("mousedown")||this.activatedItems.length&&this.activatedItems[this.activatedItems.length-1][0]==e[0]||this.itemActivate(e,!0),this.$root.triggerHandler("focus.smapi",e[0]))},itemLeave:function(t){var e=$(t.currentTarget);this.handleItemEvents(e)&&(this.isTouchMode()||(e[0].blur(),this.showTimeout&&(clearTimeout(this.showTimeout),this.showTimeout=0)),e.removeDataSM("mousedown"),this.$root.triggerHandler("mouseleave.smapi",e[0]))},menuHide:function(t){if(this.$root.triggerHandler("beforehide.smapi",t[0])!==!1&&(canAnimate&&t.stop(!0,!0),"none"!=t.css("display"))){var e=function(){t.css("z-index","")};this.isCollapsible()?canAnimate&&this.opts.collapsibleHideFunction?this.opts.collapsibleHideFunction.call(this,t,e):t.hide(this.opts.collapsibleHideDuration,e):canAnimate&&this.opts.hideFunction?this.opts.hideFunction.call(this,t,e):t.hide(this.opts.hideDuration,e),t.dataSM("scroll")&&(this.menuScrollStop(t),t.css({"touch-action":"","-ms-touch-action":"","-webkit-transform":"",transform:""}).off(".smartmenus_scroll").removeDataSM("scroll").dataSM("scroll-arrows").hide()),t.dataSM("parent-a").removeClass("highlighted").attr("aria-expanded","false"),t.attr({"aria-expanded":"false","aria-hidden":"true"});var i=t.dataSM("level");this.activatedItems.splice(i-1,1),this.visibleSubMenus.splice($.inArray(t,this.visibleSubMenus),1),this.$root.triggerHandler("hide.smapi",t[0])}},menuHideAll:function(){this.showTimeout&&(clearTimeout(this.showTimeout),this.showTimeout=0);for(var t=this.opts.isPopup?1:0,e=this.visibleSubMenus.length-1;e>=t;e--)this.menuHide(this.visibleSubMenus[e]);this.opts.isPopup&&(canAnimate&&this.$root.stop(!0,!0),this.$root.is(":visible")&&(canAnimate&&this.opts.hideFunction?this.opts.hideFunction.call(this,this.$root):this.$root.hide(this.opts.hideDuration))),this.activatedItems=[],this.visibleSubMenus=[],this.clickActivated=!1,this.focusActivated=!1,this.zIndexInc=0,this.$root.triggerHandler("hideAll.smapi")},menuHideSubMenus:function(t){for(var e=this.activatedItems.length-1;e>=t;e--){var i=this.activatedItems[e].dataSM("sub");i&&this.menuHide(i)}},menuInit:function(t){if(!t.dataSM("in-mega")){t.hasClass("mega-menu")&&t.find("ul").dataSM("in-mega",!0);for(var e=2,i=t[0];(i=i.parentNode.parentNode)!=this.$root[0];)e++;var s=t.prevAll("a").eq(-1);s.length||(s=t.prevAll().find("a").eq(-1)),s.addClass("has-submenu").dataSM("sub",t),t.dataSM("parent-a",s).dataSM("level",e).parent().dataSM("sub",t);var o=s.attr("id")||this.accessIdPrefix+ ++this.idInc,a=t.attr("id")||this.accessIdPrefix+ ++this.idInc;s.attr({id:o,"aria-haspopup":"true","aria-controls":a,"aria-expanded":"false"}),t.attr({id:a,role:"group","aria-hidden":"true","aria-labelledby":o,"aria-expanded":"false"}),this.opts.subIndicators&&s[this.opts.subIndicatorsPos](this.$subArrow.clone())}},menuPosition:function(t){var e,i,s=t.dataSM("parent-a"),o=s.closest("li"),a=o.parent(),n=t.dataSM("level"),r=this.getWidth(t),h=this.getHeight(t),u=s.offset(),l=u.left,c=u.top,d=this.getWidth(s),m=this.getHeight(s),p=$(window),f=p.scrollLeft(),v=p.scrollTop(),b=this.getViewportWidth(),S=this.getViewportHeight(),g=a.parent().is("[data-sm-horizontal-sub]")||2==n&&!a.hasClass("sm-vertical"),M=this.opts.rightToLeftSubMenus&&!o.is("[data-sm-reverse]")||!this.opts.rightToLeftSubMenus&&o.is("[data-sm-reverse]"),w=2==n?this.opts.mainMenuSubOffsetX:this.opts.subMenusSubOffsetX,T=2==n?this.opts.mainMenuSubOffsetY:this.opts.subMenusSubOffsetY;if(g?(e=M?d-r-w:w,i=this.opts.bottomToTopSubMenus?-h-T:m+T):(e=M?w-r:d-w,i=this.opts.bottomToTopSubMenus?m-T-h:T),this.opts.keepInViewport){var y=l+e,I=c+i;if(M&&f>y?e=g?f-y+e:d-w:!M&&y+r>f+b&&(e=g?f+b-r-y+e:w-r),g||(S>h&&I+h>v+S?i+=v+S-h-I:(h>=S||v>I)&&(i+=v-I)),g&&(I+h>v+S+.49||v>I)||!g&&h>S+.49){var x=this;t.dataSM("scroll-arrows")||t.dataSM("scroll-arrows",$([$('')[0],$('')[0]]).on({mouseenter:function(){t.dataSM("scroll").up=$(this).hasClass("scroll-up"),x.menuScroll(t)},mouseleave:function(e){x.menuScrollStop(t),x.menuScrollOut(t,e)},"mousewheel DOMMouseScroll":function(t){t.preventDefault()}}).insertAfter(t));var A=".smartmenus_scroll";if(t.dataSM("scroll",{y:this.cssTransforms3d?0:i-m,step:1,itemH:m,subH:h,arrowDownH:this.getHeight(t.dataSM("scroll-arrows").eq(1))}).on(getEventsNS({mouseover:function(e){x.menuScrollOver(t,e)},mouseout:function(e){x.menuScrollOut(t,e)},"mousewheel DOMMouseScroll":function(e){x.menuScrollMousewheel(t,e)}},A)).dataSM("scroll-arrows").css({top:"auto",left:"0",marginLeft:e+(parseInt(t.css("border-left-width"))||0),width:r-(parseInt(t.css("border-left-width"))||0)-(parseInt(t.css("border-right-width"))||0),zIndex:t.css("z-index")}).eq(g&&this.opts.bottomToTopSubMenus?0:1).show(),this.isFixed()){var C={};C[touchEvents?"touchstart touchmove touchend":"pointerdown pointermove pointerup MSPointerDown MSPointerMove MSPointerUp"]=function(e){x.menuScrollTouch(t,e)},t.css({"touch-action":"none","-ms-touch-action":"none"}).on(getEventsNS(C,A))}}}t.css({top:"auto",left:"0",marginLeft:e,marginTop:i-m})},menuScroll:function(t,e,i){var s,o=t.dataSM("scroll"),a=t.dataSM("scroll-arrows"),n=o.up?o.upEnd:o.downEnd;if(!e&&o.momentum){if(o.momentum*=.92,s=o.momentum,.5>s)return this.menuScrollStop(t),void 0}else s=i||(e||!this.opts.scrollAccelerate?this.opts.scrollStep:Math.floor(o.step));var r=t.dataSM("level");if(this.activatedItems[r-1]&&this.activatedItems[r-1].dataSM("sub")&&this.activatedItems[r-1].dataSM("sub").is(":visible")&&this.menuHideSubMenus(r-1),o.y=o.up&&o.y>=n||!o.up&&n>=o.y?o.y:Math.abs(n-o.y)>s?o.y+(o.up?s:-s):n,t.css(this.cssTransforms3d?{"-webkit-transform":"translate3d(0, "+o.y+"px, 0)",transform:"translate3d(0, "+o.y+"px, 0)"}:{marginTop:o.y}),mouse&&(o.up&&o.y>o.downEnd||!o.up&&o.y0;t.dataSM("scroll-arrows").eq(i?0:1).is(":visible")&&(t.dataSM("scroll").up=i,this.menuScroll(t,!0))}e.preventDefault()},menuScrollOut:function(t,e){mouse&&(/^scroll-(up|down)/.test((e.relatedTarget||"").className)||(t[0]==e.relatedTarget||$.contains(t[0],e.relatedTarget))&&this.getClosestMenu(e.relatedTarget)==t[0]||t.dataSM("scroll-arrows").css("visibility","hidden"))},menuScrollOver:function(t,e){if(mouse&&!/^scroll-(up|down)/.test(e.target.className)&&this.getClosestMenu(e.target)==t[0]){this.menuScrollRefreshData(t);var i=t.dataSM("scroll"),s=$(window).scrollTop()-t.dataSM("parent-a").offset().top-i.itemH;t.dataSM("scroll-arrows").eq(0).css("margin-top",s).end().eq(1).css("margin-top",s+this.getViewportHeight()-i.arrowDownH).end().css("visibility","visible")}},menuScrollRefreshData:function(t){var e=t.dataSM("scroll"),i=$(window).scrollTop()-t.dataSM("parent-a").offset().top-e.itemH;this.cssTransforms3d&&(i=-(parseFloat(t.css("margin-top"))-i)),$.extend(e,{upEnd:i,downEnd:i+this.getViewportHeight()-e.subH})},menuScrollStop:function(t){return this.scrollTimeout?(cancelAnimationFrame(this.scrollTimeout),this.scrollTimeout=0,t.dataSM("scroll").step=1,!0):void 0},menuScrollTouch:function(t,e){if(e=e.originalEvent,isTouchEvent(e)){var i=this.getTouchPoint(e);if(this.getClosestMenu(i.target)==t[0]){var s=t.dataSM("scroll");if(/(start|down)$/i.test(e.type))this.menuScrollStop(t)?(e.preventDefault(),this.$touchScrollingSub=t):this.$touchScrollingSub=null,this.menuScrollRefreshData(t),$.extend(s,{touchStartY:i.pageY,touchStartTime:e.timeStamp});else if(/move$/i.test(e.type)){var o=void 0!==s.touchY?s.touchY:s.touchStartY;if(void 0!==o&&o!=i.pageY){this.$touchScrollingSub=t;var a=i.pageY>o;void 0!==s.up&&s.up!=a&&$.extend(s,{touchStartY:i.pageY,touchStartTime:e.timeStamp}),$.extend(s,{up:a,touchY:i.pageY}),this.menuScroll(t,!0,Math.abs(i.pageY-o))}e.preventDefault()}else void 0!==s.touchY&&((s.momentum=15*Math.pow(Math.abs(i.pageY-s.touchStartY)/(e.timeStamp-s.touchStartTime),2))&&(this.menuScrollStop(t),this.menuScroll(t),e.preventDefault()),delete s.touchY)}}},menuShow:function(t){if((t.dataSM("beforefirstshowfired")||(t.dataSM("beforefirstshowfired",!0),this.$root.triggerHandler("beforefirstshow.smapi",t[0])!==!1))&&this.$root.triggerHandler("beforeshow.smapi",t[0])!==!1&&(t.dataSM("shown-before",!0),canAnimate&&t.stop(!0,!0),!t.is(":visible"))){var e=t.dataSM("parent-a"),i=this.isCollapsible();if((this.opts.keepHighlighted||i)&&e.addClass("highlighted"),i)t.removeClass("sm-nowrap").css({zIndex:"",width:"auto",minWidth:"",maxWidth:"",top:"",left:"",marginLeft:"",marginTop:""});else{if(t.css("z-index",this.zIndexInc=(this.zIndexInc||this.getStartZIndex())+1),(this.opts.subMenusMinWidth||this.opts.subMenusMaxWidth)&&(t.css({width:"auto",minWidth:"",maxWidth:""}).addClass("sm-nowrap"),this.opts.subMenusMinWidth&&t.css("min-width",this.opts.subMenusMinWidth),this.opts.subMenusMaxWidth)){var s=this.getWidth(t);t.css("max-width",this.opts.subMenusMaxWidth),s>this.getWidth(t)&&t.removeClass("sm-nowrap").css("width",this.opts.subMenusMaxWidth)}this.menuPosition(t)}var o=function(){t.css("overflow","")};i?canAnimate&&this.opts.collapsibleShowFunction?this.opts.collapsibleShowFunction.call(this,t,o):t.show(this.opts.collapsibleShowDuration,o):canAnimate&&this.opts.showFunction?this.opts.showFunction.call(this,t,o):t.show(this.opts.showDuration,o),e.attr("aria-expanded","true"),t.attr({"aria-expanded":"true","aria-hidden":"false"}),this.visibleSubMenus.push(t),this.$root.triggerHandler("show.smapi",t[0])}},popupHide:function(t){this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=0);var e=this;this.hideTimeout=setTimeout(function(){e.menuHideAll()},t?1:this.opts.hideTimeout)},popupShow:function(t,e){if(!this.opts.isPopup)return alert('SmartMenus jQuery Error:\n\nIf you want to show this menu via the "popupShow" method, set the isPopup:true option.'),void 0;if(this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=0),this.$root.dataSM("shown-before",!0),canAnimate&&this.$root.stop(!0,!0),!this.$root.is(":visible")){this.$root.css({left:t,top:e});var i=this,s=function(){i.$root.css("overflow","")};canAnimate&&this.opts.showFunction?this.opts.showFunction.call(this,this.$root,s):this.$root.show(this.opts.showDuration,s),this.visibleSubMenus[0]=this.$root}},refresh:function(){this.destroy(!0),this.init(!0)},rootKeyDown:function(t){if(this.handleEvents())switch(t.keyCode){case 27:var e=this.activatedItems[0];if(e){this.menuHideAll(),e[0].focus();var i=e.dataSM("sub");i&&this.menuHide(i)}break;case 32:var s=$(t.target);if(s.is("a")&&this.handleItemEvents(s)){var i=s.dataSM("sub");i&&!i.is(":visible")&&(this.itemClick({currentTarget:t.target}),t.preventDefault())}}},rootOut:function(t){if(this.handleEvents()&&!this.isTouchMode()&&t.target!=this.$root[0]&&(this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=0),!this.opts.showOnClick||!this.opts.hideOnClick)){var e=this;this.hideTimeout=setTimeout(function(){e.menuHideAll()},this.opts.hideTimeout)}},rootOver:function(t){this.handleEvents()&&!this.isTouchMode()&&t.target!=this.$root[0]&&this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=0)},winResize:function(t){if(this.handleEvents()){if(!("onorientationchange"in window)||"orientationchange"==t.type){var e=this.isCollapsible();this.wasCollapsible&&e||(this.activatedItems.length&&this.activatedItems[this.activatedItems.length-1][0].blur(),this.menuHideAll()),this.wasCollapsible=e}}else if(this.$disableOverlay){var i=this.$root.offset();this.$disableOverlay.css({top:i.top,left:i.left,width:this.$root.outerWidth(),height:this.$root.outerHeight()})}}}}),$.fn.dataSM=function(t,e){return e?this.data(t+"_smartmenus",e):this.data(t+"_smartmenus")},$.fn.removeDataSM=function(t){return this.removeData(t+"_smartmenus")},$.fn.smartmenus=function(options){if("string"==typeof options){var args=arguments,method=options;return Array.prototype.shift.call(args),this.each(function(){var t=$(this).data("smartmenus");t&&t[method]&&t[method].apply(t,args)})}return this.each(function(){var dataOpts=$(this).data("sm-options")||null;if(dataOpts)try{dataOpts=eval("("+dataOpts+")")}catch(e){dataOpts=null,alert('ERROR\n\nSmartMenus jQuery init:\nInvalid "data-sm-options" attribute value syntax.')}new $.SmartMenus(this,$.extend({},$.fn.smartmenus.defaults,options,dataOpts))})},$.fn.smartmenus.defaults={isPopup:!1,mainMenuSubOffsetX:0,mainMenuSubOffsetY:0,subMenusSubOffsetX:0,subMenusSubOffsetY:0,subMenusMinWidth:"10em",subMenusMaxWidth:"20em",subIndicators:!0,subIndicatorsPos:"append",subIndicatorsText:"",scrollStep:30,scrollAccelerate:!0,showTimeout:250,hideTimeout:500,showDuration:0,showFunction:null,hideDuration:0,hideFunction:function(t,e){t.fadeOut(200,e)},collapsibleShowDuration:0,collapsibleShowFunction:function(t,e){t.slideDown(200,e)},collapsibleHideDuration:0,collapsibleHideFunction:function(t,e){t.slideUp(200,e)},showOnClick:!1,hideOnClick:!0,noMouseOver:!1,keepInViewport:!0,keepHighlighted:!0,markCurrentItem:!1,markCurrentTree:!0,rightToLeftSubMenus:!1,bottomToTopSubMenus:!1,collapsibleBehavior:"default"},$}); \ No newline at end of file diff --git a/third-party/mozjpeg/mozjpeg/doc/html/menu.js b/third-party/mozjpeg/mozjpeg/doc/html/menu.js new file mode 100644 index 00000000000..2fe2214f26a --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/menu.js @@ -0,0 +1,51 @@ +/* + @licstart The following is the entire license notice for the JavaScript code in this file. + + The MIT License (MIT) + + Copyright (C) 1997-2020 by Dimitri van Heesch + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, + sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + @licend The above is the entire license notice for the JavaScript code in this file + */ +function initMenu(relPath,searchEnabled,serverSide,searchPage,search) { + function makeTree(data,relPath) { + var result=''; + if ('children' in data) { + result+=''; + } + return result; + } + + $('#main-nav').append(makeTree(menudata,relPath)); + $('#main-nav').children(':first').addClass('sm sm-dox').attr('id','main-menu'); + if (searchEnabled) { + if (serverSide) { + $('#main-menu').append('
  • '); + } else { + $('#main-menu').append('
  • '); + } + } + $('#main-menu').smartmenus(); +} +/* @license-end */ diff --git a/third-party/mozjpeg/mozjpeg/doc/html/menudata.js b/third-party/mozjpeg/mozjpeg/doc/html/menudata.js new file mode 100644 index 00000000000..fa25d425458 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/menudata.js @@ -0,0 +1,33 @@ +/* + @licstart The following is the entire license notice for the JavaScript code in this file. + + The MIT License (MIT) + + Copyright (C) 1997-2020 by Dimitri van Heesch + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, + sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + @licend The above is the entire license notice for the JavaScript code in this file +*/ +var menudata={children:[ +{text:"Main Page",url:"index.html"}, +{text:"Modules",url:"modules.html"}, +{text:"Data Structures",url:"annotated.html",children:[ +{text:"Data Structures",url:"annotated.html"}, +{text:"Data Structure Index",url:"classes.html"}, +{text:"Data Fields",url:"functions.html",children:[ +{text:"All",url:"functions.html"}, +{text:"Variables",url:"functions_vars.html"}]}]}]} diff --git a/third-party/mozjpeg/mozjpeg/doc/html/modules.html b/third-party/mozjpeg/mozjpeg/doc/html/modules.html index e79f2262908..d48980a46bf 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/modules.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/modules.html @@ -1,18 +1,17 @@ - + - + + TurboJPEG: Modules + - @@ -22,9 +21,9 @@ - @@ -32,40 +31,29 @@
    +
    TurboJPEG -  2.0 +  2.1.4
    - + - + + + +
    +
    @@ -81,15 +69,13 @@
    Here is a list of all modules:
    - +
    \TurboJPEGTurboJPEG API
     TurboJPEGTurboJPEG API
    diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_63.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_0.html similarity index 59% rename from third-party/mozjpeg/mozjpeg/doc/html/search/all_63.html rename to third-party/mozjpeg/mozjpeg/doc/html/search/all_0.html index e7f34db5862..a34319f3095 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_63.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_0.html @@ -1,9 +1,9 @@ - + - + - + @@ -11,15 +11,25 @@
    Loading...
    Searching...
    No Matches
    diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_0.js new file mode 100644 index 00000000000..07cc58c7c20 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_0.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['customfilter_0',['customFilter',['../structtjtransform.html#a0dc7697d59a7abe48afc629e96cbc1d2',1,'tjtransform']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_64.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_1.html similarity index 59% rename from third-party/mozjpeg/mozjpeg/doc/html/search/all_64.html rename to third-party/mozjpeg/mozjpeg/doc/html/search/all_1.html index 360601fa722..51aff6f69ca 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_64.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_1.html @@ -1,9 +1,9 @@ - + - + - + @@ -11,15 +11,25 @@
    Loading...
    Searching...
    No Matches
    diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_1.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_1.js new file mode 100644 index 00000000000..73ec5f552f6 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_1.js @@ -0,0 +1,5 @@ +var searchData= +[ + ['data_1',['data',['../structtjtransform.html#a688fe8f1a8ecc12a538d9e561cf338e3',1,'tjtransform']]], + ['denom_2',['denom',['../structtjscalingfactor.html#aefbcdf3e9e62274b2d312c695f133ce3',1,'tjscalingfactor']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_68.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_2.html similarity index 59% rename from third-party/mozjpeg/mozjpeg/doc/html/search/all_68.html rename to third-party/mozjpeg/mozjpeg/doc/html/search/all_2.html index dec41d62efe..1f81f6645ea 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_68.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_2.html @@ -1,9 +1,9 @@ - + - + - + @@ -11,15 +11,25 @@
    Loading...
    Searching...
    No Matches
    diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_2.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_2.js new file mode 100644 index 00000000000..1c5615e3d50 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_2.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['h_3',['h',['../structtjregion.html#aecefc45a26f4d8b60dd4d825c1710115',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6e.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_3.html similarity index 59% rename from third-party/mozjpeg/mozjpeg/doc/html/search/all_6e.html rename to third-party/mozjpeg/mozjpeg/doc/html/search/all_3.html index e0fd7653a04..2e31ab9110e 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6e.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_3.html @@ -1,9 +1,9 @@ - + - + - + @@ -11,15 +11,25 @@
    Loading...
    Searching...
    No Matches
    diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_3.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_3.js new file mode 100644 index 00000000000..77d139a01f2 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_3.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['num_4',['num',['../structtjscalingfactor.html#a9b011e57f981ee23083e2c1aa5e640ec',1,'tjscalingfactor']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_4.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_4.html new file mode 100644 index 00000000000..0540c163366 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_4.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_4.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_4.js new file mode 100644 index 00000000000..f976db4d3de --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_4.js @@ -0,0 +1,5 @@ +var searchData= +[ + ['op_5',['op',['../structtjtransform.html#a2525aab4ba6978a1c273f74fef50e498',1,'tjtransform']]], + ['options_6',['options',['../structtjtransform.html#ac0e74655baa4402209a21e1ae481c8f6',1,'tjtransform']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_5.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_5.html new file mode 100644 index 00000000000..ebec30bfa22 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_5.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_5.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_5.js new file mode 100644 index 00000000000..193b900760e --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_5.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['r_7',['r',['../structtjtransform.html#ac324e5e442abec8a961e5bf219db12cf',1,'tjtransform']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_6.html new file mode 100644 index 00000000000..31cbd052ed6 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_6.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_6.js new file mode 100644 index 00000000000..6b43b306042 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_6.js @@ -0,0 +1,103 @@ +var searchData= +[ + ['tj_5fnumcs_8',['TJ_NUMCS',['../group___turbo_j_p_e_g.html#ga39f57a6fb02d9cf32e7b6890099b5a71',1,'turbojpeg.h']]], + ['tj_5fnumerr_9',['TJ_NUMERR',['../group___turbo_j_p_e_g.html#ga79bde1b4a3e2351e00887e47781b966e',1,'turbojpeg.h']]], + ['tj_5fnumpf_10',['TJ_NUMPF',['../group___turbo_j_p_e_g.html#ga7010a4402f54a45ba822ad8675a4655e',1,'turbojpeg.h']]], + ['tj_5fnumsamp_11',['TJ_NUMSAMP',['../group___turbo_j_p_e_g.html#ga5ef3d169162ce77ce348e292a0b7477c',1,'turbojpeg.h']]], + ['tj_5fnumxop_12',['TJ_NUMXOP',['../group___turbo_j_p_e_g.html#ga0f6dbd18adf38b7d46ac547f0f4d562c',1,'turbojpeg.h']]], + ['tjalloc_13',['tjAlloc',['../group___turbo_j_p_e_g.html#gaec627dd4c5f30b7a775a7aea3bec5d83',1,'turbojpeg.h']]], + ['tjalphaoffset_14',['tjAlphaOffset',['../group___turbo_j_p_e_g.html#ga5af0ab065feefd526debf1e20c43e837',1,'turbojpeg.h']]], + ['tjblueoffset_15',['tjBlueOffset',['../group___turbo_j_p_e_g.html#ga84e2e35d3f08025f976ec1ec53693dea',1,'turbojpeg.h']]], + ['tjbufsize_16',['tjBufSize',['../group___turbo_j_p_e_g.html#ga67ac12fee79073242cb216e07c9f1f90',1,'turbojpeg.h']]], + ['tjbufsizeyuv2_17',['tjBufSizeYUV2',['../group___turbo_j_p_e_g.html#ga5e5aac9e8bcf17049279301e2466474c',1,'turbojpeg.h']]], + ['tjcompress2_18',['tjCompress2',['../group___turbo_j_p_e_g.html#gafbdce0112fd78fd38efae841443a9bcf',1,'turbojpeg.h']]], + ['tjcompressfromyuv_19',['tjCompressFromYUV',['../group___turbo_j_p_e_g.html#gab40f5096a72fd7e5bda9d6b58fa37e2e',1,'turbojpeg.h']]], + ['tjcompressfromyuvplanes_20',['tjCompressFromYUVPlanes',['../group___turbo_j_p_e_g.html#ga29ec5dfbd2d84b8724e951d6fa0d5d9e',1,'turbojpeg.h']]], + ['tjcs_21',['TJCS',['../group___turbo_j_p_e_g.html#ga4f83ad3368e0e29d1957be0efa7c3720',1,'turbojpeg.h']]], + ['tjcs_5fcmyk_22',['TJCS_CMYK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a6c8b636152ac8195b869587db315ee53',1,'turbojpeg.h']]], + ['tjcs_5fgray_23',['TJCS_GRAY',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720ab3e7d6a87f695e45b81c1b5262b5a50a',1,'turbojpeg.h']]], + ['tjcs_5frgb_24',['TJCS_RGB',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a677cb7ccb85c4038ac41964a2e09e555',1,'turbojpeg.h']]], + ['tjcs_5fycbcr_25',['TJCS_YCbCr',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a7389b8f65bb387ffedce3efd0d78ec75',1,'turbojpeg.h']]], + ['tjcs_5fycck_26',['TJCS_YCCK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a53839e0fe867b76b58d16b0a1a7c598e',1,'turbojpeg.h']]], + ['tjdecodeyuv_27',['tjDecodeYUV',['../group___turbo_j_p_e_g.html#ga97c2cedc1e2bade15a84164c94e503c1',1,'turbojpeg.h']]], + ['tjdecodeyuvplanes_28',['tjDecodeYUVPlanes',['../group___turbo_j_p_e_g.html#ga10e837c07fa9d25770565b237d3898d9',1,'turbojpeg.h']]], + ['tjdecompress2_29',['tjDecompress2',['../group___turbo_j_p_e_g.html#gae9eccef8b682a48f43a9117c231ed013',1,'turbojpeg.h']]], + ['tjdecompressheader3_30',['tjDecompressHeader3',['../group___turbo_j_p_e_g.html#ga0595681096bba7199cc6f3533cb25f77',1,'turbojpeg.h']]], + ['tjdecompresstoyuv2_31',['tjDecompressToYUV2',['../group___turbo_j_p_e_g.html#ga5a3093e325598c17a9f004323af6fafa',1,'turbojpeg.h']]], + ['tjdecompresstoyuvplanes_32',['tjDecompressToYUVPlanes',['../group___turbo_j_p_e_g.html#gaa59f901a5258ada5bd0185ad59368540',1,'turbojpeg.h']]], + ['tjdestroy_33',['tjDestroy',['../group___turbo_j_p_e_g.html#ga75f355fa27225ba1a4ee392c852394d2',1,'turbojpeg.h']]], + ['tjencodeyuv3_34',['tjEncodeYUV3',['../group___turbo_j_p_e_g.html#ga5d619e0a02b71e05a8dffb764f6d7a64',1,'turbojpeg.h']]], + ['tjencodeyuvplanes_35',['tjEncodeYUVPlanes',['../group___turbo_j_p_e_g.html#gae2d04c72457fe7f4d60cf78ab1b1feb1',1,'turbojpeg.h']]], + ['tjerr_36',['TJERR',['../group___turbo_j_p_e_g.html#gafbc17cfa57d0d5d11fea35ac025950fe',1,'turbojpeg.h']]], + ['tjerr_5ffatal_37',['TJERR_FATAL',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950feafc9cceeada13122b09e4851e3788039a',1,'turbojpeg.h']]], + ['tjerr_5fwarning_38',['TJERR_WARNING',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950fea342dd6e2aedb47bb257b4e7568329b59',1,'turbojpeg.h']]], + ['tjflag_5faccuratedct_39',['TJFLAG_ACCURATEDCT',['../group___turbo_j_p_e_g.html#gacb233cfd722d66d1ccbf48a7de81f0e0',1,'turbojpeg.h']]], + ['tjflag_5fbottomup_40',['TJFLAG_BOTTOMUP',['../group___turbo_j_p_e_g.html#ga72ecf4ebe6eb702d3c6f5ca27455e1ec',1,'turbojpeg.h']]], + ['tjflag_5ffastdct_41',['TJFLAG_FASTDCT',['../group___turbo_j_p_e_g.html#gaabce235db80d3f698b27f36cbd453da2',1,'turbojpeg.h']]], + ['tjflag_5ffastupsample_42',['TJFLAG_FASTUPSAMPLE',['../group___turbo_j_p_e_g.html#ga4ee4506c81177a06f77e2504a22efd2d',1,'turbojpeg.h']]], + ['tjflag_5flimitscans_43',['TJFLAG_LIMITSCANS',['../group___turbo_j_p_e_g.html#ga163e6482dc5096831feef9c79ff3f805',1,'turbojpeg.h']]], + ['tjflag_5fnorealloc_44',['TJFLAG_NOREALLOC',['../group___turbo_j_p_e_g.html#ga8808d403c68b62aaa58a4c1e58e98963',1,'turbojpeg.h']]], + ['tjflag_5fprogressive_45',['TJFLAG_PROGRESSIVE',['../group___turbo_j_p_e_g.html#ga43b426750b46190a25d34a67ef76df1b',1,'turbojpeg.h']]], + ['tjflag_5fstoponwarning_46',['TJFLAG_STOPONWARNING',['../group___turbo_j_p_e_g.html#ga519cfa4ef6c18d9e5b455fdf59306a3a',1,'turbojpeg.h']]], + ['tjfree_47',['tjFree',['../group___turbo_j_p_e_g.html#gaea863d2da0cdb609563aabdf9196514b',1,'turbojpeg.h']]], + ['tjgeterrorcode_48',['tjGetErrorCode',['../group___turbo_j_p_e_g.html#ga414feeffbf860ebd31c745df203de410',1,'turbojpeg.h']]], + ['tjgeterrorstr2_49',['tjGetErrorStr2',['../group___turbo_j_p_e_g.html#ga1ead8574f9f39fbafc6b497124e7aafa',1,'turbojpeg.h']]], + ['tjgetscalingfactors_50',['tjGetScalingFactors',['../group___turbo_j_p_e_g.html#ga193d0977b3b9966d53a6c402e90899b1',1,'turbojpeg.h']]], + ['tjgreenoffset_51',['tjGreenOffset',['../group___turbo_j_p_e_g.html#ga82d6e35da441112a411da41923c0ba2f',1,'turbojpeg.h']]], + ['tjhandle_52',['tjhandle',['../group___turbo_j_p_e_g.html#ga758d2634ecb4949de7815cba621f5763',1,'turbojpeg.h']]], + ['tjinitcompress_53',['tjInitCompress',['../group___turbo_j_p_e_g.html#ga9d63a05fc6d813f4aae06107041a37e8',1,'turbojpeg.h']]], + ['tjinitdecompress_54',['tjInitDecompress',['../group___turbo_j_p_e_g.html#ga52300eac3f3d9ef4bab303bc244f62d3',1,'turbojpeg.h']]], + ['tjinittransform_55',['tjInitTransform',['../group___turbo_j_p_e_g.html#ga928beff6ac248ceadf01089fc6b41957',1,'turbojpeg.h']]], + ['tjloadimage_56',['tjLoadImage',['../group___turbo_j_p_e_g.html#gaffbd83c375e79f5db4b5c5d8ad4466e7',1,'turbojpeg.h']]], + ['tjmcuheight_57',['tjMCUHeight',['../group___turbo_j_p_e_g.html#gabd247bb9fecb393eca57366feb8327bf',1,'turbojpeg.h']]], + ['tjmcuwidth_58',['tjMCUWidth',['../group___turbo_j_p_e_g.html#ga9e61e7cd47a15a173283ba94e781308c',1,'turbojpeg.h']]], + ['tjpad_59',['TJPAD',['../group___turbo_j_p_e_g.html#ga0aba955473315e405295d978f0c16511',1,'turbojpeg.h']]], + ['tjpf_60',['TJPF',['../group___turbo_j_p_e_g.html#gac916144e26c3817ac514e64ae5d12e2a',1,'turbojpeg.h']]], + ['tjpf_5fabgr_61',['TJPF_ABGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa1ba1a7f1631dbeaa49a0a85fc4a40081',1,'turbojpeg.h']]], + ['tjpf_5fargb_62',['TJPF_ARGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aae8f846ed9d9de99b6e1dfe448848765c',1,'turbojpeg.h']]], + ['tjpf_5fbgr_63',['TJPF_BGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aab10624437fb8ef495a0b153e65749839',1,'turbojpeg.h']]], + ['tjpf_5fbgra_64',['TJPF_BGRA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aac037ff1845cf9b74bb81a3659c2b9fb4',1,'turbojpeg.h']]], + ['tjpf_5fbgrx_65',['TJPF_BGRX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa2a1fbf569ca79897eae886e3376ca4c8',1,'turbojpeg.h']]], + ['tjpf_5fcmyk_66',['TJPF_CMYK',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7f5100ec44c91994e243f1cf55553f8b',1,'turbojpeg.h']]], + ['tjpf_5fgray_67',['TJPF_GRAY',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa5431b54b015337705f13118073711a1a',1,'turbojpeg.h']]], + ['tjpf_5frgb_68',['TJPF_RGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7ce93230bff449518ce387c17e6ed37c',1,'turbojpeg.h']]], + ['tjpf_5frgba_69',['TJPF_RGBA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa88d2e88fab67f6503cf972e14851cc12',1,'turbojpeg.h']]], + ['tjpf_5frgbx_70',['TJPF_RGBX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa83973bebb7e2dc6fa8bae89ff3f42e01',1,'turbojpeg.h']]], + ['tjpf_5funknown_71',['TJPF_UNKNOWN',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa84c1a6cead7952998e2fb895844a21ed',1,'turbojpeg.h']]], + ['tjpf_5fxbgr_72',['TJPF_XBGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aaf6603b27147de47e212e75dac027b2af',1,'turbojpeg.h']]], + ['tjpf_5fxrgb_73',['TJPF_XRGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aadae996905efcfa3b42a0bb3bea7f9d84',1,'turbojpeg.h']]], + ['tjpixelsize_74',['tjPixelSize',['../group___turbo_j_p_e_g.html#gad77cf8fe5b2bfd3cb3f53098146abb4c',1,'turbojpeg.h']]], + ['tjplaneheight_75',['tjPlaneHeight',['../group___turbo_j_p_e_g.html#ga1a209696c6a80748f20e134b3c64789f',1,'turbojpeg.h']]], + ['tjplanesizeyuv_76',['tjPlaneSizeYUV',['../group___turbo_j_p_e_g.html#gab4ab7b24f6e797d79abaaa670373961d',1,'turbojpeg.h']]], + ['tjplanewidth_77',['tjPlaneWidth',['../group___turbo_j_p_e_g.html#ga63fb66bb1e36c74008c4634360becbb1',1,'turbojpeg.h']]], + ['tjredoffset_78',['tjRedOffset',['../group___turbo_j_p_e_g.html#gadd9b446742ac8a3923f7992c7988fea8',1,'turbojpeg.h']]], + ['tjregion_79',['tjregion',['../structtjregion.html',1,'']]], + ['tjsamp_80',['TJSAMP',['../group___turbo_j_p_e_g.html#ga1d047060ea80bb9820d540bb928e9074',1,'turbojpeg.h']]], + ['tjsamp_5f411_81',['TJSAMP_411',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a28ec62575e5ea295c3fde3001dc628e2',1,'turbojpeg.h']]], + ['tjsamp_5f420_82',['TJSAMP_420',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a63085dbf683cfe39e513cdb6343e3737',1,'turbojpeg.h']]], + ['tjsamp_5f422_83',['TJSAMP_422',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a136130902cc578f11f32429b59368404',1,'turbojpeg.h']]], + ['tjsamp_5f440_84',['TJSAMP_440',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074accf740e6f3aa6ba20ba922cad13cb974',1,'turbojpeg.h']]], + ['tjsamp_5f444_85',['TJSAMP_444',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074afb8da4f44197837bdec0a4f593dacae3',1,'turbojpeg.h']]], + ['tjsamp_5fgray_86',['TJSAMP_GRAY',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a3f1c9504842ddc7a48d0f690754b6248',1,'turbojpeg.h']]], + ['tjsaveimage_87',['tjSaveImage',['../group___turbo_j_p_e_g.html#ga6f445b22d8933ae4815b3370a538d879',1,'turbojpeg.h']]], + ['tjscaled_88',['TJSCALED',['../group___turbo_j_p_e_g.html#ga84878bb65404204743aa18cac02781df',1,'turbojpeg.h']]], + ['tjscalingfactor_89',['tjscalingfactor',['../structtjscalingfactor.html',1,'']]], + ['tjtransform_90',['tjtransform',['../structtjtransform.html',1,'tjtransform'],['../group___turbo_j_p_e_g.html#ga9cb8abf4cc91881e04a0329b2270be25',1,'tjTransform(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, int n, unsigned char **dstBufs, unsigned long *dstSizes, tjtransform *transforms, int flags): turbojpeg.h'],['../group___turbo_j_p_e_g.html#ga504805ec0161f1b505397ca0118bf8fd',1,'tjtransform(): turbojpeg.h']]], + ['tjxop_91',['TJXOP',['../group___turbo_j_p_e_g.html#ga2de531af4e7e6c4f124908376b354866',1,'turbojpeg.h']]], + ['tjxop_5fhflip_92',['TJXOP_HFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aa0df69776caa30f0fa28e26332d311ce',1,'turbojpeg.h']]], + ['tjxop_5fnone_93',['TJXOP_NONE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aad88c0366cd3f7d0eac9d7a3fa1c2c27',1,'turbojpeg.h']]], + ['tjxop_5frot180_94',['TJXOP_ROT180',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a140952eb8dd0300accfcc22726d69692',1,'turbojpeg.h']]], + ['tjxop_5frot270_95',['TJXOP_ROT270',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a3064ee5dfb7f032df332818587567a08',1,'turbojpeg.h']]], + ['tjxop_5frot90_96',['TJXOP_ROT90',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a43b2bbb23bc4bd548422d43fbe9af128',1,'turbojpeg.h']]], + ['tjxop_5ftranspose_97',['TJXOP_TRANSPOSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a31060aed199f886afdd417f80499c32d',1,'turbojpeg.h']]], + ['tjxop_5ftransverse_98',['TJXOP_TRANSVERSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866af3b14d488aea6ece9e5b3df73a74d6a4',1,'turbojpeg.h']]], + ['tjxop_5fvflip_99',['TJXOP_VFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a324eddfbec53b7e691f61e56929d0d5d',1,'turbojpeg.h']]], + ['tjxopt_5fcopynone_100',['TJXOPT_COPYNONE',['../group___turbo_j_p_e_g.html#ga153b468cfb905d0de61706c838986fe8',1,'turbojpeg.h']]], + ['tjxopt_5fcrop_101',['TJXOPT_CROP',['../group___turbo_j_p_e_g.html#ga9c771a757fc1294add611906b89ab2d2',1,'turbojpeg.h']]], + ['tjxopt_5fgray_102',['TJXOPT_GRAY',['../group___turbo_j_p_e_g.html#ga3acee7b48ade1b99e5588736007c2589',1,'turbojpeg.h']]], + ['tjxopt_5fnooutput_103',['TJXOPT_NOOUTPUT',['../group___turbo_j_p_e_g.html#gafbf992bbf6e006705886333703ffab31',1,'turbojpeg.h']]], + ['tjxopt_5fperfect_104',['TJXOPT_PERFECT',['../group___turbo_j_p_e_g.html#ga50e03cb5ed115330e212417429600b00',1,'turbojpeg.h']]], + ['tjxopt_5fprogressive_105',['TJXOPT_PROGRESSIVE',['../group___turbo_j_p_e_g.html#gad2371c80674584ecc1a7d75e564cf026',1,'turbojpeg.h']]], + ['tjxopt_5ftrim_106',['TJXOPT_TRIM',['../group___turbo_j_p_e_g.html#ga319826b7eb1583c0595bbe7b95428709',1,'turbojpeg.h']]], + ['turbojpeg_107',['TurboJPEG',['../group___turbo_j_p_e_g.html',1,'']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_63.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_63.js deleted file mode 100644 index 7b058da46f3..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_63.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['customfilter',['customFilter',['../structtjtransform.html#a43ee1bcdd2a8d7249a756774f78793c1',1,'tjtransform']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_64.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_64.js deleted file mode 100644 index e19a0501696..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_64.js +++ /dev/null @@ -1,5 +0,0 @@ -var searchData= -[ - ['data',['data',['../structtjtransform.html#a688fe8f1a8ecc12a538d9e561cf338e3',1,'tjtransform']]], - ['denom',['denom',['../structtjscalingfactor.html#aefbcdf3e9e62274b2d312c695f133ce3',1,'tjscalingfactor']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_68.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_68.js deleted file mode 100644 index 7b17e974cd2..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_68.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['h',['h',['../structtjregion.html#aecefc45a26f4d8b60dd4d825c1710115',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6e.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_6e.js deleted file mode 100644 index 83faa1343fb..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6e.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['num',['num',['../structtjscalingfactor.html#a9b011e57f981ee23083e2c1aa5e640ec',1,'tjscalingfactor']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.html deleted file mode 100644 index 5e86b030d2c..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.js deleted file mode 100644 index 1cca83243bb..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_6f.js +++ /dev/null @@ -1,5 +0,0 @@ -var searchData= -[ - ['op',['op',['../structtjtransform.html#a2525aab4ba6978a1c273f74fef50e498',1,'tjtransform']]], - ['options',['options',['../structtjtransform.html#ac0e74655baa4402209a21e1ae481c8f6',1,'tjtransform']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_7.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_7.html new file mode 100644 index 00000000000..18c555de264 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_7.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_7.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_7.js new file mode 100644 index 00000000000..46e4994990d --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_7.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['w_108',['w',['../structtjregion.html#ab6eb73ceef584fc23c8c8097926dce42',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_72.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_72.html deleted file mode 100644 index 347b9f66607..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_72.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_72.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_72.js deleted file mode 100644 index 01cde35e1eb..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_72.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['r',['r',['../structtjtransform.html#ac324e5e442abec8a961e5bf219db12cf',1,'tjtransform']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_74.html deleted file mode 100644 index c646aeffcdf..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_74.js deleted file mode 100644 index 5b97c71e19b..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_74.js +++ /dev/null @@ -1,102 +0,0 @@ -var searchData= -[ - ['tj_5fnumcs',['TJ_NUMCS',['../group___turbo_j_p_e_g.html#ga39f57a6fb02d9cf32e7b6890099b5a71',1,'turbojpeg.h']]], - ['tj_5fnumerr',['TJ_NUMERR',['../group___turbo_j_p_e_g.html#ga79bde1b4a3e2351e00887e47781b966e',1,'turbojpeg.h']]], - ['tj_5fnumpf',['TJ_NUMPF',['../group___turbo_j_p_e_g.html#ga7010a4402f54a45ba822ad8675a4655e',1,'turbojpeg.h']]], - ['tj_5fnumsamp',['TJ_NUMSAMP',['../group___turbo_j_p_e_g.html#ga5ef3d169162ce77ce348e292a0b7477c',1,'turbojpeg.h']]], - ['tj_5fnumxop',['TJ_NUMXOP',['../group___turbo_j_p_e_g.html#ga0f6dbd18adf38b7d46ac547f0f4d562c',1,'turbojpeg.h']]], - ['tjalloc',['tjAlloc',['../group___turbo_j_p_e_g.html#gaec627dd4c5f30b7a775a7aea3bec5d83',1,'turbojpeg.h']]], - ['tjalphaoffset',['tjAlphaOffset',['../group___turbo_j_p_e_g.html#ga5af0ab065feefd526debf1e20c43e837',1,'turbojpeg.h']]], - ['tjblueoffset',['tjBlueOffset',['../group___turbo_j_p_e_g.html#ga84e2e35d3f08025f976ec1ec53693dea',1,'turbojpeg.h']]], - ['tjbufsize',['tjBufSize',['../group___turbo_j_p_e_g.html#ga67ac12fee79073242cb216e07c9f1f90',1,'turbojpeg.h']]], - ['tjbufsizeyuv2',['tjBufSizeYUV2',['../group___turbo_j_p_e_g.html#ga2be2b9969d4df9ecce9b05deed273194',1,'turbojpeg.h']]], - ['tjcompress2',['tjCompress2',['../group___turbo_j_p_e_g.html#gafbdce0112fd78fd38efae841443a9bcf',1,'turbojpeg.h']]], - ['tjcompressfromyuv',['tjCompressFromYUV',['../group___turbo_j_p_e_g.html#ga7622a459b79aa1007e005b58783f875b',1,'turbojpeg.h']]], - ['tjcompressfromyuvplanes',['tjCompressFromYUVPlanes',['../group___turbo_j_p_e_g.html#ga29ec5dfbd2d84b8724e951d6fa0d5d9e',1,'turbojpeg.h']]], - ['tjcs',['TJCS',['../group___turbo_j_p_e_g.html#ga4f83ad3368e0e29d1957be0efa7c3720',1,'turbojpeg.h']]], - ['tjcs_5fcmyk',['TJCS_CMYK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a6c8b636152ac8195b869587db315ee53',1,'turbojpeg.h']]], - ['tjcs_5fgray',['TJCS_GRAY',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720ab3e7d6a87f695e45b81c1b5262b5a50a',1,'turbojpeg.h']]], - ['tjcs_5frgb',['TJCS_RGB',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a677cb7ccb85c4038ac41964a2e09e555',1,'turbojpeg.h']]], - ['tjcs_5fycbcr',['TJCS_YCbCr',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a7389b8f65bb387ffedce3efd0d78ec75',1,'turbojpeg.h']]], - ['tjcs_5fycck',['TJCS_YCCK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a53839e0fe867b76b58d16b0a1a7c598e',1,'turbojpeg.h']]], - ['tjdecodeyuv',['tjDecodeYUV',['../group___turbo_j_p_e_g.html#ga70abbf38f77a26fd6da8813bef96f695',1,'turbojpeg.h']]], - ['tjdecodeyuvplanes',['tjDecodeYUVPlanes',['../group___turbo_j_p_e_g.html#ga10e837c07fa9d25770565b237d3898d9',1,'turbojpeg.h']]], - ['tjdecompress2',['tjDecompress2',['../group___turbo_j_p_e_g.html#gae9eccef8b682a48f43a9117c231ed013',1,'turbojpeg.h']]], - ['tjdecompressheader3',['tjDecompressHeader3',['../group___turbo_j_p_e_g.html#ga0595681096bba7199cc6f3533cb25f77',1,'turbojpeg.h']]], - ['tjdecompresstoyuv2',['tjDecompressToYUV2',['../group___turbo_j_p_e_g.html#ga04d1e839ff9a0860dd1475cff78d3364',1,'turbojpeg.h']]], - ['tjdecompresstoyuvplanes',['tjDecompressToYUVPlanes',['../group___turbo_j_p_e_g.html#gaa59f901a5258ada5bd0185ad59368540',1,'turbojpeg.h']]], - ['tjdestroy',['tjDestroy',['../group___turbo_j_p_e_g.html#ga75f355fa27225ba1a4ee392c852394d2',1,'turbojpeg.h']]], - ['tjencodeyuv3',['tjEncodeYUV3',['../group___turbo_j_p_e_g.html#gac519b922cdf446e97d0cdcba513636bf',1,'turbojpeg.h']]], - ['tjencodeyuvplanes',['tjEncodeYUVPlanes',['../group___turbo_j_p_e_g.html#gae2d04c72457fe7f4d60cf78ab1b1feb1',1,'turbojpeg.h']]], - ['tjerr',['TJERR',['../group___turbo_j_p_e_g.html#gafbc17cfa57d0d5d11fea35ac025950fe',1,'turbojpeg.h']]], - ['tjerr_5ffatal',['TJERR_FATAL',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950feafc9cceeada13122b09e4851e3788039a',1,'turbojpeg.h']]], - ['tjerr_5fwarning',['TJERR_WARNING',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950fea342dd6e2aedb47bb257b4e7568329b59',1,'turbojpeg.h']]], - ['tjflag_5faccuratedct',['TJFLAG_ACCURATEDCT',['../group___turbo_j_p_e_g.html#gacb233cfd722d66d1ccbf48a7de81f0e0',1,'turbojpeg.h']]], - ['tjflag_5fbottomup',['TJFLAG_BOTTOMUP',['../group___turbo_j_p_e_g.html#ga72ecf4ebe6eb702d3c6f5ca27455e1ec',1,'turbojpeg.h']]], - ['tjflag_5ffastdct',['TJFLAG_FASTDCT',['../group___turbo_j_p_e_g.html#gaabce235db80d3f698b27f36cbd453da2',1,'turbojpeg.h']]], - ['tjflag_5ffastupsample',['TJFLAG_FASTUPSAMPLE',['../group___turbo_j_p_e_g.html#ga4ee4506c81177a06f77e2504a22efd2d',1,'turbojpeg.h']]], - ['tjflag_5fnorealloc',['TJFLAG_NOREALLOC',['../group___turbo_j_p_e_g.html#ga8808d403c68b62aaa58a4c1e58e98963',1,'turbojpeg.h']]], - ['tjflag_5fprogressive',['TJFLAG_PROGRESSIVE',['../group___turbo_j_p_e_g.html#ga43b426750b46190a25d34a67ef76df1b',1,'turbojpeg.h']]], - ['tjflag_5fstoponwarning',['TJFLAG_STOPONWARNING',['../group___turbo_j_p_e_g.html#ga519cfa4ef6c18d9e5b455fdf59306a3a',1,'turbojpeg.h']]], - ['tjfree',['tjFree',['../group___turbo_j_p_e_g.html#gaea863d2da0cdb609563aabdf9196514b',1,'turbojpeg.h']]], - ['tjgeterrorcode',['tjGetErrorCode',['../group___turbo_j_p_e_g.html#ga414feeffbf860ebd31c745df203de410',1,'turbojpeg.h']]], - ['tjgeterrorstr2',['tjGetErrorStr2',['../group___turbo_j_p_e_g.html#ga1ead8574f9f39fbafc6b497124e7aafa',1,'turbojpeg.h']]], - ['tjgetscalingfactors',['tjGetScalingFactors',['../group___turbo_j_p_e_g.html#gac3854476006b10787bd128f7ede48057',1,'turbojpeg.h']]], - ['tjgreenoffset',['tjGreenOffset',['../group___turbo_j_p_e_g.html#ga82d6e35da441112a411da41923c0ba2f',1,'turbojpeg.h']]], - ['tjhandle',['tjhandle',['../group___turbo_j_p_e_g.html#ga758d2634ecb4949de7815cba621f5763',1,'turbojpeg.h']]], - ['tjinitcompress',['tjInitCompress',['../group___turbo_j_p_e_g.html#ga9d63a05fc6d813f4aae06107041a37e8',1,'turbojpeg.h']]], - ['tjinitdecompress',['tjInitDecompress',['../group___turbo_j_p_e_g.html#ga52300eac3f3d9ef4bab303bc244f62d3',1,'turbojpeg.h']]], - ['tjinittransform',['tjInitTransform',['../group___turbo_j_p_e_g.html#ga928beff6ac248ceadf01089fc6b41957',1,'turbojpeg.h']]], - ['tjloadimage',['tjLoadImage',['../group___turbo_j_p_e_g.html#gaffbd83c375e79f5db4b5c5d8ad4466e7',1,'turbojpeg.h']]], - ['tjmcuheight',['tjMCUHeight',['../group___turbo_j_p_e_g.html#gabd247bb9fecb393eca57366feb8327bf',1,'turbojpeg.h']]], - ['tjmcuwidth',['tjMCUWidth',['../group___turbo_j_p_e_g.html#ga9e61e7cd47a15a173283ba94e781308c',1,'turbojpeg.h']]], - ['tjpad',['TJPAD',['../group___turbo_j_p_e_g.html#ga0aba955473315e405295d978f0c16511',1,'turbojpeg.h']]], - ['tjpf',['TJPF',['../group___turbo_j_p_e_g.html#gac916144e26c3817ac514e64ae5d12e2a',1,'turbojpeg.h']]], - ['tjpf_5fabgr',['TJPF_ABGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa1ba1a7f1631dbeaa49a0a85fc4a40081',1,'turbojpeg.h']]], - ['tjpf_5fargb',['TJPF_ARGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aae8f846ed9d9de99b6e1dfe448848765c',1,'turbojpeg.h']]], - ['tjpf_5fbgr',['TJPF_BGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aab10624437fb8ef495a0b153e65749839',1,'turbojpeg.h']]], - ['tjpf_5fbgra',['TJPF_BGRA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aac037ff1845cf9b74bb81a3659c2b9fb4',1,'turbojpeg.h']]], - ['tjpf_5fbgrx',['TJPF_BGRX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa2a1fbf569ca79897eae886e3376ca4c8',1,'turbojpeg.h']]], - ['tjpf_5fcmyk',['TJPF_CMYK',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7f5100ec44c91994e243f1cf55553f8b',1,'turbojpeg.h']]], - ['tjpf_5fgray',['TJPF_GRAY',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa5431b54b015337705f13118073711a1a',1,'turbojpeg.h']]], - ['tjpf_5frgb',['TJPF_RGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7ce93230bff449518ce387c17e6ed37c',1,'turbojpeg.h']]], - ['tjpf_5frgba',['TJPF_RGBA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa88d2e88fab67f6503cf972e14851cc12',1,'turbojpeg.h']]], - ['tjpf_5frgbx',['TJPF_RGBX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa83973bebb7e2dc6fa8bae89ff3f42e01',1,'turbojpeg.h']]], - ['tjpf_5funknown',['TJPF_UNKNOWN',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa84c1a6cead7952998e2fb895844a21ed',1,'turbojpeg.h']]], - ['tjpf_5fxbgr',['TJPF_XBGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aaf6603b27147de47e212e75dac027b2af',1,'turbojpeg.h']]], - ['tjpf_5fxrgb',['TJPF_XRGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aadae996905efcfa3b42a0bb3bea7f9d84',1,'turbojpeg.h']]], - ['tjpixelsize',['tjPixelSize',['../group___turbo_j_p_e_g.html#gad77cf8fe5b2bfd3cb3f53098146abb4c',1,'turbojpeg.h']]], - ['tjplaneheight',['tjPlaneHeight',['../group___turbo_j_p_e_g.html#ga1a209696c6a80748f20e134b3c64789f',1,'turbojpeg.h']]], - ['tjplanesizeyuv',['tjPlaneSizeYUV',['../group___turbo_j_p_e_g.html#gab4ab7b24f6e797d79abaaa670373961d',1,'turbojpeg.h']]], - ['tjplanewidth',['tjPlaneWidth',['../group___turbo_j_p_e_g.html#ga63fb66bb1e36c74008c4634360becbb1',1,'turbojpeg.h']]], - ['tjredoffset',['tjRedOffset',['../group___turbo_j_p_e_g.html#gadd9b446742ac8a3923f7992c7988fea8',1,'turbojpeg.h']]], - ['tjregion',['tjregion',['../structtjregion.html',1,'']]], - ['tjsamp',['TJSAMP',['../group___turbo_j_p_e_g.html#ga1d047060ea80bb9820d540bb928e9074',1,'turbojpeg.h']]], - ['tjsamp_5f411',['TJSAMP_411',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a28ec62575e5ea295c3fde3001dc628e2',1,'turbojpeg.h']]], - ['tjsamp_5f420',['TJSAMP_420',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a63085dbf683cfe39e513cdb6343e3737',1,'turbojpeg.h']]], - ['tjsamp_5f422',['TJSAMP_422',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a136130902cc578f11f32429b59368404',1,'turbojpeg.h']]], - ['tjsamp_5f440',['TJSAMP_440',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074accf740e6f3aa6ba20ba922cad13cb974',1,'turbojpeg.h']]], - ['tjsamp_5f444',['TJSAMP_444',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074afb8da4f44197837bdec0a4f593dacae3',1,'turbojpeg.h']]], - ['tjsamp_5fgray',['TJSAMP_GRAY',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a3f1c9504842ddc7a48d0f690754b6248',1,'turbojpeg.h']]], - ['tjsaveimage',['tjSaveImage',['../group___turbo_j_p_e_g.html#ga6f445b22d8933ae4815b3370a538d879',1,'turbojpeg.h']]], - ['tjscaled',['TJSCALED',['../group___turbo_j_p_e_g.html#ga84878bb65404204743aa18cac02781df',1,'turbojpeg.h']]], - ['tjscalingfactor',['tjscalingfactor',['../structtjscalingfactor.html',1,'']]], - ['tjtransform',['tjtransform',['../structtjtransform.html',1,'tjtransform'],['../group___turbo_j_p_e_g.html#gaa29f3189c41be12ec5dee7caec318a31',1,'tjtransform(): turbojpeg.h'],['../group___turbo_j_p_e_g.html#ga9cb8abf4cc91881e04a0329b2270be25',1,'tjTransform(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, int n, unsigned char **dstBufs, unsigned long *dstSizes, tjtransform *transforms, int flags): turbojpeg.h']]], - ['tjxop',['TJXOP',['../group___turbo_j_p_e_g.html#ga2de531af4e7e6c4f124908376b354866',1,'turbojpeg.h']]], - ['tjxop_5fhflip',['TJXOP_HFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aa0df69776caa30f0fa28e26332d311ce',1,'turbojpeg.h']]], - ['tjxop_5fnone',['TJXOP_NONE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aad88c0366cd3f7d0eac9d7a3fa1c2c27',1,'turbojpeg.h']]], - ['tjxop_5frot180',['TJXOP_ROT180',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a140952eb8dd0300accfcc22726d69692',1,'turbojpeg.h']]], - ['tjxop_5frot270',['TJXOP_ROT270',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a3064ee5dfb7f032df332818587567a08',1,'turbojpeg.h']]], - ['tjxop_5frot90',['TJXOP_ROT90',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a43b2bbb23bc4bd548422d43fbe9af128',1,'turbojpeg.h']]], - ['tjxop_5ftranspose',['TJXOP_TRANSPOSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a31060aed199f886afdd417f80499c32d',1,'turbojpeg.h']]], - ['tjxop_5ftransverse',['TJXOP_TRANSVERSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866af3b14d488aea6ece9e5b3df73a74d6a4',1,'turbojpeg.h']]], - ['tjxop_5fvflip',['TJXOP_VFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a324eddfbec53b7e691f61e56929d0d5d',1,'turbojpeg.h']]], - ['tjxopt_5fcopynone',['TJXOPT_COPYNONE',['../group___turbo_j_p_e_g.html#ga153b468cfb905d0de61706c838986fe8',1,'turbojpeg.h']]], - ['tjxopt_5fcrop',['TJXOPT_CROP',['../group___turbo_j_p_e_g.html#ga9c771a757fc1294add611906b89ab2d2',1,'turbojpeg.h']]], - ['tjxopt_5fgray',['TJXOPT_GRAY',['../group___turbo_j_p_e_g.html#ga3acee7b48ade1b99e5588736007c2589',1,'turbojpeg.h']]], - ['tjxopt_5fnooutput',['TJXOPT_NOOUTPUT',['../group___turbo_j_p_e_g.html#gafbf992bbf6e006705886333703ffab31',1,'turbojpeg.h']]], - ['tjxopt_5fperfect',['TJXOPT_PERFECT',['../group___turbo_j_p_e_g.html#ga50e03cb5ed115330e212417429600b00',1,'turbojpeg.h']]], - ['tjxopt_5fprogressive',['TJXOPT_PROGRESSIVE',['../group___turbo_j_p_e_g.html#gad2371c80674584ecc1a7d75e564cf026',1,'turbojpeg.h']]], - ['tjxopt_5ftrim',['TJXOPT_TRIM',['../group___turbo_j_p_e_g.html#ga319826b7eb1583c0595bbe7b95428709',1,'turbojpeg.h']]], - ['turbojpeg',['TurboJPEG',['../group___turbo_j_p_e_g.html',1,'']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_77.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_77.html deleted file mode 100644 index 55d7142924d..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_77.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_77.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_77.js deleted file mode 100644 index 42670029cde..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_77.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['w',['w',['../structtjregion.html#ab6eb73ceef584fc23c8c8097926dce42',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_78.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_78.html deleted file mode 100644 index 39075d44ebb..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_78.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_78.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_78.js deleted file mode 100644 index 41a27f2fc34..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_78.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['x',['x',['../structtjregion.html#a4b6a37a93997091b26a75831fa291ad9',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_79.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_79.html deleted file mode 100644 index 033719a1b38..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_79.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_79.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_79.js deleted file mode 100644 index 86890a698fe..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/all_79.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['y',['y',['../structtjregion.html#a7b3e0c24cfe87acc80e334cafdcf22c2',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_8.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_8.html new file mode 100644 index 00000000000..0f9eb416d3d --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_8.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_8.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_8.js new file mode 100644 index 00000000000..157ee981068 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_8.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['x_109',['x',['../structtjregion.html#a4b6a37a93997091b26a75831fa291ad9',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_9.html b/third-party/mozjpeg/mozjpeg/doc/html/search/all_9.html new file mode 100644 index 00000000000..d27c0f7c2dc --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_9.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/all_9.js b/third-party/mozjpeg/mozjpeg/doc/html/search/all_9.js new file mode 100644 index 00000000000..80ac522352d --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/all_9.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['y_110',['y',['../structtjregion.html#a7b3e0c24cfe87acc80e334cafdcf22c2',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.html b/third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.html new file mode 100644 index 00000000000..7e0afc8447c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.js new file mode 100644 index 00000000000..ed057f2a03e --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/classes_0.js @@ -0,0 +1,6 @@ +var searchData= +[ + ['tjregion_111',['tjregion',['../structtjregion.html',1,'']]], + ['tjscalingfactor_112',['tjscalingfactor',['../structtjscalingfactor.html',1,'']]], + ['tjtransform_113',['tjtransform',['../structtjtransform.html',1,'']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.html deleted file mode 100644 index 4b0fdaa1601..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.js deleted file mode 100644 index cd623d263a7..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/classes_74.js +++ /dev/null @@ -1,6 +0,0 @@ -var searchData= -[ - ['tjregion',['tjregion',['../structtjregion.html',1,'']]], - ['tjscalingfactor',['tjscalingfactor',['../structtjscalingfactor.html',1,'']]], - ['tjtransform',['tjtransform',['../structtjtransform.html',1,'']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/close.png b/third-party/mozjpeg/mozjpeg/doc/html/search/close.png deleted file mode 100644 index 9342d3dfeea7b7c4ee610987e717804b5a42ceb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273 zcmV+s0q*{ZP)4(RlMby96)VwnbG{ zbe&}^BDn7x>$<{ck4zAK-=nT;=hHG)kmplIF${xqm8db3oX6wT3bvp`TE@m0cg;b) zBuSL}5?N7O(iZLdAlz@)b)Rd~DnSsSX&P5qC`XwuFwcAYLC+d2>+1(8on;wpt8QIC X2MT$R4iQDd00000NkvXXu0mjfia~GN diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/close.svg b/third-party/mozjpeg/mozjpeg/doc/html/search/close.svg new file mode 100644 index 00000000000..a933eea1a26 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/close.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.html b/third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.html new file mode 100644 index 00000000000..9035e6aa121 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.js new file mode 100644 index 00000000000..0e15c9a3897 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/enums_0.js @@ -0,0 +1,8 @@ +var searchData= +[ + ['tjcs_162',['TJCS',['../group___turbo_j_p_e_g.html#ga4f83ad3368e0e29d1957be0efa7c3720',1,'turbojpeg.h']]], + ['tjerr_163',['TJERR',['../group___turbo_j_p_e_g.html#gafbc17cfa57d0d5d11fea35ac025950fe',1,'turbojpeg.h']]], + ['tjpf_164',['TJPF',['../group___turbo_j_p_e_g.html#gac916144e26c3817ac514e64ae5d12e2a',1,'turbojpeg.h']]], + ['tjsamp_165',['TJSAMP',['../group___turbo_j_p_e_g.html#ga1d047060ea80bb9820d540bb928e9074',1,'turbojpeg.h']]], + ['tjxop_166',['TJXOP',['../group___turbo_j_p_e_g.html#ga2de531af4e7e6c4f124908376b354866',1,'turbojpeg.h']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.html deleted file mode 100644 index 9b754ee67f2..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.js deleted file mode 100644 index 19c20cfdd2a..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/enums_74.js +++ /dev/null @@ -1,8 +0,0 @@ -var searchData= -[ - ['tjcs',['TJCS',['../group___turbo_j_p_e_g.html#ga4f83ad3368e0e29d1957be0efa7c3720',1,'turbojpeg.h']]], - ['tjerr',['TJERR',['../group___turbo_j_p_e_g.html#gafbc17cfa57d0d5d11fea35ac025950fe',1,'turbojpeg.h']]], - ['tjpf',['TJPF',['../group___turbo_j_p_e_g.html#gac916144e26c3817ac514e64ae5d12e2a',1,'turbojpeg.h']]], - ['tjsamp',['TJSAMP',['../group___turbo_j_p_e_g.html#ga1d047060ea80bb9820d540bb928e9074',1,'turbojpeg.h']]], - ['tjxop',['TJXOP',['../group___turbo_j_p_e_g.html#ga2de531af4e7e6c4f124908376b354866',1,'turbojpeg.h']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.html b/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.html new file mode 100644 index 00000000000..c2cd472acd3 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.js new file mode 100644 index 00000000000..67c78fef560 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_0.js @@ -0,0 +1,37 @@ +var searchData= +[ + ['tjcs_5fcmyk_167',['TJCS_CMYK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a6c8b636152ac8195b869587db315ee53',1,'turbojpeg.h']]], + ['tjcs_5fgray_168',['TJCS_GRAY',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720ab3e7d6a87f695e45b81c1b5262b5a50a',1,'turbojpeg.h']]], + ['tjcs_5frgb_169',['TJCS_RGB',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a677cb7ccb85c4038ac41964a2e09e555',1,'turbojpeg.h']]], + ['tjcs_5fycbcr_170',['TJCS_YCbCr',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a7389b8f65bb387ffedce3efd0d78ec75',1,'turbojpeg.h']]], + ['tjcs_5fycck_171',['TJCS_YCCK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a53839e0fe867b76b58d16b0a1a7c598e',1,'turbojpeg.h']]], + ['tjerr_5ffatal_172',['TJERR_FATAL',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950feafc9cceeada13122b09e4851e3788039a',1,'turbojpeg.h']]], + ['tjerr_5fwarning_173',['TJERR_WARNING',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950fea342dd6e2aedb47bb257b4e7568329b59',1,'turbojpeg.h']]], + ['tjpf_5fabgr_174',['TJPF_ABGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa1ba1a7f1631dbeaa49a0a85fc4a40081',1,'turbojpeg.h']]], + ['tjpf_5fargb_175',['TJPF_ARGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aae8f846ed9d9de99b6e1dfe448848765c',1,'turbojpeg.h']]], + ['tjpf_5fbgr_176',['TJPF_BGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aab10624437fb8ef495a0b153e65749839',1,'turbojpeg.h']]], + ['tjpf_5fbgra_177',['TJPF_BGRA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aac037ff1845cf9b74bb81a3659c2b9fb4',1,'turbojpeg.h']]], + ['tjpf_5fbgrx_178',['TJPF_BGRX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa2a1fbf569ca79897eae886e3376ca4c8',1,'turbojpeg.h']]], + ['tjpf_5fcmyk_179',['TJPF_CMYK',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7f5100ec44c91994e243f1cf55553f8b',1,'turbojpeg.h']]], + ['tjpf_5fgray_180',['TJPF_GRAY',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa5431b54b015337705f13118073711a1a',1,'turbojpeg.h']]], + ['tjpf_5frgb_181',['TJPF_RGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7ce93230bff449518ce387c17e6ed37c',1,'turbojpeg.h']]], + ['tjpf_5frgba_182',['TJPF_RGBA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa88d2e88fab67f6503cf972e14851cc12',1,'turbojpeg.h']]], + ['tjpf_5frgbx_183',['TJPF_RGBX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa83973bebb7e2dc6fa8bae89ff3f42e01',1,'turbojpeg.h']]], + ['tjpf_5funknown_184',['TJPF_UNKNOWN',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa84c1a6cead7952998e2fb895844a21ed',1,'turbojpeg.h']]], + ['tjpf_5fxbgr_185',['TJPF_XBGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aaf6603b27147de47e212e75dac027b2af',1,'turbojpeg.h']]], + ['tjpf_5fxrgb_186',['TJPF_XRGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aadae996905efcfa3b42a0bb3bea7f9d84',1,'turbojpeg.h']]], + ['tjsamp_5f411_187',['TJSAMP_411',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a28ec62575e5ea295c3fde3001dc628e2',1,'turbojpeg.h']]], + ['tjsamp_5f420_188',['TJSAMP_420',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a63085dbf683cfe39e513cdb6343e3737',1,'turbojpeg.h']]], + ['tjsamp_5f422_189',['TJSAMP_422',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a136130902cc578f11f32429b59368404',1,'turbojpeg.h']]], + ['tjsamp_5f440_190',['TJSAMP_440',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074accf740e6f3aa6ba20ba922cad13cb974',1,'turbojpeg.h']]], + ['tjsamp_5f444_191',['TJSAMP_444',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074afb8da4f44197837bdec0a4f593dacae3',1,'turbojpeg.h']]], + ['tjsamp_5fgray_192',['TJSAMP_GRAY',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a3f1c9504842ddc7a48d0f690754b6248',1,'turbojpeg.h']]], + ['tjxop_5fhflip_193',['TJXOP_HFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aa0df69776caa30f0fa28e26332d311ce',1,'turbojpeg.h']]], + ['tjxop_5fnone_194',['TJXOP_NONE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aad88c0366cd3f7d0eac9d7a3fa1c2c27',1,'turbojpeg.h']]], + ['tjxop_5frot180_195',['TJXOP_ROT180',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a140952eb8dd0300accfcc22726d69692',1,'turbojpeg.h']]], + ['tjxop_5frot270_196',['TJXOP_ROT270',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a3064ee5dfb7f032df332818587567a08',1,'turbojpeg.h']]], + ['tjxop_5frot90_197',['TJXOP_ROT90',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a43b2bbb23bc4bd548422d43fbe9af128',1,'turbojpeg.h']]], + ['tjxop_5ftranspose_198',['TJXOP_TRANSPOSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a31060aed199f886afdd417f80499c32d',1,'turbojpeg.h']]], + ['tjxop_5ftransverse_199',['TJXOP_TRANSVERSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866af3b14d488aea6ece9e5b3df73a74d6a4',1,'turbojpeg.h']]], + ['tjxop_5fvflip_200',['TJXOP_VFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a324eddfbec53b7e691f61e56929d0d5d',1,'turbojpeg.h']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.html deleted file mode 100644 index 0d69a0acb0c..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.js deleted file mode 100644 index e683856978d..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/enumvalues_74.js +++ /dev/null @@ -1,37 +0,0 @@ -var searchData= -[ - ['tjcs_5fcmyk',['TJCS_CMYK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a6c8b636152ac8195b869587db315ee53',1,'turbojpeg.h']]], - ['tjcs_5fgray',['TJCS_GRAY',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720ab3e7d6a87f695e45b81c1b5262b5a50a',1,'turbojpeg.h']]], - ['tjcs_5frgb',['TJCS_RGB',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a677cb7ccb85c4038ac41964a2e09e555',1,'turbojpeg.h']]], - ['tjcs_5fycbcr',['TJCS_YCbCr',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a7389b8f65bb387ffedce3efd0d78ec75',1,'turbojpeg.h']]], - ['tjcs_5fycck',['TJCS_YCCK',['../group___turbo_j_p_e_g.html#gga4f83ad3368e0e29d1957be0efa7c3720a53839e0fe867b76b58d16b0a1a7c598e',1,'turbojpeg.h']]], - ['tjerr_5ffatal',['TJERR_FATAL',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950feafc9cceeada13122b09e4851e3788039a',1,'turbojpeg.h']]], - ['tjerr_5fwarning',['TJERR_WARNING',['../group___turbo_j_p_e_g.html#ggafbc17cfa57d0d5d11fea35ac025950fea342dd6e2aedb47bb257b4e7568329b59',1,'turbojpeg.h']]], - ['tjpf_5fabgr',['TJPF_ABGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa1ba1a7f1631dbeaa49a0a85fc4a40081',1,'turbojpeg.h']]], - ['tjpf_5fargb',['TJPF_ARGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aae8f846ed9d9de99b6e1dfe448848765c',1,'turbojpeg.h']]], - ['tjpf_5fbgr',['TJPF_BGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aab10624437fb8ef495a0b153e65749839',1,'turbojpeg.h']]], - ['tjpf_5fbgra',['TJPF_BGRA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aac037ff1845cf9b74bb81a3659c2b9fb4',1,'turbojpeg.h']]], - ['tjpf_5fbgrx',['TJPF_BGRX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa2a1fbf569ca79897eae886e3376ca4c8',1,'turbojpeg.h']]], - ['tjpf_5fcmyk',['TJPF_CMYK',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7f5100ec44c91994e243f1cf55553f8b',1,'turbojpeg.h']]], - ['tjpf_5fgray',['TJPF_GRAY',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa5431b54b015337705f13118073711a1a',1,'turbojpeg.h']]], - ['tjpf_5frgb',['TJPF_RGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa7ce93230bff449518ce387c17e6ed37c',1,'turbojpeg.h']]], - ['tjpf_5frgba',['TJPF_RGBA',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa88d2e88fab67f6503cf972e14851cc12',1,'turbojpeg.h']]], - ['tjpf_5frgbx',['TJPF_RGBX',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa83973bebb7e2dc6fa8bae89ff3f42e01',1,'turbojpeg.h']]], - ['tjpf_5funknown',['TJPF_UNKNOWN',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aa84c1a6cead7952998e2fb895844a21ed',1,'turbojpeg.h']]], - ['tjpf_5fxbgr',['TJPF_XBGR',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aaf6603b27147de47e212e75dac027b2af',1,'turbojpeg.h']]], - ['tjpf_5fxrgb',['TJPF_XRGB',['../group___turbo_j_p_e_g.html#ggac916144e26c3817ac514e64ae5d12e2aadae996905efcfa3b42a0bb3bea7f9d84',1,'turbojpeg.h']]], - ['tjsamp_5f411',['TJSAMP_411',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a28ec62575e5ea295c3fde3001dc628e2',1,'turbojpeg.h']]], - ['tjsamp_5f420',['TJSAMP_420',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a63085dbf683cfe39e513cdb6343e3737',1,'turbojpeg.h']]], - ['tjsamp_5f422',['TJSAMP_422',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a136130902cc578f11f32429b59368404',1,'turbojpeg.h']]], - ['tjsamp_5f440',['TJSAMP_440',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074accf740e6f3aa6ba20ba922cad13cb974',1,'turbojpeg.h']]], - ['tjsamp_5f444',['TJSAMP_444',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074afb8da4f44197837bdec0a4f593dacae3',1,'turbojpeg.h']]], - ['tjsamp_5fgray',['TJSAMP_GRAY',['../group___turbo_j_p_e_g.html#gga1d047060ea80bb9820d540bb928e9074a3f1c9504842ddc7a48d0f690754b6248',1,'turbojpeg.h']]], - ['tjxop_5fhflip',['TJXOP_HFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aa0df69776caa30f0fa28e26332d311ce',1,'turbojpeg.h']]], - ['tjxop_5fnone',['TJXOP_NONE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866aad88c0366cd3f7d0eac9d7a3fa1c2c27',1,'turbojpeg.h']]], - ['tjxop_5frot180',['TJXOP_ROT180',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a140952eb8dd0300accfcc22726d69692',1,'turbojpeg.h']]], - ['tjxop_5frot270',['TJXOP_ROT270',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a3064ee5dfb7f032df332818587567a08',1,'turbojpeg.h']]], - ['tjxop_5frot90',['TJXOP_ROT90',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a43b2bbb23bc4bd548422d43fbe9af128',1,'turbojpeg.h']]], - ['tjxop_5ftranspose',['TJXOP_TRANSPOSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a31060aed199f886afdd417f80499c32d',1,'turbojpeg.h']]], - ['tjxop_5ftransverse',['TJXOP_TRANSVERSE',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866af3b14d488aea6ece9e5b3df73a74d6a4',1,'turbojpeg.h']]], - ['tjxop_5fvflip',['TJXOP_VFLIP',['../group___turbo_j_p_e_g.html#gga2de531af4e7e6c4f124908376b354866a324eddfbec53b7e691f61e56929d0d5d',1,'turbojpeg.h']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.html b/third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.html new file mode 100644 index 00000000000..f04535ae678 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.js new file mode 100644 index 00000000000..a608dabe22d --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/functions_0.js @@ -0,0 +1,31 @@ +var searchData= +[ + ['tjalloc_114',['tjAlloc',['../group___turbo_j_p_e_g.html#gaec627dd4c5f30b7a775a7aea3bec5d83',1,'turbojpeg.h']]], + ['tjbufsize_115',['tjBufSize',['../group___turbo_j_p_e_g.html#ga67ac12fee79073242cb216e07c9f1f90',1,'turbojpeg.h']]], + ['tjbufsizeyuv2_116',['tjBufSizeYUV2',['../group___turbo_j_p_e_g.html#ga5e5aac9e8bcf17049279301e2466474c',1,'turbojpeg.h']]], + ['tjcompress2_117',['tjCompress2',['../group___turbo_j_p_e_g.html#gafbdce0112fd78fd38efae841443a9bcf',1,'turbojpeg.h']]], + ['tjcompressfromyuv_118',['tjCompressFromYUV',['../group___turbo_j_p_e_g.html#gab40f5096a72fd7e5bda9d6b58fa37e2e',1,'turbojpeg.h']]], + ['tjcompressfromyuvplanes_119',['tjCompressFromYUVPlanes',['../group___turbo_j_p_e_g.html#ga29ec5dfbd2d84b8724e951d6fa0d5d9e',1,'turbojpeg.h']]], + ['tjdecodeyuv_120',['tjDecodeYUV',['../group___turbo_j_p_e_g.html#ga97c2cedc1e2bade15a84164c94e503c1',1,'turbojpeg.h']]], + ['tjdecodeyuvplanes_121',['tjDecodeYUVPlanes',['../group___turbo_j_p_e_g.html#ga10e837c07fa9d25770565b237d3898d9',1,'turbojpeg.h']]], + ['tjdecompress2_122',['tjDecompress2',['../group___turbo_j_p_e_g.html#gae9eccef8b682a48f43a9117c231ed013',1,'turbojpeg.h']]], + ['tjdecompressheader3_123',['tjDecompressHeader3',['../group___turbo_j_p_e_g.html#ga0595681096bba7199cc6f3533cb25f77',1,'turbojpeg.h']]], + ['tjdecompresstoyuv2_124',['tjDecompressToYUV2',['../group___turbo_j_p_e_g.html#ga5a3093e325598c17a9f004323af6fafa',1,'turbojpeg.h']]], + ['tjdecompresstoyuvplanes_125',['tjDecompressToYUVPlanes',['../group___turbo_j_p_e_g.html#gaa59f901a5258ada5bd0185ad59368540',1,'turbojpeg.h']]], + ['tjdestroy_126',['tjDestroy',['../group___turbo_j_p_e_g.html#ga75f355fa27225ba1a4ee392c852394d2',1,'turbojpeg.h']]], + ['tjencodeyuv3_127',['tjEncodeYUV3',['../group___turbo_j_p_e_g.html#ga5d619e0a02b71e05a8dffb764f6d7a64',1,'turbojpeg.h']]], + ['tjencodeyuvplanes_128',['tjEncodeYUVPlanes',['../group___turbo_j_p_e_g.html#gae2d04c72457fe7f4d60cf78ab1b1feb1',1,'turbojpeg.h']]], + ['tjfree_129',['tjFree',['../group___turbo_j_p_e_g.html#gaea863d2da0cdb609563aabdf9196514b',1,'turbojpeg.h']]], + ['tjgeterrorcode_130',['tjGetErrorCode',['../group___turbo_j_p_e_g.html#ga414feeffbf860ebd31c745df203de410',1,'turbojpeg.h']]], + ['tjgeterrorstr2_131',['tjGetErrorStr2',['../group___turbo_j_p_e_g.html#ga1ead8574f9f39fbafc6b497124e7aafa',1,'turbojpeg.h']]], + ['tjgetscalingfactors_132',['tjGetScalingFactors',['../group___turbo_j_p_e_g.html#ga193d0977b3b9966d53a6c402e90899b1',1,'turbojpeg.h']]], + ['tjinitcompress_133',['tjInitCompress',['../group___turbo_j_p_e_g.html#ga9d63a05fc6d813f4aae06107041a37e8',1,'turbojpeg.h']]], + ['tjinitdecompress_134',['tjInitDecompress',['../group___turbo_j_p_e_g.html#ga52300eac3f3d9ef4bab303bc244f62d3',1,'turbojpeg.h']]], + ['tjinittransform_135',['tjInitTransform',['../group___turbo_j_p_e_g.html#ga928beff6ac248ceadf01089fc6b41957',1,'turbojpeg.h']]], + ['tjloadimage_136',['tjLoadImage',['../group___turbo_j_p_e_g.html#gaffbd83c375e79f5db4b5c5d8ad4466e7',1,'turbojpeg.h']]], + ['tjplaneheight_137',['tjPlaneHeight',['../group___turbo_j_p_e_g.html#ga1a209696c6a80748f20e134b3c64789f',1,'turbojpeg.h']]], + ['tjplanesizeyuv_138',['tjPlaneSizeYUV',['../group___turbo_j_p_e_g.html#gab4ab7b24f6e797d79abaaa670373961d',1,'turbojpeg.h']]], + ['tjplanewidth_139',['tjPlaneWidth',['../group___turbo_j_p_e_g.html#ga63fb66bb1e36c74008c4634360becbb1',1,'turbojpeg.h']]], + ['tjsaveimage_140',['tjSaveImage',['../group___turbo_j_p_e_g.html#ga6f445b22d8933ae4815b3370a538d879',1,'turbojpeg.h']]], + ['tjtransform_141',['tjTransform',['../group___turbo_j_p_e_g.html#ga9cb8abf4cc91881e04a0329b2270be25',1,'turbojpeg.h']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.html deleted file mode 100644 index 1605901ee1f..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.js deleted file mode 100644 index bd4f34fb9f3..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/functions_74.js +++ /dev/null @@ -1,31 +0,0 @@ -var searchData= -[ - ['tjalloc',['tjAlloc',['../group___turbo_j_p_e_g.html#gaec627dd4c5f30b7a775a7aea3bec5d83',1,'turbojpeg.h']]], - ['tjbufsize',['tjBufSize',['../group___turbo_j_p_e_g.html#ga67ac12fee79073242cb216e07c9f1f90',1,'turbojpeg.h']]], - ['tjbufsizeyuv2',['tjBufSizeYUV2',['../group___turbo_j_p_e_g.html#ga2be2b9969d4df9ecce9b05deed273194',1,'turbojpeg.h']]], - ['tjcompress2',['tjCompress2',['../group___turbo_j_p_e_g.html#gafbdce0112fd78fd38efae841443a9bcf',1,'turbojpeg.h']]], - ['tjcompressfromyuv',['tjCompressFromYUV',['../group___turbo_j_p_e_g.html#ga7622a459b79aa1007e005b58783f875b',1,'turbojpeg.h']]], - ['tjcompressfromyuvplanes',['tjCompressFromYUVPlanes',['../group___turbo_j_p_e_g.html#ga29ec5dfbd2d84b8724e951d6fa0d5d9e',1,'turbojpeg.h']]], - ['tjdecodeyuv',['tjDecodeYUV',['../group___turbo_j_p_e_g.html#ga70abbf38f77a26fd6da8813bef96f695',1,'turbojpeg.h']]], - ['tjdecodeyuvplanes',['tjDecodeYUVPlanes',['../group___turbo_j_p_e_g.html#ga10e837c07fa9d25770565b237d3898d9',1,'turbojpeg.h']]], - ['tjdecompress2',['tjDecompress2',['../group___turbo_j_p_e_g.html#gae9eccef8b682a48f43a9117c231ed013',1,'turbojpeg.h']]], - ['tjdecompressheader3',['tjDecompressHeader3',['../group___turbo_j_p_e_g.html#ga0595681096bba7199cc6f3533cb25f77',1,'turbojpeg.h']]], - ['tjdecompresstoyuv2',['tjDecompressToYUV2',['../group___turbo_j_p_e_g.html#ga04d1e839ff9a0860dd1475cff78d3364',1,'turbojpeg.h']]], - ['tjdecompresstoyuvplanes',['tjDecompressToYUVPlanes',['../group___turbo_j_p_e_g.html#gaa59f901a5258ada5bd0185ad59368540',1,'turbojpeg.h']]], - ['tjdestroy',['tjDestroy',['../group___turbo_j_p_e_g.html#ga75f355fa27225ba1a4ee392c852394d2',1,'turbojpeg.h']]], - ['tjencodeyuv3',['tjEncodeYUV3',['../group___turbo_j_p_e_g.html#gac519b922cdf446e97d0cdcba513636bf',1,'turbojpeg.h']]], - ['tjencodeyuvplanes',['tjEncodeYUVPlanes',['../group___turbo_j_p_e_g.html#gae2d04c72457fe7f4d60cf78ab1b1feb1',1,'turbojpeg.h']]], - ['tjfree',['tjFree',['../group___turbo_j_p_e_g.html#gaea863d2da0cdb609563aabdf9196514b',1,'turbojpeg.h']]], - ['tjgeterrorcode',['tjGetErrorCode',['../group___turbo_j_p_e_g.html#ga414feeffbf860ebd31c745df203de410',1,'turbojpeg.h']]], - ['tjgeterrorstr2',['tjGetErrorStr2',['../group___turbo_j_p_e_g.html#ga1ead8574f9f39fbafc6b497124e7aafa',1,'turbojpeg.h']]], - ['tjgetscalingfactors',['tjGetScalingFactors',['../group___turbo_j_p_e_g.html#gac3854476006b10787bd128f7ede48057',1,'turbojpeg.h']]], - ['tjinitcompress',['tjInitCompress',['../group___turbo_j_p_e_g.html#ga9d63a05fc6d813f4aae06107041a37e8',1,'turbojpeg.h']]], - ['tjinitdecompress',['tjInitDecompress',['../group___turbo_j_p_e_g.html#ga52300eac3f3d9ef4bab303bc244f62d3',1,'turbojpeg.h']]], - ['tjinittransform',['tjInitTransform',['../group___turbo_j_p_e_g.html#ga928beff6ac248ceadf01089fc6b41957',1,'turbojpeg.h']]], - ['tjloadimage',['tjLoadImage',['../group___turbo_j_p_e_g.html#gaffbd83c375e79f5db4b5c5d8ad4466e7',1,'turbojpeg.h']]], - ['tjplaneheight',['tjPlaneHeight',['../group___turbo_j_p_e_g.html#ga1a209696c6a80748f20e134b3c64789f',1,'turbojpeg.h']]], - ['tjplanesizeyuv',['tjPlaneSizeYUV',['../group___turbo_j_p_e_g.html#gab4ab7b24f6e797d79abaaa670373961d',1,'turbojpeg.h']]], - ['tjplanewidth',['tjPlaneWidth',['../group___turbo_j_p_e_g.html#ga63fb66bb1e36c74008c4634360becbb1',1,'turbojpeg.h']]], - ['tjsaveimage',['tjSaveImage',['../group___turbo_j_p_e_g.html#ga6f445b22d8933ae4815b3370a538d879',1,'turbojpeg.h']]], - ['tjtransform',['tjTransform',['../group___turbo_j_p_e_g.html#ga9cb8abf4cc91881e04a0329b2270be25',1,'turbojpeg.h']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.html b/third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.html new file mode 100644 index 00000000000..5c10318cac1 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.js new file mode 100644 index 00000000000..b4e000d7cea --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/groups_0.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['turbojpeg_201',['TurboJPEG',['../group___turbo_j_p_e_g.html',1,'']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.html deleted file mode 100644 index a1695607519..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.js deleted file mode 100644 index 27d4ffb254f..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/groups_74.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['turbojpeg',['TurboJPEG',['../group___turbo_j_p_e_g.html',1,'']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/mag_sel.png b/third-party/mozjpeg/mozjpeg/doc/html/search/mag_sel.png deleted file mode 100644 index 81f6040a2092402b4d98f9ffa8855d12a0d4ca17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 563 zcmV-30?hr1P)zxx&tqG15pu7)IiiXFflOc2k;dXd>%13GZAy? zRz!q0=|E6a6vV)&ZBS~G9oe0kbqyw1*gvY`{Pop2oKq#FlzgXt@Xh-7fxh>}`Fxg> z$%N%{$!4=5nM{(;=c!aG1Ofr^Do{u%Ih{^&Fc@H2)+a-?TBXrw5DW&z%Nb6mQ!L9O zl}b@6mB?f=tX3;#vl)}ggh(Vpyh(IK z(Mb0D{l{U$FsRjP;!{($+bsaaVi8T#1c0V#qEIOCYa9@UVLV`f__E81L;?WEaRA;Y zUH;rZ;vb;mk7JX|$=i3O~&If0O@oZfLg8gfIjW=dcBsz;gI=!{-r4# z4%6v$&~;q^j7Fo67yJ(NJWuX+I~I!tj^nW3?}^9bq|<3^+vapS5sgM^x7!cs(+mMT z&y%j};&~po+YO)3hoUH4E*E;e9>?R6SS&`X)p`njycAVcg{rEb41T{~Hk(bl-7eSb zmFxA2uIqo#@R?lKm50ND`~6Nfn|-b1|L6O98vt3Tx@gKz#isxO002ovPDHLkV1kyW B_l^Jn diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/mag_sel.svg b/third-party/mozjpeg/mozjpeg/doc/html/search/mag_sel.svg new file mode 100644 index 00000000000..03626f64a02 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/mag_sel.svg @@ -0,0 +1,74 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/nomatches.html b/third-party/mozjpeg/mozjpeg/doc/html/search/nomatches.html index b1ded27e9ad..4377320895b 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/nomatches.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/nomatches.html @@ -1,4 +1,4 @@ - + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/search.css b/third-party/mozjpeg/mozjpeg/doc/html/search/search.css index 5b208eddd85..933cf088082 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/search.css +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/search.css @@ -1,99 +1,82 @@ /*---------------- Search Box */ -#FSearchBox { - float: left; -} - #MSearchBox { white-space : nowrap; - position: absolute; - float: none; - display: inline; - margin-top: 8px; - right: 0px; - width: 170px; + background: white; + border-radius: 0.65em; + box-shadow: inset 0.5px 0.5px 3px 0px #555; z-index: 102; - background-color: white; } -#MSearchBox .left -{ - display:block; - position:absolute; - left:10px; - width:20px; - height:19px; - background:url('search_l.png') no-repeat; - background-position:right; +#MSearchBox .left { + display: inline-block; + vertical-align: middle; + height: 1.4em; } #MSearchSelect { - display:block; - position:absolute; - width:20px; - height:19px; -} - -.left #MSearchSelect { - left:4px; -} - -.right #MSearchSelect { - right:5px; + display: inline-block; + vertical-align: middle; + height: 1.4em; + padding: 0 0 0 0.3em; + margin: 0; } #MSearchField { - display:block; - position:absolute; - height:19px; - background:url('search_m.png') repeat-x; + display: inline-block; + vertical-align: middle; + width: 7.5em; + height: 1.1em; + margin: 0 0.15em; + padding: 0; + line-height: 1em; border:none; - width:116px; - margin-left:20px; - padding-left:4px; color: #909090; outline: none; - font: 9pt Arial, Verdana, sans-serif; + font-family: Arial, Verdana, sans-serif; + -webkit-border-radius: 0px; + border-radius: 0px; + background: none; } -#FSearchBox #MSearchField { - margin-left:15px; -} #MSearchBox .right { - display:block; - position:absolute; - right:10px; - top:0px; - width:20px; - height:19px; - background:url('search_r.png') no-repeat; - background-position:left; + display: inline-block; + vertical-align: middle; + width: 1.4em; + height: 1.4em; } #MSearchClose { display: none; - position: absolute; - top: 4px; + font-size: inherit; background : none; border: none; - margin: 0px 4px 0px 0px; - padding: 0px 0px; + margin: 0; + padding: 0; outline: none; -} -.left #MSearchClose { - left: 6px; } -.right #MSearchClose { - right: 2px; +#MSearchCloseImg { + height: 1.4em; + padding: 0.3em; + margin: 0; } .MSearchBoxActive #MSearchField { color: #000000; } +#main-menu > li:last-child { + /* This
  • object is the parent of the search bar */ + display: flex; + justify-content: center; + align-items: center; + height: 36px; + margin-right: 1em; +} + /*---------------- Search filter selection */ #MSearchSelectWindow { @@ -102,7 +85,7 @@ left: 0; top: 0; border: 1px solid #90A5CE; background-color: #F9FAFC; - z-index: 1; + z-index: 10001; padding-top: 4px; padding-bottom: 4px; -moz-border-radius: 4px; @@ -165,6 +148,7 @@ iframe#MSearchResults { left: 0; top: 0; border: 1px solid #000; background-color: #EEF1F7; + z-index:10000; } /* ----------------------------------- */ diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/search.js b/third-party/mozjpeg/mozjpeg/doc/html/search/search.js index 409672cc562..92b609464af 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/search.js +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/search.js @@ -1,34 +1,27 @@ -// Search script generated by doxygen -// Copyright (C) 2009 by Dimitri van Heesch. +/* + @licstart The following is the entire license notice for the JavaScript code in this file. -// The code in this file is loosly based on main.js, part of Natural Docs, -// which is Copyright (C) 2003-2008 Greg Valure -// Natural Docs is licensed under the GPL. + The MIT License (MIT) -var indexSectionsWithContent = -{ - 0: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100010000011001010011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - 1: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - 2: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - 3: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100010000011001010011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - 4: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - 5: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - 6: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - 7: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" -}; - -var indexSectionNames = -{ - 0: "all", - 1: "classes", - 2: "functions", - 3: "variables", - 4: "typedefs", - 5: "enums", - 6: "enumvalues", - 7: "groups" -}; + Copyright (C) 1997-2020 by Dimitri van Heesch + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, + sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + @licend The above is the entire license notice for the JavaScript code in this file + */ function convertToId(search) { var result = ''; @@ -36,15 +29,15 @@ function convertToId(search) { var c = search.charAt(i); var cn = c.charCodeAt(0); - if (c.match(/[a-z0-9]/)) + if (c.match(/[a-z0-9\u0080-\uFFFF]/)) { result+=c; } - else if (cn<16) + else if (cn<16) { result+="_0"+cn.toString(16); } - else + else { result+="_"+cn.toString(16); } @@ -83,14 +76,14 @@ function getYPos(item) /* A class handling everything associated with the search panel. Parameters: - name - The name of the global variable that will be + name - The name of the global variable that will be storing this instance. Is needed to be able to set timeouts. resultPath - path to use for external files */ function SearchBox(name, resultsPath, inFrame, label) { if (!name || !resultsPath) { alert("Missing parameters to SearchBox."); } - + // ---------- Instance variables this.name = name; this.resultsPath = resultsPath; @@ -167,7 +160,7 @@ function SearchBox(name, resultsPath, inFrame, label) } // stop selection hide timer - if (this.hideTimeout) + if (this.hideTimeout) { clearTimeout(this.hideTimeout); this.hideTimeout=0; @@ -196,7 +189,7 @@ function SearchBox(name, resultsPath, inFrame, label) if (e.shiftKey==1) { this.OnSearchSelectShow(); - var win=this.DOMSearchSelectWindow(); + var win=this.DOMSearchSelectWindow(); for (i=0;i 1) // surrogate pair { - hexCode="0"+code.toString(16); - } - else - { - hexCode=code.toString(16); + idxChar = searchValue.substr(0, 2); } var resultsPage; var resultsPageWithSearch; var hasResultsPage; - if (indexSectionsWithContent[this.searchIndex].charAt(code) == '1') + var idx = indexSectionsWithContent[this.searchIndex].indexOf(idxChar); + if (idx!=-1) { + var hexCode=idx.toString(16); resultsPage = this.resultsPath + '/' + indexSectionNames[this.searchIndex] + '_' + hexCode + '.html'; resultsPageWithSearch = resultsPage+'?'+escape(searchValue); hasResultsPage = true; @@ -368,13 +358,13 @@ function SearchBox(name, resultsPath, inFrame, label) hasResultsPage = false; } - window.frames.MSearchResults.location = resultsPageWithSearch; + window.frames.MSearchResults.location = resultsPageWithSearch; var domPopupSearchResultsWindow = this.DOMPopupSearchResultsWindow(); if (domPopupSearchResultsWindow.style.display!='block') { var domSearchBox = this.DOMSearchBox(); - this.DOMSearchClose().style.display = 'inline'; + this.DOMSearchClose().style.display = 'inline-block'; if (this.insideFrame) { var domPopupSearchResults = this.DOMPopupSearchResults(); @@ -402,12 +392,12 @@ function SearchBox(name, resultsPath, inFrame, label) // -------- Activation Functions - // Activates or deactivates the search panel, resetting things to - // their default values if necessary. + // Activates or deactivates the search panel, resetting things to + // their default values if necessary. this.Activate = function(isActive) { if (isActive || // open it - this.DOMPopupSearchResultsWindow().style.display == 'block' + this.DOMPopupSearchResultsWindow().style.display == 'block' ) { this.DOMSearchBox().className = 'MSearchBoxActive'; @@ -415,8 +405,8 @@ function SearchBox(name, resultsPath, inFrame, label) var searchField = this.DOMSearchField(); if (searchField.value == this.searchLabel) // clear "Search" term upon entry - { - searchField.value = ''; + { + searchField.value = ''; this.searchActive = true; } } @@ -455,12 +445,12 @@ function SearchResults(name) } if (element.nodeName == 'DIV' && element.hasChildNodes()) - { - element = element.firstChild; + { + element = element.firstChild; } else if (element.nextSibling) - { - element = element.nextSibling; + { + element = element.nextSibling; } else { @@ -471,8 +461,8 @@ function SearchResults(name) while (element && element!=parentElement && !element.nextSibling); if (element && element!=parentElement) - { - element = element.nextSibling; + { + element = element.nextSibling; } } } @@ -525,7 +515,7 @@ function SearchResults(name) var rowMatchName = row.id.toLowerCase(); rowMatchName = rowMatchName.replace(/^sr\d*_/, ''); // strip 'sr123_' - if (search.length<=rowMatchName.length && + if (search.length<=rowMatchName.length && rowMatchName.substr(0, search.length)==search) { row.style.display = 'block'; @@ -596,7 +586,7 @@ function SearchResults(name) this.ProcessKeys = function(e) { - if (e.type == "keydown") + if (e.type == "keydown") { this.repeatOn = false; this.lastKey = e.keyCode; @@ -617,7 +607,7 @@ function SearchResults(name) return this.lastKey!=0; } - this.Nav = function(evt,itemIndex) + this.Nav = function(evt,itemIndex) { var e = (evt) ? evt : window.event; // for IE if (e.keyCode==13) return true; @@ -631,7 +621,7 @@ function SearchResults(name) { var child = this.FindChildElement(focusItem.parentNode.parentNode.id); if (child && child.style.display == 'block') // children visible - { + { var n=0; var tmpElem; while (1) // search for last child @@ -724,7 +714,7 @@ function SearchResults(name) if (elem) { elem.focus(); - } + } } else if (this.lastKey==27) // Escape { @@ -807,3 +797,18 @@ function createResults() } } +function init_search() +{ + var results = document.getElementById("MSearchSelectWindow"); + for (var key in indexSectionLabels) + { + var link = document.createElement('a'); + link.setAttribute('class','SelectItem'); + link.setAttribute('onclick','searchBox.OnSelectItem('+key+')'); + link.href='javascript:void(0)'; + link.innerHTML=' '+indexSectionLabels[key]; + results.appendChild(link); + } + searchBox.OnSelectItem(0); +} +/* @license-end */ diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/search_l.png b/third-party/mozjpeg/mozjpeg/doc/html/search/search_l.png index c872f4da4a01d0754f923e6c94fd8159c0621bd1..fd5f7daa41a4c79b4ae9bea5aa7bdfb94e14084b 100644 GIT binary patch delta 490 zcmcb^vYlmuqy!5C1A|rTQ#&BVlJ4m1$iT3%pZiZD>qIj>9tNf)Z+91l|Ly%}_D@ul zsAn(n^mS!_#KX=lBk2D&{5}H%qn4+OV~EE2-brV@oB~DMzW1v2cBy@1;GO-&?&6Y~ zW{oDf18$B2lAZ#+TwkXx5}KGW%Q0X<<5h);bqD4*s<~D5&%3JN__k|%zOPw(Dc}7@ z-4ptmhf6B&&b!_FjBE9pn*uC5&60xesh+<(kE_1GagvF-8H=anvo~dTmJ2zopr03(~h8~^|S delta 547 zcmV+;0^I$#1l$CW865@y005ATEwumu010qNS#tmY3ljhU3ljkVnw%JsF)n`r;z>k7 zRCwB~R6VQOP#AvB$vH7i{6H{96zot$7cZT<7246EF5Np6N}+$IbiG6Wg#87A+NFaX z+=_^xM1#gCtshC=E{%9^uQX_%?YwXvo{#q&MnpJ8uh(O?ZRc&~_1%^SsPxGh+H0ARIRr6-fgr(&`AvRbVopU=ZE3`i-#IY(T(03h1! zHlI$XuT0Z?QLoowSr&9%hm;bRKr9vulZf8dYBk-mEEt9XAp|Z3_dI{ESuU5KManqm zxCT53f<~cGcyz6@BcYV?X(p55QD*n|()axbFP@uoRaLW*RmK>?FuWV`8P(_JTuM0x za9oSH>v7gH5q0+a{n6^xr4Z4#^?DtIVF)6+o={Pgua4vV-0gPwAK-~Z;>U8i{O&jo zeBVD>zu$Ij!bYR#AUxE%gx1-^Km_jxcFjAyeMMJ1h6Nkljt z4md6I&Tj)?DTMgwd7j(v_urRFrN_}zR8Fdh=h=-k9M$rFl_8(j zC{hPbXF zRCwB?)W514K@j&X?z2*SxFI6-@HT2E2K=9X9%PbEK*!TBw&g( zDMC;|A)uGlRkOS9vd-?zNs%bR4d$w+ox_iFnE8fvIvv7^5<(>Te12Li7C)9srCzmK z{ZcNM{YIl9j{DePFgOWiS%!ZkNe~2q@H}s`*=&ATmUUaJ)!sCl&0hz|_x+QQl=6Uu zVTk2&N#pTY#&P_?w(aMRQc9$0iYSV(SS&Cc4u$Kw?`yT%O{>+4 zG{)E|2aGW&iUJ~nuIn%i1G!udhGD2u%BS=B{Aa)4f2H7o#TWx)44QwYp-?EGQmLR` zuWJBatk>&D4~C9GRaIe{CMuN*Y}>YiAb55*w8!?7RjXAdgm|nRU-P-8>pCpUg0Abk z1Egu%*;6Ts0@E~^VHnGcRy)T2PIh+{L`2}63nKceT!Tm{5r*N0h`wJn(Qdbc=XpI< zRejI}C8Z?JIZY;$MYn(3eL_S~E?G$kf%STwOsCU#LWpkwpzRN{EIZ`sU-={YlWop9 zR;!g54u_wDAb8zwx6^y+C!%`@5g|=eaLy6Ov2fB#XE uB-n1ZVH8E5Ip=Q~W4DguM8|!<2LNApMvO2l*qs0X002ovPDBK*LSTZi=Kr7o diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/searchdata.js b/third-party/mozjpeg/mozjpeg/doc/html/search/searchdata.js new file mode 100644 index 00000000000..332a8aa4890 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/searchdata.js @@ -0,0 +1,36 @@ +var indexSectionsWithContent = +{ + 0: "cdhnortwxy", + 1: "t", + 2: "t", + 3: "cdhnortwxy", + 4: "t", + 5: "t", + 6: "t", + 7: "t" +}; + +var indexSectionNames = +{ + 0: "all", + 1: "classes", + 2: "functions", + 3: "variables", + 4: "typedefs", + 5: "enums", + 6: "enumvalues", + 7: "groups" +}; + +var indexSectionLabels = +{ + 0: "All", + 1: "Data Structures", + 2: "Functions", + 3: "Variables", + 4: "Typedefs", + 5: "Enumerations", + 6: "Enumerator", + 7: "Modules" +}; + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.html b/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.html new file mode 100644 index 00000000000..b66f0a7b911 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.js new file mode 100644 index 00000000000..bad1a20095f --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_0.js @@ -0,0 +1,5 @@ +var searchData= +[ + ['tjhandle_160',['tjhandle',['../group___turbo_j_p_e_g.html#ga758d2634ecb4949de7815cba621f5763',1,'turbojpeg.h']]], + ['tjtransform_161',['tjtransform',['../group___turbo_j_p_e_g.html#ga504805ec0161f1b505397ca0118bf8fd',1,'turbojpeg.h']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.html deleted file mode 100644 index b2f6d2a0988..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.js deleted file mode 100644 index 85b00f5ee29..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/typedefs_74.js +++ /dev/null @@ -1,5 +0,0 @@ -var searchData= -[ - ['tjhandle',['tjhandle',['../group___turbo_j_p_e_g.html#ga758d2634ecb4949de7815cba621f5763',1,'turbojpeg.h']]], - ['tjtransform',['tjtransform',['../group___turbo_j_p_e_g.html#gaa29f3189c41be12ec5dee7caec318a31',1,'turbojpeg.h']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.html new file mode 100644 index 00000000000..2edd1114bd1 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.js new file mode 100644 index 00000000000..e68263d961a --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_0.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['customfilter_142',['customFilter',['../structtjtransform.html#a0dc7697d59a7abe48afc629e96cbc1d2',1,'tjtransform']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.html new file mode 100644 index 00000000000..98b95a99c0f --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.js new file mode 100644 index 00000000000..3dc21087358 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_1.js @@ -0,0 +1,5 @@ +var searchData= +[ + ['data_143',['data',['../structtjtransform.html#a688fe8f1a8ecc12a538d9e561cf338e3',1,'tjtransform']]], + ['denom_144',['denom',['../structtjscalingfactor.html#aefbcdf3e9e62274b2d312c695f133ce3',1,'tjscalingfactor']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.html new file mode 100644 index 00000000000..3e0c5910a87 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.js new file mode 100644 index 00000000000..f0d8327c779 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_2.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['h_145',['h',['../structtjregion.html#aecefc45a26f4d8b60dd4d825c1710115',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.html new file mode 100644 index 00000000000..7867da33244 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.js new file mode 100644 index 00000000000..e7192581f85 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_3.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['num_146',['num',['../structtjscalingfactor.html#a9b011e57f981ee23083e2c1aa5e640ec',1,'tjscalingfactor']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.html new file mode 100644 index 00000000000..732dee2e2f5 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.js new file mode 100644 index 00000000000..265062340ea --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_4.js @@ -0,0 +1,5 @@ +var searchData= +[ + ['op_147',['op',['../structtjtransform.html#a2525aab4ba6978a1c273f74fef50e498',1,'tjtransform']]], + ['options_148',['options',['../structtjtransform.html#ac0e74655baa4402209a21e1ae481c8f6',1,'tjtransform']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.html new file mode 100644 index 00000000000..ad9b545638c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.js new file mode 100644 index 00000000000..2639dfd3637 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_5.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['r_149',['r',['../structtjtransform.html#ac324e5e442abec8a961e5bf219db12cf',1,'tjtransform']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.html new file mode 100644 index 00000000000..1cf2da1954a --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.js new file mode 100644 index 00000000000..50b3fd761f9 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6.js @@ -0,0 +1,10 @@ +var searchData= +[ + ['tjalphaoffset_150',['tjAlphaOffset',['../group___turbo_j_p_e_g.html#ga5af0ab065feefd526debf1e20c43e837',1,'turbojpeg.h']]], + ['tjblueoffset_151',['tjBlueOffset',['../group___turbo_j_p_e_g.html#ga84e2e35d3f08025f976ec1ec53693dea',1,'turbojpeg.h']]], + ['tjgreenoffset_152',['tjGreenOffset',['../group___turbo_j_p_e_g.html#ga82d6e35da441112a411da41923c0ba2f',1,'turbojpeg.h']]], + ['tjmcuheight_153',['tjMCUHeight',['../group___turbo_j_p_e_g.html#gabd247bb9fecb393eca57366feb8327bf',1,'turbojpeg.h']]], + ['tjmcuwidth_154',['tjMCUWidth',['../group___turbo_j_p_e_g.html#ga9e61e7cd47a15a173283ba94e781308c',1,'turbojpeg.h']]], + ['tjpixelsize_155',['tjPixelSize',['../group___turbo_j_p_e_g.html#gad77cf8fe5b2bfd3cb3f53098146abb4c',1,'turbojpeg.h']]], + ['tjredoffset_156',['tjRedOffset',['../group___turbo_j_p_e_g.html#gadd9b446742ac8a3923f7992c7988fea8',1,'turbojpeg.h']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.html deleted file mode 100644 index 422085c1279..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.js deleted file mode 100644 index 7b058da46f3..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_63.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['customfilter',['customFilter',['../structtjtransform.html#a43ee1bcdd2a8d7249a756774f78793c1',1,'tjtransform']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.html deleted file mode 100644 index df4414b92ea..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.js deleted file mode 100644 index e19a0501696..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_64.js +++ /dev/null @@ -1,5 +0,0 @@ -var searchData= -[ - ['data',['data',['../structtjtransform.html#a688fe8f1a8ecc12a538d9e561cf338e3',1,'tjtransform']]], - ['denom',['denom',['../structtjscalingfactor.html#aefbcdf3e9e62274b2d312c695f133ce3',1,'tjscalingfactor']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.html deleted file mode 100644 index 2f0a862b90b..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.js deleted file mode 100644 index 7b17e974cd2..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_68.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['h',['h',['../structtjregion.html#aecefc45a26f4d8b60dd4d825c1710115',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.html deleted file mode 100644 index 2eb4def979b..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.js deleted file mode 100644 index 83faa1343fb..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6e.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['num',['num',['../structtjscalingfactor.html#a9b011e57f981ee23083e2c1aa5e640ec',1,'tjscalingfactor']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.html deleted file mode 100644 index f06e2e0f4e3..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.js deleted file mode 100644 index 1cca83243bb..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_6f.js +++ /dev/null @@ -1,5 +0,0 @@ -var searchData= -[ - ['op',['op',['../structtjtransform.html#a2525aab4ba6978a1c273f74fef50e498',1,'tjtransform']]], - ['options',['options',['../structtjtransform.html#ac0e74655baa4402209a21e1ae481c8f6',1,'tjtransform']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.html new file mode 100644 index 00000000000..ab2db0b895f --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.js new file mode 100644 index 00000000000..cd4a680051b --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_7.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['w_157',['w',['../structtjregion.html#ab6eb73ceef584fc23c8c8097926dce42',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.html deleted file mode 100644 index 8a4ee7bb3f4..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.js deleted file mode 100644 index 01cde35e1eb..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_72.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['r',['r',['../structtjtransform.html#ac324e5e442abec8a961e5bf219db12cf',1,'tjtransform']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.html deleted file mode 100644 index 1665fb806e1..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.js deleted file mode 100644 index 2d20942e641..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_74.js +++ /dev/null @@ -1,10 +0,0 @@ -var searchData= -[ - ['tjalphaoffset',['tjAlphaOffset',['../group___turbo_j_p_e_g.html#ga5af0ab065feefd526debf1e20c43e837',1,'turbojpeg.h']]], - ['tjblueoffset',['tjBlueOffset',['../group___turbo_j_p_e_g.html#ga84e2e35d3f08025f976ec1ec53693dea',1,'turbojpeg.h']]], - ['tjgreenoffset',['tjGreenOffset',['../group___turbo_j_p_e_g.html#ga82d6e35da441112a411da41923c0ba2f',1,'turbojpeg.h']]], - ['tjmcuheight',['tjMCUHeight',['../group___turbo_j_p_e_g.html#gabd247bb9fecb393eca57366feb8327bf',1,'turbojpeg.h']]], - ['tjmcuwidth',['tjMCUWidth',['../group___turbo_j_p_e_g.html#ga9e61e7cd47a15a173283ba94e781308c',1,'turbojpeg.h']]], - ['tjpixelsize',['tjPixelSize',['../group___turbo_j_p_e_g.html#gad77cf8fe5b2bfd3cb3f53098146abb4c',1,'turbojpeg.h']]], - ['tjredoffset',['tjRedOffset',['../group___turbo_j_p_e_g.html#gadd9b446742ac8a3923f7992c7988fea8',1,'turbojpeg.h']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.html deleted file mode 100644 index 434c6df9ef3..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.js deleted file mode 100644 index 42670029cde..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_77.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['w',['w',['../structtjregion.html#ab6eb73ceef584fc23c8c8097926dce42',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.html deleted file mode 100644 index 602e87995b9..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.js deleted file mode 100644 index 41a27f2fc34..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_78.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['x',['x',['../structtjregion.html#a4b6a37a93997091b26a75831fa291ad9',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.html deleted file mode 100644 index 17faef9c324..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - -
    -
    Loading...
    -
    - -
    Searching...
    -
    No Matches
    - -
    - - diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.js deleted file mode 100644 index 86890a698fe..00000000000 --- a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_79.js +++ /dev/null @@ -1,4 +0,0 @@ -var searchData= -[ - ['y',['y',['../structtjregion.html#a7b3e0c24cfe87acc80e334cafdcf22c2',1,'tjregion']]] -]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.html new file mode 100644 index 00000000000..baec040fc24 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.js new file mode 100644 index 00000000000..61434f04510 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_8.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['x_158',['x',['../structtjregion.html#a4b6a37a93997091b26a75831fa291ad9',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.html b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.html new file mode 100644 index 00000000000..df2fea0f99e --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.html @@ -0,0 +1,36 @@ + + + + + + + + + +
    +
    Loading...
    +
    + +
    Searching...
    +
    No Matches
    + +
    + + diff --git a/third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.js b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.js new file mode 100644 index 00000000000..32719d415f1 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/doc/html/search/variables_9.js @@ -0,0 +1,4 @@ +var searchData= +[ + ['y_159',['y',['../structtjregion.html#a7b3e0c24cfe87acc80e334cafdcf22c2',1,'tjregion']]] +]; diff --git a/third-party/mozjpeg/mozjpeg/doc/html/ftv2splitbar.png b/third-party/mozjpeg/mozjpeg/doc/html/splitbar.png similarity index 100% rename from third-party/mozjpeg/mozjpeg/doc/html/ftv2splitbar.png rename to third-party/mozjpeg/mozjpeg/doc/html/splitbar.png diff --git a/third-party/mozjpeg/mozjpeg/doc/html/structtjregion.html b/third-party/mozjpeg/mozjpeg/doc/html/structtjregion.html index 50a9adb155e..72d49d2706b 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/structtjregion.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/structtjregion.html @@ -1,18 +1,17 @@ - + - + + TurboJPEG: tjregion Struct Reference + - @@ -22,9 +21,9 @@ - @@ -32,46 +31,28 @@
    +
    TurboJPEG -  2.0 +  2.1.4
    - + - - + + + + +
    @@ -97,22 +78,24 @@

    Data Fields

    int x - The left boundary of the cropping region. More...
    + The left boundary of the cropping region. More...
      int y - The upper boundary of the cropping region. More...
    + The upper boundary of the cropping region. More...
      int w - The width of the cropping region. More...
    + The width of the cropping region. More...
      int h - The height of the cropping region. More...
    + The height of the cropping region. More...
     

    Detailed Description

    Cropping region.

    Field Documentation

    - + +

    ◆ h

    +
    @@ -127,7 +110,9 @@ - + +

    ◆ w

    +
    @@ -142,7 +127,9 @@ - + +

    ◆ x

    +
    @@ -157,7 +144,9 @@ - + +

    ◆ y

    +
    @@ -178,9 +167,7 @@ diff --git a/third-party/mozjpeg/mozjpeg/doc/html/structtjscalingfactor.html b/third-party/mozjpeg/mozjpeg/doc/html/structtjscalingfactor.html index d7fa67b311d..1606a02cc23 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/structtjscalingfactor.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/structtjscalingfactor.html @@ -1,18 +1,17 @@ - + - + +TurboJPEG: tjscalingfactor Struct Reference + - @@ -22,9 +21,9 @@
    - @@ -32,46 +31,28 @@
    +
    TurboJPEG -  2.0 +  2.1.4
    - + - - + + + + +
    @@ -97,16 +78,18 @@

    Data Fields

    int num - Numerator. More...
    + Numerator. More...
      int denom - Denominator. More...
    + Denominator. More...
     

    Detailed Description

    Scaling factor.

    Field Documentation

    - + +

    ◆ denom

    +
    @@ -120,7 +103,9 @@ - + +

    ◆ num

    +
    @@ -140,9 +125,7 @@ diff --git a/third-party/mozjpeg/mozjpeg/doc/html/structtjtransform.html b/third-party/mozjpeg/mozjpeg/doc/html/structtjtransform.html index fcf72eebf75..c6fcb5bf2fd 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/structtjtransform.html +++ b/third-party/mozjpeg/mozjpeg/doc/html/structtjtransform.html @@ -1,18 +1,17 @@ - + - + +TurboJPEG: tjtransform Struct Reference + - @@ -22,9 +21,9 @@
    - @@ -32,46 +31,28 @@
    +
    TurboJPEG -  2.0 +  2.1.4
    - + - - + + + + +
    @@ -97,30 +78,32 @@

    Data Fields

    tjregion r - Cropping region. More...
    + Cropping region. More...
      int op - One of the transform operations. More...
    + One of the transform operations. More...
      int options - The bitwise OR of one of more of the transform options. More...
    + The bitwise OR of one of more of the transform options. More...
      void * data - Arbitrary data that can be accessed within the body of the callback function. More...
    + Arbitrary data that can be accessed within the body of the callback function. More...
      -int(* customFilter )(short *coeffs, tjregion arrayRegion, tjregion planeRegion, int componentIndex, int transformIndex, struct tjtransform *transform) - A callback function that can be used to modify the DCT coefficients after they are losslessly transformed but before they are transcoded to a new JPEG image. More...
    -  +int(* customFilter )(short *coeffs, tjregion arrayRegion, tjregion planeRegion, int componentID, int transformID, struct tjtransform *transform) + A callback function that can be used to modify the DCT coefficients after they are losslessly transformed but before they are transcoded to a new JPEG image. More...

    Detailed Description

    Lossless transform.

    Field Documentation

    - + +

    ◆ customFilter

    +
    - +
    int(* tjtransform::customFilter)(short *coeffs, tjregion arrayRegion, tjregion planeRegion, int componentIndex, int transformIndex, struct tjtransform *transform)int(* tjtransform::customFilter) (short *coeffs, tjregion arrayRegion, tjregion planeRegion, int componentID, int transformID, struct tjtransform *transform)
    @@ -132,7 +115,7 @@ coeffspointer to an array of transformed DCT coefficients. (NOTE: this pointer is not guaranteed to be valid once the callback returns, so applications wishing to hand off the DCT coefficients to another function or library should make a copy of them within the body of the callback.) arrayRegiontjregion structure containing the width and height of the array pointed to by coeffs as well as its offset relative to the component plane. TurboJPEG implementations may choose to split each component plane into multiple DCT coefficient arrays and call the callback function once for each array. planeRegiontjregion structure containing the width and height of the component plane to which coeffs belongs - componentIDID number of the component plane to which coeffs belongs (Y, Cb, and Cr have, respectively, ID's of 0, 1, and 2 in typical JPEG images.) + componentIDID number of the component plane to which coeffs belongs. (Y, Cb, and Cr have, respectively, ID's of 0, 1, and 2 in typical JPEG images.) transformIDID number of the transformed image to which coeffs belongs. This is the same as the index of the transform in the transforms array that was passed to tjTransform(). transforma pointer to a tjtransform structure that specifies the parameters and/or cropping region for this transform @@ -142,7 +125,9 @@
    - + +

    ◆ data

    +
    @@ -156,7 +141,9 @@ - + +

    ◆ op

    +
    @@ -170,7 +157,9 @@ - + +

    ◆ options

    +
    @@ -180,11 +169,13 @@
    -

    The bitwise OR of one of more of the transform options.

    +

    The bitwise OR of one of more of the transform options.

    - + +

    ◆ r

    +
    @@ -204,9 +195,7 @@ diff --git a/third-party/mozjpeg/mozjpeg/doc/html/tabs.css b/third-party/mozjpeg/mozjpeg/doc/html/tabs.css index 9cf578f23a1..85a0cd5b585 100644 --- a/third-party/mozjpeg/mozjpeg/doc/html/tabs.css +++ b/third-party/mozjpeg/mozjpeg/doc/html/tabs.css @@ -1,60 +1 @@ -.tabs, .tabs2, .tabs3 { - background-image: url('tab_b.png'); - width: 100%; - z-index: 101; - font-size: 13px; - font-family: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; -} - -.tabs2 { - font-size: 10px; -} -.tabs3 { - font-size: 9px; -} - -.tablist { - margin: 0; - padding: 0; - display: table; -} - -.tablist li { - float: left; - display: table-cell; - background-image: url('tab_b.png'); - line-height: 36px; - list-style: none; -} - -.tablist a { - display: block; - padding: 0 20px; - font-weight: bold; - background-image:url('tab_s.png'); - background-repeat:no-repeat; - background-position:right; - color: #283A5D; - text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); - text-decoration: none; - outline: none; -} - -.tabs3 .tablist a { - padding: 0 10px; -} - -.tablist a:hover { - background-image: url('tab_h.png'); - background-repeat:repeat-x; - color: #fff; - text-shadow: 0px 1px 1px rgba(0, 0, 0, 1.0); - text-decoration: none; -} - -.tablist li.current a { - background-image: url('tab_a.png'); - background-repeat:repeat-x; - color: #fff; - text-shadow: 0px 1px 1px rgba(0, 0, 0, 1.0); -} +.sm{position:relative;z-index:9999}.sm,.sm ul,.sm li{display:block;list-style:none;margin:0;padding:0;line-height:normal;direction:ltr;text-align:left;-webkit-tap-highlight-color:rgba(0,0,0,0)}.sm-rtl,.sm-rtl ul,.sm-rtl li{direction:rtl;text-align:right}.sm>li>h1,.sm>li>h2,.sm>li>h3,.sm>li>h4,.sm>li>h5,.sm>li>h6{margin:0;padding:0}.sm ul{display:none}.sm li,.sm a{position:relative}.sm a{display:block}.sm a.disabled{cursor:not-allowed}.sm:after{content:"\00a0";display:block;height:0;font:0/0 serif;clear:both;visibility:hidden;overflow:hidden}.sm,.sm *,.sm *:before,.sm *:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sm-dox{background-image:url("tab_b.png")}.sm-dox a,.sm-dox a:focus,.sm-dox a:hover,.sm-dox a:active{padding:0 12px;padding-right:43px;font-family:"Lucida Grande","Geneva","Helvetica",Arial,sans-serif;font-size:13px;font-weight:bold;line-height:36px;text-decoration:none;text-shadow:0 1px 1px rgba(255,255,255,0.9);color:#283a5d;outline:0}.sm-dox a:hover{background-image:url("tab_a.png");background-repeat:repeat-x;color:white;text-shadow:0 1px 1px black}.sm-dox a.current{color:#d23600}.sm-dox a.disabled{color:#bbb}.sm-dox a span.sub-arrow{position:absolute;top:50%;margin-top:-14px;left:auto;right:3px;width:28px;height:28px;overflow:hidden;font:bold 12px/28px monospace!important;text-align:center;text-shadow:none;background:rgba(255,255,255,0.5);-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.sm-dox a.highlighted span.sub-arrow:before{display:block;content:'-'}.sm-dox>li:first-child>a,.sm-dox>li:first-child>:not(ul) a{-moz-border-radius:5px 5px 0 0;-webkit-border-radius:5px;border-radius:5px 5px 0 0}.sm-dox>li:last-child>a,.sm-dox>li:last-child>*:not(ul) a,.sm-dox>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>*:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>*:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>*:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>*:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul{-moz-border-radius:0 0 5px 5px;-webkit-border-radius:0;border-radius:0 0 5px 5px}.sm-dox>li:last-child>a.highlighted,.sm-dox>li:last-child>*:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>*:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>*:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>*:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>*:not(ul) a.highlighted{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}.sm-dox ul{background:rgba(162,162,162,0.1)}.sm-dox ul a,.sm-dox ul a:focus,.sm-dox ul a:hover,.sm-dox ul a:active{font-size:12px;border-left:8px solid transparent;line-height:36px;text-shadow:none;background-color:white;background-image:none}.sm-dox ul a:hover{background-image:url("tab_a.png");background-repeat:repeat-x;color:white;text-shadow:0 1px 1px black}.sm-dox ul ul a,.sm-dox ul ul a:hover,.sm-dox ul ul a:focus,.sm-dox ul ul a:active{border-left:16px solid transparent}.sm-dox ul ul ul a,.sm-dox ul ul ul a:hover,.sm-dox ul ul ul a:focus,.sm-dox ul ul ul a:active{border-left:24px solid transparent}.sm-dox ul ul ul ul a,.sm-dox ul ul ul ul a:hover,.sm-dox ul ul ul ul a:focus,.sm-dox ul ul ul ul a:active{border-left:32px solid transparent}.sm-dox ul ul ul ul ul a,.sm-dox ul ul ul ul ul a:hover,.sm-dox ul ul ul ul ul a:focus,.sm-dox ul ul ul ul ul a:active{border-left:40px solid transparent}@media(min-width:768px){.sm-dox ul{position:absolute;width:12em}.sm-dox li{float:left}.sm-dox.sm-rtl li{float:right}.sm-dox ul li,.sm-dox.sm-rtl ul li,.sm-dox.sm-vertical li{float:none}.sm-dox a{white-space:nowrap}.sm-dox ul a,.sm-dox.sm-vertical a{white-space:normal}.sm-dox .sm-nowrap>li>a,.sm-dox .sm-nowrap>li>:not(ul) a{white-space:nowrap}.sm-dox{padding:0 10px;background-image:url("tab_b.png");line-height:36px}.sm-dox a span.sub-arrow{top:50%;margin-top:-2px;right:12px;width:0;height:0;border-width:4px;border-style:solid dashed dashed dashed;border-color:#283a5d transparent transparent transparent;background:transparent;-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}.sm-dox a,.sm-dox a:focus,.sm-dox a:active,.sm-dox a:hover,.sm-dox a.highlighted{padding:0 12px;background-image:url("tab_s.png");background-repeat:no-repeat;background-position:right;-moz-border-radius:0!important;-webkit-border-radius:0;border-radius:0!important}.sm-dox a:hover{background-image:url("tab_a.png");background-repeat:repeat-x;color:white;text-shadow:0 1px 1px black}.sm-dox a:hover span.sub-arrow{border-color:white transparent transparent transparent}.sm-dox a.has-submenu{padding-right:24px}.sm-dox li{border-top:0}.sm-dox>li>ul:before,.sm-dox>li>ul:after{content:'';position:absolute;top:-18px;left:30px;width:0;height:0;overflow:hidden;border-width:9px;border-style:dashed dashed solid dashed;border-color:transparent transparent #bbb transparent}.sm-dox>li>ul:after{top:-16px;left:31px;border-width:8px;border-color:transparent transparent #fff transparent}.sm-dox ul{border:1px solid #bbb;padding:5px 0;background:#fff;-moz-border-radius:5px!important;-webkit-border-radius:5px;border-radius:5px!important;-moz-box-shadow:0 5px 9px rgba(0,0,0,0.2);-webkit-box-shadow:0 5px 9px rgba(0,0,0,0.2);box-shadow:0 5px 9px rgba(0,0,0,0.2)}.sm-dox ul a span.sub-arrow{right:8px;top:50%;margin-top:-5px;border-width:5px;border-color:transparent transparent transparent #555;border-style:dashed dashed dashed solid}.sm-dox ul a,.sm-dox ul a:hover,.sm-dox ul a:focus,.sm-dox ul a:active,.sm-dox ul a.highlighted{color:#555;background-image:none;border:0!important;color:#555;background-image:none}.sm-dox ul a:hover{background-image:url("tab_a.png");background-repeat:repeat-x;color:white;text-shadow:0 1px 1px black}.sm-dox ul a:hover span.sub-arrow{border-color:transparent transparent transparent white}.sm-dox span.scroll-up,.sm-dox span.scroll-down{position:absolute;display:none;visibility:hidden;overflow:hidden;background:#fff;height:36px}.sm-dox span.scroll-up:hover,.sm-dox span.scroll-down:hover{background:#eee}.sm-dox span.scroll-up:hover span.scroll-up-arrow,.sm-dox span.scroll-up:hover span.scroll-down-arrow{border-color:transparent transparent #d23600 transparent}.sm-dox span.scroll-down:hover span.scroll-down-arrow{border-color:#d23600 transparent transparent transparent}.sm-dox span.scroll-up-arrow,.sm-dox span.scroll-down-arrow{position:absolute;top:0;left:50%;margin-left:-6px;width:0;height:0;overflow:hidden;border-width:6px;border-style:dashed dashed solid dashed;border-color:transparent transparent #555 transparent}.sm-dox span.scroll-down-arrow{top:8px;border-style:solid dashed dashed dashed;border-color:#555 transparent transparent transparent}.sm-dox.sm-rtl a.has-submenu{padding-right:12px;padding-left:24px}.sm-dox.sm-rtl a span.sub-arrow{right:auto;left:12px}.sm-dox.sm-rtl.sm-vertical a.has-submenu{padding:10px 20px}.sm-dox.sm-rtl.sm-vertical a span.sub-arrow{right:auto;left:8px;border-style:dashed solid dashed dashed;border-color:transparent #555 transparent transparent}.sm-dox.sm-rtl>li>ul:before{left:auto;right:30px}.sm-dox.sm-rtl>li>ul:after{left:auto;right:31px}.sm-dox.sm-rtl ul a.has-submenu{padding:10px 20px!important}.sm-dox.sm-rtl ul a span.sub-arrow{right:auto;left:8px;border-style:dashed solid dashed dashed;border-color:transparent #555 transparent transparent}.sm-dox.sm-vertical{padding:10px 0;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.sm-dox.sm-vertical a{padding:10px 20px}.sm-dox.sm-vertical a:hover,.sm-dox.sm-vertical a:focus,.sm-dox.sm-vertical a:active,.sm-dox.sm-vertical a.highlighted{background:#fff}.sm-dox.sm-vertical a.disabled{background-image:url("tab_b.png")}.sm-dox.sm-vertical a span.sub-arrow{right:8px;top:50%;margin-top:-5px;border-width:5px;border-style:dashed dashed dashed solid;border-color:transparent transparent transparent #555}.sm-dox.sm-vertical>li>ul:before,.sm-dox.sm-vertical>li>ul:after{display:none}.sm-dox.sm-vertical ul a{padding:10px 20px}.sm-dox.sm-vertical ul a:hover,.sm-dox.sm-vertical ul a:focus,.sm-dox.sm-vertical ul a:active,.sm-dox.sm-vertical ul a.highlighted{background:#eee}.sm-dox.sm-vertical ul a.disabled{background:#fff}} \ No newline at end of file diff --git a/third-party/mozjpeg/mozjpeg/doxygen.config b/third-party/mozjpeg/mozjpeg/doxygen.config index cb884f9d6ce..16708b03e79 100644 --- a/third-party/mozjpeg/mozjpeg/doxygen.config +++ b/third-party/mozjpeg/mozjpeg/doxygen.config @@ -1,5 +1,5 @@ PROJECT_NAME = TurboJPEG -PROJECT_NUMBER = 2.0 +PROJECT_NUMBER = 2.1.4 OUTPUT_DIRECTORY = doc/ USE_WINDOWS_ENCODING = NO OPTIMIZE_OUTPUT_FOR_C = YES diff --git a/third-party/mozjpeg/mozjpeg/example.txt b/third-party/mozjpeg/mozjpeg/example.txt index bc0ba49d292..d473aede26c 100644 --- a/third-party/mozjpeg/mozjpeg/example.txt +++ b/third-party/mozjpeg/mozjpeg/example.txt @@ -311,7 +311,7 @@ read_JPEG_file(char *filename) * We call the libjpeg API from within a separate function, because modifying * the local non-volatile jpeg_decompress_struct instance below the setjmp() * return point and then accessing the instance after setjmp() returns would - * return in undefined behavior that may potentially overwrite all or part of + * result in undefined behavior that may potentially overwrite all or part of * the structure. */ diff --git a/third-party/mozjpeg/mozjpeg/fuzz/CMakeLists.txt b/third-party/mozjpeg/mozjpeg/fuzz/CMakeLists.txt new file mode 100644 index 00000000000..9f044c6791e --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/CMakeLists.txt @@ -0,0 +1,55 @@ +if(NOT ENABLE_STATIC) + message(FATAL_ERROR "Fuzz targets require static libraries.") +endif() +if(NOT WITH_TURBOJPEG) + message(FATAL_ERROR "Fuzz targets require the TurboJPEG API library.") +endif() + +set(FUZZ_BINDIR "" CACHE PATH + "Directory into which fuzz targets should be installed") +if(NOT FUZZ_BINDIR) + message(FATAL_ERROR "FUZZ_BINDIR must be specified.") +endif() +message(STATUS "FUZZ_BINDIR = ${FUZZ_BINDIR}") + +set(FUZZ_LIBRARY "" CACHE STRING + "Path to fuzzer library or flags necessary to link with it") +if(NOT FUZZ_LIBRARY) + message(FATAL_ERROR "FUZZ_LIBRARY must be specified.") +endif() +message(STATUS "FUZZ_LIBRARY = ${FUZZ_LIBRARY}") + +enable_language(CXX) + +set(EFFECTIVE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE_UC}}") +message(STATUS "C++ Compiler flags = ${EFFECTIVE_CXX_FLAGS}") + +add_executable(cjpeg_fuzzer${FUZZER_SUFFIX} cjpeg.cc ../cdjpeg.c ../rdbmp.c + ../rdgif.c ../rdppm.c ../rdswitch.c ../rdtarga.c) +set_property(TARGET cjpeg_fuzzer${FUZZER_SUFFIX} PROPERTY COMPILE_FLAGS + ${COMPILE_FLAGS}) +target_link_libraries(cjpeg_fuzzer${FUZZER_SUFFIX} ${FUZZ_LIBRARY} jpeg-static) +install(TARGETS cjpeg_fuzzer${FUZZER_SUFFIX} RUNTIME DESTINATION + ${FUZZ_BINDIR}) + +macro(add_fuzz_target target source_file) + add_executable(${target}_fuzzer${FUZZER_SUFFIX} ${source_file}) + target_link_libraries(${target}_fuzzer${FUZZER_SUFFIX} ${FUZZ_LIBRARY} + turbojpeg-static) + install(TARGETS ${target}_fuzzer${FUZZER_SUFFIX} RUNTIME DESTINATION + ${FUZZ_BINDIR}) +endmacro() + +add_fuzz_target(compress compress.cc) + +add_fuzz_target(compress_yuv compress_yuv.cc) + +# NOTE: This target is named libjpeg_turbo_fuzzer instead of decompress_fuzzer +# in order to preserve the corpora from Google's OSS-Fuzz target for +# libjpeg-turbo, which this target replaces. +add_fuzz_target(libjpeg_turbo decompress.cc) + +add_fuzz_target(decompress_yuv decompress_yuv.cc) + +add_fuzz_target(transform transform.cc) diff --git a/third-party/mozjpeg/mozjpeg/fuzz/build.sh b/third-party/mozjpeg/mozjpeg/fuzz/build.sh new file mode 100644 index 00000000000..703302245c8 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/build.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -u +set -e + +FUZZER_SUFFIX= +if [ $# -ge 1 ]; then + FUZZER_SUFFIX="$1" + FUZZER_SUFFIX="`echo $1 | sed 's/\./_/g'`" +fi + +cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_STATIC=1 -DENABLE_SHARED=0 \ + -DCMAKE_C_FLAGS_RELWITHDEBINFO="-g -DNDEBUG" \ + -DCMAKE_CXX_FLAGS_RELWITHDEBINFO="-g -DNDEBUG" -DCMAKE_INSTALL_PREFIX=$WORK \ + -DWITH_FUZZ=1 -DFUZZ_BINDIR=$OUT -DFUZZ_LIBRARY=$LIB_FUZZING_ENGINE \ + -DFUZZER_SUFFIX="$FUZZER_SUFFIX" +make "-j$(nproc)" "--load-average=$(nproc)" +make install + +cp $SRC/compress_fuzzer_seed_corpus.zip $OUT/cjpeg_fuzzer${FUZZER_SUFFIX}_seed_corpus.zip +cp $SRC/compress_fuzzer_seed_corpus.zip $OUT/compress_fuzzer${FUZZER_SUFFIX}_seed_corpus.zip +cp $SRC/compress_fuzzer_seed_corpus.zip $OUT/compress_yuv_fuzzer${FUZZER_SUFFIX}_seed_corpus.zip +cp $SRC/decompress_fuzzer_seed_corpus.zip $OUT/libjpeg_turbo_fuzzer${FUZZER_SUFFIX}_seed_corpus.zip +cp $SRC/decompress_fuzzer_seed_corpus.zip $OUT/decompress_yuv_fuzzer${FUZZER_SUFFIX}_seed_corpus.zip +cp $SRC/decompress_fuzzer_seed_corpus.zip $OUT/transform_fuzzer${FUZZER_SUFFIX}_seed_corpus.zip diff --git a/third-party/mozjpeg/mozjpeg/fuzz/cjpeg.cc b/third-party/mozjpeg/mozjpeg/fuzz/cjpeg.cc new file mode 100644 index 00000000000..ee7b7ab8c37 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/cjpeg.cc @@ -0,0 +1,89 @@ +/* + * Copyright (C)2021 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* This fuzz target wraps cjpeg in order to test esoteric compression options + as well as the GIF and Targa readers. */ + +#define main cjpeg_main +#define CJPEG_FUZZER +extern "C" { +#include "../cjpeg.c" +} +#undef main +#undef CJPEG_FUZZER + +#include +#include + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + char filename[FILENAME_MAX] = { 0 }; + char *argv1[] = { + (char *)"cjpeg", (char *)"-dct", (char *)"float", (char *)"-memdst", + (char *)"-optimize", (char *)"-quality", (char *)"100,99,98", + (char *)"-restart", (char *)"2", (char *)"-sample", (char *)"4x1,2x2,1x2", + (char *)"-targa", NULL + }; + char *argv2[] = { + (char *)"cjpeg", (char *)"-arithmetic", (char *)"-dct", (char *)"float", + (char *)"-memdst", (char *)"-quality", (char *)"90,80,70", (char *)"-rgb", + (char *)"-sample", (char *)"2x2", (char *)"-smooth", (char *)"50", + (char *)"-targa", NULL + }; + int fd = -1; +#if defined(__has_feature) && __has_feature(memory_sanitizer) + char env[18] = "JSIMD_FORCENONE=1"; + + /* The libjpeg-turbo SIMD extensions produce false positives with + MemorySanitizer. */ + putenv(env); +#endif + + snprintf(filename, FILENAME_MAX, "/tmp/libjpeg-turbo_cjpeg_fuzz.XXXXXX"); + if ((fd = mkstemp(filename)) < 0 || write(fd, data, size) < 0) + goto bailout; + + argv1[12] = argv2[13] = filename; + + cjpeg_main(13, argv1); + cjpeg_main(14, argv2); + + argv1[12] = argv2[13] = NULL; + argv1[11] = argv2[12] = filename; + + cjpeg_main(12, argv1); + cjpeg_main(13, argv2); + +bailout: + if (fd >= 0) { + close(fd); + if (strlen(filename) > 0) unlink(filename); + } + return 0; +} diff --git a/third-party/mozjpeg/mozjpeg/fuzz/compress.cc b/third-party/mozjpeg/mozjpeg/fuzz/compress.cc new file mode 100644 index 00000000000..539932f13de --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/compress.cc @@ -0,0 +1,133 @@ +/* + * Copyright (C)2021 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include + + +#define NUMTESTS 7 +/* Private flag that triggers different TurboJPEG API behavior when fuzzing */ +#define TJFLAG_FUZZING (1 << 30) + + +struct test { + enum TJPF pf; + enum TJSAMP subsamp; + int quality; +}; + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + tjhandle handle = NULL; + unsigned char *srcBuf = NULL, *dstBuf = NULL; + int width = 0, height = 0, fd = -1, i, ti; + char filename[FILENAME_MAX] = { 0 }; + struct test tests[NUMTESTS] = { + { TJPF_RGB, TJSAMP_444, 100 }, + { TJPF_BGR, TJSAMP_422, 90 }, + { TJPF_RGBX, TJSAMP_420, 80 }, + { TJPF_BGRA, TJSAMP_411, 70 }, + { TJPF_XRGB, TJSAMP_GRAY, 60 }, + { TJPF_GRAY, TJSAMP_GRAY, 50 }, + { TJPF_CMYK, TJSAMP_440, 40 } + }; +#if defined(__has_feature) && __has_feature(memory_sanitizer) + char env[18] = "JSIMD_FORCENONE=1"; + + /* The libjpeg-turbo SIMD extensions produce false positives with + MemorySanitizer. */ + putenv(env); +#endif + + snprintf(filename, FILENAME_MAX, "/tmp/libjpeg-turbo_compress_fuzz.XXXXXX"); + if ((fd = mkstemp(filename)) < 0 || write(fd, data, size) < 0) + goto bailout; + + if ((handle = tjInitCompress()) == NULL) + goto bailout; + + for (ti = 0; ti < NUMTESTS; ti++) { + int flags = TJFLAG_FUZZING, sum = 0, pf = tests[ti].pf; + unsigned long dstSize = 0, maxBufSize; + + /* Test non-default compression options on specific iterations. */ + if (ti == 0) + flags |= TJFLAG_BOTTOMUP | TJFLAG_ACCURATEDCT; + else if (ti == 1) + flags |= TJFLAG_PROGRESSIVE; + if (ti != 2) + flags |= TJFLAG_NOREALLOC; + + /* tjLoadImage() refuses to load images larger than 1 Megapixel when + FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION is defined (yes, that's a dirty + hack), so we don't need to check the width and height here. */ + if ((srcBuf = tjLoadImage(filename, &width, 1, &height, &pf, + flags)) == NULL) + continue; + + maxBufSize = tjBufSize(width, height, tests[ti].subsamp); + if (flags & TJFLAG_NOREALLOC) { + if ((dstBuf = (unsigned char *)malloc(maxBufSize)) == NULL) + goto bailout; + } else + dstBuf = NULL; + + if (tjCompress2(handle, srcBuf, width, 0, height, pf, &dstBuf, &dstSize, + tests[ti].subsamp, tests[ti].quality, flags) == 0) { + /* Touch all of the output pixels in order to catch uninitialized reads + when using MemorySanitizer. */ + for (i = 0; i < dstSize; i++) + sum += dstBuf[i]; + } + + free(dstBuf); + dstBuf = NULL; + tjFree(srcBuf); + srcBuf = NULL; + + /* Prevent the code above from being optimized out. This test should never + be true, but the compiler doesn't know that. */ + if (sum > 255 * maxBufSize) + goto bailout; + } + +bailout: + free(dstBuf); + tjFree(srcBuf); + if (fd >= 0) { + close(fd); + if (strlen(filename) > 0) unlink(filename); + } + if (handle) tjDestroy(handle); + return 0; +} diff --git a/third-party/mozjpeg/mozjpeg/fuzz/compress_yuv.cc b/third-party/mozjpeg/mozjpeg/fuzz/compress_yuv.cc new file mode 100644 index 00000000000..021d661508e --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/compress_yuv.cc @@ -0,0 +1,148 @@ +/* + * Copyright (C)2021 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include + + +#define NUMTESTS 6 +/* Private flag that triggers different TurboJPEG API behavior when fuzzing */ +#define TJFLAG_FUZZING (1 << 30) + + +struct test { + enum TJPF pf; + enum TJSAMP subsamp; + int quality; +}; + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + tjhandle handle = NULL; + unsigned char *srcBuf = NULL, *dstBuf = NULL, *yuvBuf = NULL; + int width = 0, height = 0, fd = -1, i, ti; + char filename[FILENAME_MAX] = { 0 }; + struct test tests[NUMTESTS] = { + { TJPF_XBGR, TJSAMP_444, 100 }, + { TJPF_XRGB, TJSAMP_422, 90 }, + { TJPF_BGR, TJSAMP_420, 80 }, + { TJPF_RGB, TJSAMP_411, 70 }, + { TJPF_BGR, TJSAMP_GRAY, 60 }, + { TJPF_GRAY, TJSAMP_GRAY, 50 } + }; + char arithEnv[16] = "TJ_ARITHMETIC=0"; + char restartEnv[13] = "TJ_RESTART=0"; +#if defined(__has_feature) && __has_feature(memory_sanitizer) + char simdEnv[18] = "JSIMD_FORCENONE=1"; + + /* The libjpeg-turbo SIMD extensions produce false positives with + MemorySanitizer. */ + putenv(simdEnv); +#endif + putenv(arithEnv); + putenv(restartEnv); + + snprintf(filename, FILENAME_MAX, "/tmp/libjpeg-turbo_compress_yuv_fuzz.XXXXXX"); + if ((fd = mkstemp(filename)) < 0 || write(fd, data, size) < 0) + goto bailout; + + if ((handle = tjInitCompress()) == NULL) + goto bailout; + + for (ti = 0; ti < NUMTESTS; ti++) { + int flags = TJFLAG_FUZZING | TJFLAG_NOREALLOC, sum = 0, pf = tests[ti].pf; + unsigned long dstSize = 0, maxBufSize; + + /* Test non-default compression options on specific iterations. */ + if (ti == 0) + flags |= TJFLAG_BOTTOMUP | TJFLAG_ACCURATEDCT; + else if (ti == 1 || ti == 3) + flags |= TJFLAG_PROGRESSIVE; + if (ti == 2 || ti == 3) + arithEnv[14] = '1'; + else + arithEnv[14] = '0'; + if (ti == 1 || ti == 2) + restartEnv[11] = '2'; + else + restartEnv[11] = '0'; + + /* tjLoadImage() refuses to load images larger than 1 Megapixel when + FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION is defined (yes, that's a dirty + hack), so we don't need to check the width and height here. */ + if ((srcBuf = tjLoadImage(filename, &width, 1, &height, &pf, + flags)) == NULL) + continue; + + maxBufSize = tjBufSize(width, height, tests[ti].subsamp); + if ((dstBuf = (unsigned char *)malloc(maxBufSize)) == NULL) + goto bailout; + if ((yuvBuf = + (unsigned char *)malloc(tjBufSizeYUV2(width, 1, height, + tests[ti].subsamp))) == NULL) + goto bailout; + + if (tjEncodeYUV3(handle, srcBuf, width, 0, height, pf, yuvBuf, 1, + tests[ti].subsamp, flags) == 0 && + tjCompressFromYUV(handle, yuvBuf, width, 1, height, tests[ti].subsamp, + &dstBuf, &dstSize, tests[ti].quality, flags) == 0) { + /* Touch all of the output pixels in order to catch uninitialized reads + when using MemorySanitizer. */ + for (i = 0; i < dstSize; i++) + sum += dstBuf[i]; + } + + free(dstBuf); + dstBuf = NULL; + free(yuvBuf); + yuvBuf = NULL; + tjFree(srcBuf); + srcBuf = NULL; + + /* Prevent the code above from being optimized out. This test should never + be true, but the compiler doesn't know that. */ + if (sum > 255 * maxBufSize) + goto bailout; + } + +bailout: + free(dstBuf); + free(yuvBuf); + tjFree(srcBuf); + if (fd >= 0) { + close(fd); + if (strlen(filename) > 0) unlink(filename); + } + if (handle) tjDestroy(handle); + return 0; +} diff --git a/third-party/mozjpeg/mozjpeg/fuzz/decompress.cc b/third-party/mozjpeg/mozjpeg/fuzz/decompress.cc new file mode 100644 index 00000000000..cdc65fe9e2e --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/decompress.cc @@ -0,0 +1,107 @@ +/* + * Copyright (C)2021, 2023 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + + +#define NUMPF 4 + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + tjhandle handle = NULL; + unsigned char *dstBuf = NULL; + int width = 0, height = 0, jpegSubsamp, jpegColorspace, pfi; + /* TJPF_RGB-TJPF_BGR share the same code paths, as do TJPF_RGBX-TJPF_XRGB and + TJPF_RGBA-TJPF_ARGB. Thus, the pixel formats below should be the minimum + necessary to achieve full coverage. */ + enum TJPF pixelFormats[NUMPF] = + { TJPF_RGB, TJPF_BGRX, TJPF_GRAY, TJPF_CMYK }; +#if defined(__has_feature) && __has_feature(memory_sanitizer) + char env[18] = "JSIMD_FORCENONE=1"; + + /* The libjpeg-turbo SIMD extensions produce false positives with + MemorySanitizer. */ + putenv(env); +#endif + + if ((handle = tjInitDecompress()) == NULL) + goto bailout; + + /* We ignore the return value of tjDecompressHeader3(), because some JPEG + images may have unusual subsampling configurations that the TurboJPEG API + cannot identify but can still decompress. */ + tjDecompressHeader3(handle, data, size, &width, &height, &jpegSubsamp, + &jpegColorspace); + + /* Ignore 0-pixel images and images larger than 1 Megapixel, as Google's + OSS-Fuzz target for libjpeg-turbo did. Casting width to (uint64_t) + prevents integer overflow if width * height > INT_MAX. */ + if (width < 1 || height < 1 || (uint64_t)width * height > 1048576) + goto bailout; + + for (pfi = 0; pfi < NUMPF; pfi++) { + int pf = pixelFormats[pfi], flags = TJFLAG_LIMITSCANS, i, sum = 0; + int w = width, h = height; + + /* Test non-default decompression options on the first iteration. */ + if (pfi == 0) + flags |= TJFLAG_BOTTOMUP | TJFLAG_FASTUPSAMPLE | TJFLAG_FASTDCT; + /* Test IDCT scaling on the second iteration. */ + else if (pfi == 1) { + w = (width + 1) / 2; + h = (height + 1) / 2; + } + + if ((dstBuf = (unsigned char *)malloc(w * h * tjPixelSize[pf])) == NULL) + goto bailout; + + if (tjDecompress2(handle, data, size, dstBuf, w, 0, h, pf, flags) == 0) { + /* Touch all of the output pixels in order to catch uninitialized reads + when using MemorySanitizer. */ + for (i = 0; i < w * h * tjPixelSize[pf]; i++) + sum += dstBuf[i]; + } else + goto bailout; + + free(dstBuf); + dstBuf = NULL; + + /* Prevent the code above from being optimized out. This test should never + be true, but the compiler doesn't know that. */ + if (sum > 255 * 1048576 * tjPixelSize[pf]) + goto bailout; + } + +bailout: + free(dstBuf); + if (handle) tjDestroy(handle); + return 0; +} diff --git a/third-party/mozjpeg/mozjpeg/fuzz/decompress_yuv.cc b/third-party/mozjpeg/mozjpeg/fuzz/decompress_yuv.cc new file mode 100644 index 00000000000..a057e83e33f --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/decompress_yuv.cc @@ -0,0 +1,112 @@ +/* + * Copyright (C)2021, 2023 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + + +#define NUMPF 3 + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + tjhandle handle = NULL; + unsigned char *dstBuf = NULL, *yuvBuf = NULL; + int width = 0, height = 0, jpegSubsamp, jpegColorspace, pfi; + /* TJPF_RGB-TJPF_BGR share the same code paths, as do TJPF_RGBX-TJPF_XRGB and + TJPF_RGBA-TJPF_ARGB. Thus, the pixel formats below should be the minimum + necessary to achieve full coverage. */ + enum TJPF pixelFormats[NUMPF] = + { TJPF_BGR, TJPF_XRGB, TJPF_GRAY }; +#if defined(__has_feature) && __has_feature(memory_sanitizer) + char env[18] = "JSIMD_FORCENONE=1"; + + /* The libjpeg-turbo SIMD extensions produce false positives with + MemorySanitizer. */ + putenv(env); +#endif + + if ((handle = tjInitDecompress()) == NULL) + goto bailout; + + if (tjDecompressHeader3(handle, data, size, &width, &height, &jpegSubsamp, + &jpegColorspace) < 0) + goto bailout; + + /* Ignore 0-pixel images and images larger than 1 Megapixel. Casting width + to (uint64_t) prevents integer overflow if width * height > INT_MAX. */ + if (width < 1 || height < 1 || (uint64_t)width * height > 1048576) + goto bailout; + + for (pfi = 0; pfi < NUMPF; pfi++) { + int pf = pixelFormats[pfi], flags = TJFLAG_LIMITSCANS, i, sum = 0; + int w = width, h = height; + + /* Test non-default decompression options on the first iteration. */ + if (pfi == 0) + flags |= TJFLAG_BOTTOMUP | TJFLAG_FASTUPSAMPLE | TJFLAG_FASTDCT; + /* Test IDCT scaling on the second iteration. */ + else if (pfi == 1) { + w = (width + 3) / 4; + h = (height + 3) / 4; + } + + if ((dstBuf = (unsigned char *)malloc(w * h * tjPixelSize[pf])) == NULL) + goto bailout; + if ((yuvBuf = + (unsigned char *)malloc(tjBufSizeYUV2(w, 1, h, jpegSubsamp))) == NULL) + goto bailout; + + if (tjDecompressToYUV2(handle, data, size, yuvBuf, w, 1, h, flags) == 0 && + tjDecodeYUV(handle, yuvBuf, 1, jpegSubsamp, dstBuf, w, 0, h, pf, + flags) == 0) { + /* Touch all of the output pixels in order to catch uninitialized reads + when using MemorySanitizer. */ + for (i = 0; i < w * h * tjPixelSize[pf]; i++) + sum += dstBuf[i]; + } else + goto bailout; + + free(dstBuf); + dstBuf = NULL; + free(yuvBuf); + yuvBuf = NULL; + + /* Prevent the code above from being optimized out. This test should never + be true, but the compiler doesn't know that. */ + if (sum > 255 * 1048576 * tjPixelSize[pf]) + goto bailout; + } + +bailout: + free(dstBuf); + free(yuvBuf); + if (handle) tjDestroy(handle); + return 0; +} diff --git a/third-party/mozjpeg/mozjpeg/fuzz/transform.cc b/third-party/mozjpeg/mozjpeg/fuzz/transform.cc new file mode 100644 index 00000000000..6fd94b4d50b --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/fuzz/transform.cc @@ -0,0 +1,162 @@ +/* + * Copyright (C)2021, 2023 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + tjhandle handle = NULL; + unsigned char *dstBufs[1] = { NULL }; + unsigned long dstSizes[1] = { 0 }, maxBufSize; + int width = 0, height = 0, jpegSubsamp, jpegColorspace, i; + tjtransform transforms[1]; +#if defined(__has_feature) && __has_feature(memory_sanitizer) + char env[18] = "JSIMD_FORCENONE=1"; + + /* The libjpeg-turbo SIMD extensions produce false positives with + MemorySanitizer. */ + putenv(env); +#endif + + if ((handle = tjInitTransform()) == NULL) + goto bailout; + + /* We ignore the return value of tjDecompressHeader3(), because some JPEG + images may have unusual subsampling configurations that the TurboJPEG API + cannot identify but can still transform. */ + tjDecompressHeader3(handle, data, size, &width, &height, &jpegSubsamp, + &jpegColorspace); + + /* Ignore 0-pixel images and images larger than 1 Megapixel. Casting width + to (uint64_t) prevents integer overflow if width * height > INT_MAX. */ + if (width < 1 || height < 1 || (uint64_t)width * height > 1048576) + goto bailout; + + if (jpegSubsamp < 0 || jpegSubsamp >= TJ_NUMSAMP) + jpegSubsamp = TJSAMP_444; + + memset(&transforms[0], 0, sizeof(tjtransform)); + + transforms[0].op = TJXOP_NONE; + transforms[0].options = TJXOPT_PROGRESSIVE | TJXOPT_COPYNONE; + dstBufs[0] = (unsigned char *)malloc(tjBufSize(width, height, jpegSubsamp)); + if (!dstBufs[0]) + goto bailout; + + maxBufSize = tjBufSize(width, height, jpegSubsamp); + + if (tjTransform(handle, data, size, 1, dstBufs, dstSizes, transforms, + TJFLAG_LIMITSCANS | TJFLAG_NOREALLOC) == 0) { + /* Touch all of the output pixels in order to catch uninitialized reads + when using MemorySanitizer. */ + int sum = 0; + + for (i = 0; i < dstSizes[0]; i++) + sum += dstBufs[0][i]; + + /* Prevent the code above from being optimized out. This test should + never be true, but the compiler doesn't know that. */ + if (sum > 255 * maxBufSize) + goto bailout; + } + + free(dstBufs[0]); + dstBufs[0] = NULL; + + transforms[0].r.w = (height + 1) / 2; + transforms[0].r.h = (width + 1) / 2; + transforms[0].op = TJXOP_TRANSPOSE; + transforms[0].options = TJXOPT_GRAY | TJXOPT_CROP | TJXOPT_COPYNONE; + dstBufs[0] = + (unsigned char *)malloc(tjBufSize((height + 1) / 2, (width + 1) / 2, + jpegSubsamp)); + if (!dstBufs[0]) + goto bailout; + + maxBufSize = tjBufSize((height + 1) / 2, (width + 1) / 2, jpegSubsamp); + + if (tjTransform(handle, data, size, 1, dstBufs, dstSizes, transforms, + TJFLAG_LIMITSCANS | TJFLAG_NOREALLOC) == 0) { + int sum = 0; + + for (i = 0; i < dstSizes[0]; i++) + sum += dstBufs[0][i]; + + if (sum > 255 * maxBufSize) + goto bailout; + } + + free(dstBufs[0]); + dstBufs[0] = NULL; + + transforms[0].op = TJXOP_ROT90; + transforms[0].options = TJXOPT_TRIM; + dstBufs[0] = (unsigned char *)malloc(tjBufSize(height, width, jpegSubsamp)); + if (!dstBufs[0]) + goto bailout; + + maxBufSize = tjBufSize(height, width, jpegSubsamp); + + if (tjTransform(handle, data, size, 1, dstBufs, dstSizes, transforms, + TJFLAG_LIMITSCANS | TJFLAG_NOREALLOC) == 0) { + int sum = 0; + + for (i = 0; i < dstSizes[0]; i++) + sum += dstBufs[0][i]; + + if (sum > 255 * maxBufSize) + goto bailout; + } + + free(dstBufs[0]); + dstBufs[0] = NULL; + + transforms[0].op = TJXOP_NONE; + transforms[0].options = TJXOPT_PROGRESSIVE; + dstSizes[0] = 0; + + if (tjTransform(handle, data, size, 1, dstBufs, dstSizes, transforms, + TJFLAG_LIMITSCANS) == 0) { + int sum = 0; + + for (i = 0; i < dstSizes[0]; i++) + sum += dstBufs[0][i]; + + if (sum > 255 * maxBufSize) + goto bailout; + } + +bailout: + free(dstBufs[0]); + if (handle) tjDestroy(handle); + return 0; +} diff --git a/third-party/mozjpeg/mozjpeg/java/README b/third-party/mozjpeg/mozjpeg/java/README index 88ddc3bdda6..5af1e31ddc4 100644 --- a/third-party/mozjpeg/mozjpeg/java/README +++ b/third-party/mozjpeg/mozjpeg/java/README @@ -38,7 +38,7 @@ Installation Directory ---------------------- The TurboJPEG Java Wrapper will look for the TurboJPEG JNI library -(libturbojpeg.so, libturbojpeg.jnilib, or turbojpeg.dll) in the system library +(libturbojpeg.so, libturbojpeg.dylib, or turbojpeg.dll) in the system library paths or in any paths specified in LD_LIBRARY_PATH (Un*x), DYLD_LIBRARY_PATH (Mac), or PATH (Windows.) Failing this, on Un*x and Mac systems, the wrapper will look for the JNI library under the library directory configured when diff --git a/third-party/mozjpeg/mozjpeg/java/TJBench.java b/third-party/mozjpeg/mozjpeg/java/TJBench.java index e43645eafe5..3cdee138aec 100644 --- a/third-party/mozjpeg/mozjpeg/java/TJBench.java +++ b/third-party/mozjpeg/mozjpeg/java/TJBench.java @@ -1,5 +1,6 @@ /* - * Copyright (C)2009-2014, 2016-2019 D. R. Commander. All Rights Reserved. + * Copyright (C)2009-2014, 2016-2019, 2021, 2023 D. R. Commander. + * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,7 +37,7 @@ final class TJBench { private TJBench() {} - private static int flags = 0, quiet = 0, pf = TJ.PF_BGR, yuvPad = 1; + private static int flags = 0, quiet = 0, pf = TJ.PF_BGR, yuvAlign = 1; private static boolean compOnly, decompOnly, doTile, doYUV, write = true; static final String[] PIXFORMATSTR = { @@ -191,7 +192,7 @@ static void decomp(byte[] srcBuf, byte[][] jpegBuf, int[] jpegSize, int width = doTile ? tilew : scaledw; int height = doTile ? tileh : scaledh; - yuvImage = new YUVImage(width, yuvPad, height, subsamp); + yuvImage = new YUVImage(width, yuvAlign, height, subsamp); Arrays.fill(yuvImage.getBuf(), (byte)127); } @@ -211,7 +212,8 @@ static void decomp(byte[] srcBuf, byte[][] jpegBuf, int[] jpegSize, tjd.setSourceImage(jpegBuf[tile], jpegSize[tile]); } catch (TJException e) { handleTJException(e); } if (doYUV) { - yuvImage.setBuf(yuvImage.getBuf(), width, yuvPad, height, subsamp); + yuvImage.setBuf(yuvImage.getBuf(), width, yuvAlign, height, + subsamp); try { tjd.decompressToYUV(yuvImage, flags); } catch (TJException e) { handleTJException(e); } @@ -371,7 +373,7 @@ static void fullTest(byte[] srcBuf, int w, int h, int subsamp, int jpegQual, tjc.setSubsamp(subsamp); if (doYUV) { - yuvImage = new YUVImage(tilew, yuvPad, tileh, subsamp); + yuvImage = new YUVImage(tilew, yuvAlign, tileh, subsamp); Arrays.fill(yuvImage.getBuf(), (byte)127); } @@ -392,7 +394,7 @@ static void fullTest(byte[] srcBuf, int w, int h, int subsamp, int jpegQual, if (doYUV) { double startEncode = getTime(); - yuvImage.setBuf(yuvImage.getBuf(), width, yuvPad, height, + yuvImage.setBuf(yuvImage.getBuf(), width, yuvAlign, height, subsamp); tjc.encodeYUV(yuvImage, flags); if (iter >= 0) @@ -497,7 +499,7 @@ static void decompTest(String fileName) throws Exception { // Original image int w = 0, h = 0, ntilesw = 1, ntilesh = 1, subsamp = -1, cs = -1; // Transformed image - int tw, th, ttilew, ttileh, tntilesw, tntilesh, tsubsamp; + int minTile, tw, th, ttilew, ttileh, tntilesw, tntilesh, tsubsamp; FileInputStream fis = new FileInputStream(fileName); if (fis.getChannel().size() > (long)Integer.MAX_VALUE) @@ -523,7 +525,7 @@ static void decompTest(String fileName) throws Exception { if (quiet == 1) { System.out.println("All performance values in Mpixels/sec\n"); - System.out.format("Bitmap JPEG JPEG %s %s Xform Comp Decomp ", + System.out.format("Pixel JPEG JPEG %s %s Xform Comp Decomp ", (doTile ? "Tile " : "Image"), (doTile ? "Tile " : "Image")); if (doYUV) @@ -539,7 +541,8 @@ static void decompTest(String fileName) throws Exception { (flags & TJ.FLAG_BOTTOMUP) != 0 ? "Bottom-up" : "Top-down"); - for (int tilew = doTile ? 16 : w, tileh = doTile ? 16 : h; ; + minTile = Math.max(TJ.getMCUWidth(subsamp), TJ.getMCUHeight(subsamp)); + for (int tilew = doTile ? minTile : w, tileh = doTile ? minTile : h; ; tilew *= 2, tileh *= 2) { if (tilew > w) tilew = w; @@ -648,7 +651,7 @@ else if (tsubsamp == TJ.SAMP_440) sigFig((double)(w * h * ps) / (double)totalJpegSize, 4), quiet == 2 ? "\n" : " "); - } else if (quiet == 0) { + } else { System.out.format("Transform --> Frame rate: %f fps\n", 1.0 / elapsed); System.out.format(" Output image size: %d bytes\n", @@ -694,34 +697,30 @@ static void usage() throws Exception { String className = new TJBench().getClass().getName(); System.out.println("\nUSAGE: java " + className); - System.out.println(" [options]\n"); + System.out.println(" [options]\n"); System.out.println(" java " + className); - System.out.println(" [options]\n"); + System.out.println(" [options]\n"); System.out.println("Options:\n"); - System.out.println("-alloc = Dynamically allocate JPEG image buffers"); - System.out.println("-bottomup = Test bottom-up compression/decompression"); - System.out.println("-tile = Test performance of the codec when the image is encoded as separate"); - System.out.println(" tiles of varying sizes."); + System.out.println("-bottomup = Use bottom-up row order for packed-pixel source/destination buffers"); + System.out.println("-tile = Compress/transform the input image into separate JPEG tiles of varying"); + System.out.println(" sizes (useful for measuring JPEG overhead)"); System.out.println("-rgb, -bgr, -rgbx, -bgrx, -xbgr, -xrgb ="); - System.out.println(" Test the specified color conversion path in the codec (default = BGR)"); - System.out.println("-fastupsample = Use the fastest chrominance upsampling algorithm available in"); - System.out.println(" the underlying codec"); - System.out.println("-fastdct = Use the fastest DCT/IDCT algorithms available in the underlying"); - System.out.println(" codec"); - System.out.println("-accuratedct = Use the most accurate DCT/IDCT algorithms available in the"); - System.out.println(" underlying codec"); + System.out.println(" Use the specified pixel format for packed-pixel source/destination buffers"); + System.out.println(" [default = BGR]"); + System.out.println("-fastupsample = Use the fastest chrominance upsampling algorithm available"); + System.out.println("-fastdct = Use the fastest DCT/IDCT algorithm available"); + System.out.println("-accuratedct = Use the most accurate DCT/IDCT algorithm available"); System.out.println("-progressive = Use progressive entropy coding in JPEG images generated by"); - System.out.println(" compression and transform operations."); - System.out.println("-subsamp = When testing JPEG compression, this option specifies the level"); - System.out.println(" of chrominance subsampling to use ( = 444, 422, 440, 420, 411, or"); - System.out.println(" GRAY). The default is to test Grayscale, 4:2:0, 4:2:2, and 4:4:4 in"); - System.out.println(" sequence."); + System.out.println(" compression and transform operations"); + System.out.println("-subsamp = When compressing, use the specified level of chrominance"); + System.out.println(" subsampling ( = 444, 422, 440, 420, 411, or GRAY) [default = test"); + System.out.println(" Grayscale, 4:2:0, 4:2:2, and 4:4:4 in sequence]"); System.out.println("-quiet = Output results in tabular rather than verbose format"); - System.out.println("-yuv = Test YUV encoding/decoding functions"); - System.out.println("-yuvpad

    = If testing YUV encoding/decoding, this specifies the number of"); - System.out.println(" bytes to which each row of each plane in the intermediate YUV image is"); - System.out.println(" padded (default = 1)"); - System.out.println("-scale M/N = Scale down the width/height of the decompressed JPEG image by a"); + System.out.println("-yuv = Compress from/decompress to intermediate planar YUV images"); + System.out.println("-yuvpad

    = The number of bytes by which each row in each plane of an"); + System.out.println(" intermediate YUV image is evenly divisible (must be a power of 2)"); + System.out.println(" [default = 1]"); + System.out.println("-scale M/N = When decompressing, scale the width/height of the JPEG image by a"); System.out.print(" factor of M/N (M/N = "); for (i = 0; i < nsf; i++) { System.out.format("%d/%d", scalingFactors[i].getNum(), @@ -739,22 +738,24 @@ else if (nsf > 2) { } System.out.println(")"); System.out.println("-hflip, -vflip, -transpose, -transverse, -rot90, -rot180, -rot270 ="); - System.out.println(" Perform the corresponding lossless transform prior to"); - System.out.println(" decompression (these options are mutually exclusive)"); - System.out.println("-grayscale = Perform lossless grayscale conversion prior to decompression"); - System.out.println(" test (can be combined with the other transforms above)"); + System.out.println(" Perform the specified lossless transform operation on the input image"); + System.out.println(" prior to decompression (these operations are mutually exclusive)"); + System.out.println("-grayscale = Transform the input image into a grayscale JPEG image prior to"); + System.out.println(" decompression (can be combined with the other transform operations above)"); System.out.println("-copynone = Do not copy any extra markers (including EXIF and ICC profile data)"); - System.out.println(" when transforming the image."); - System.out.println("-benchtime = Run each benchmark for at least seconds (default = 5.0)"); - System.out.println("-warmup = Run each benchmark for seconds (default = 1.0) prior to"); + System.out.println(" when transforming the input image"); + System.out.println("-benchtime = Run each benchmark for at least seconds [default = 5.0]"); + System.out.println("-warmup = Run each benchmark for seconds [default = 1.0] prior to"); System.out.println(" starting the timer, in order to prime the caches and thus improve the"); - System.out.println(" consistency of the results."); + System.out.println(" consistency of the benchmark results"); System.out.println("-componly = Stop after running compression tests. Do not test decompression."); - System.out.println("-nowrite = Do not write reference or output images (improves consistency"); - System.out.println(" of performance measurements.)"); + System.out.println("-nowrite = Do not write reference or output images (improves consistency of"); + System.out.println(" benchmark results)"); + System.out.println("-limitscans = Refuse to decompress or transform progressive JPEG images that"); + System.out.println(" have an unreasonably large number of scans"); System.out.println("-stoponwarning = Immediately discontinue the current"); - System.out.println(" compression/decompression/transform operation if the underlying codec"); - System.out.println(" throws a warning (non-fatal error)\n"); + System.out.println(" compression/decompression/transform operation if a warning (non-fatal"); + System.out.println(" error) occurs\n"); System.out.println("NOTE: If the quality is specified as a range (e.g. 90-100), a separate"); System.out.println("test will be performed for all quality values in the range.\n"); System.exit(1); @@ -782,18 +783,18 @@ public static void main(String[] argv) { minArg = 2; if (argv.length < minArg) usage(); + String[] quals = argv[1].split("-", 2); try { - minQual = Integer.parseInt(argv[1]); + minQual = Integer.parseInt(quals[0]); } catch (NumberFormatException e) {} if (minQual < 1 || minQual > 100) throw new Exception("Quality must be between 1 and 100."); - int dashIndex = argv[1].indexOf('-'); - if (dashIndex > 0 && argv[1].length() > dashIndex + 1) { + if (quals.length > 1) { try { - maxQual = Integer.parseInt(argv[1].substring(dashIndex + 1)); + maxQual = Integer.parseInt(quals[1]); } catch (NumberFormatException e) {} } - if (maxQual < 1 || maxQual > 100) + if (maxQual < 1 || maxQual > 100 || maxQual < minQual) maxQual = minQual; } @@ -802,7 +803,7 @@ public static void main(String[] argv) { if (argv[i].equalsIgnoreCase("-tile")) { doTile = true; xformOpt |= TJTransform.OPT_CROP; } else if (argv[i].equalsIgnoreCase("-fastupsample")) { - System.out.println("Using fast upsampling code\n"); + System.out.println("Using fastest upsampling algorithm\n"); flags |= TJ.FLAG_FASTUPSAMPLE; } else if (argv[i].equalsIgnoreCase("-fastdct")) { System.out.println("Using fastest DCT/IDCT algorithm\n"); @@ -813,6 +814,7 @@ public static void main(String[] argv) { } else if (argv[i].equalsIgnoreCase("-progressive")) { System.out.println("Using progressive entropy coding\n"); flags |= TJ.FLAG_PROGRESSIVE; + xformOpt |= TJTransform.OPT_PROGRESSIVE; } else if (argv[i].equalsIgnoreCase("-rgb")) pf = TJ.PF_RGB; else if (argv[i].equalsIgnoreCase("-rgbx")) @@ -899,7 +901,7 @@ else if (argv[i].equalsIgnoreCase("-benchtime") && } else usage(); } else if (argv[i].equalsIgnoreCase("-yuv")) { - System.out.println("Testing YUV planar encoding/decoding\n"); + System.out.println("Testing planar YUV encoding/decoding\n"); doYUV = true; } else if (argv[i].equalsIgnoreCase("-yuvpad") && i < argv.length - 1) { @@ -908,8 +910,10 @@ else if (argv[i].equalsIgnoreCase("-benchtime") && try { temp = Integer.parseInt(argv[++i]); } catch (NumberFormatException e) {} - if (temp >= 1) - yuvPad = temp; + if (temp >= 1 && (temp & (temp - 1)) == 0) + yuvAlign = temp; + else + usage(); } else if (argv[i].equalsIgnoreCase("-subsamp") && i < argv.length - 1) { i++; @@ -925,10 +929,14 @@ else if (argv[i].equals("420")) subsamp = TJ.SAMP_420; else if (argv[i].equals("411")) subsamp = TJ.SAMP_411; + else + usage(); } else if (argv[i].equalsIgnoreCase("-componly")) compOnly = true; else if (argv[i].equalsIgnoreCase("-nowrite")) write = false; + else if (argv[i].equalsIgnoreCase("-limitscans")) + flags |= TJ.FLAG_LIMITSCANS; else if (argv[i].equalsIgnoreCase("-stoponwarning")) flags |= TJ.FLAG_STOPONWARNING; else usage(); @@ -940,8 +948,9 @@ else if (argv[i].equalsIgnoreCase("-stoponwarning")) if ((sf.getNum() != 1 || sf.getDenom() != 1) && doTile) { System.out.println("Disabling tiled compression/decompression tests, because those tests do not"); - System.out.println("work when scaled decompression is enabled."); + System.out.println("work when scaled decompression is enabled.\n"); doTile = false; + xformOpt &= (~TJTransform.OPT_CROP); } if (!decompOnly) { @@ -956,7 +965,7 @@ else if (argv[i].equalsIgnoreCase("-stoponwarning")) if (quiet == 1 && !decompOnly) { System.out.println("All performance values in Mpixels/sec\n"); - System.out.format("Bitmap JPEG JPEG %s %s ", + System.out.format("Pixel JPEG JPEG %s %s ", (doTile ? "Tile " : "Image"), (doTile ? "Tile " : "Image")); if (doYUV) diff --git a/third-party/mozjpeg/mozjpeg/java/TJExample.java b/third-party/mozjpeg/mozjpeg/java/TJExample.java index 7859886988c..5ff1c522e24 100644 --- a/third-party/mozjpeg/mozjpeg/java/TJExample.java +++ b/third-party/mozjpeg/mozjpeg/java/TJExample.java @@ -1,6 +1,6 @@ /* - * Copyright (C)2011-2012, 2014-2015, 2017-2018 D. R. Commander. - * All Rights Reserved. + * Copyright (C)2011-2012, 2014-2015, 2017-2018, 2023 D. R. Commander. + * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -136,14 +136,11 @@ else if (SCALING_FACTORS.length > 2) { System.out.println("-display = Display output image (Output filename need not be specified in this"); System.out.println(" case.)\n"); - System.out.println("-fastupsample = Use the fastest chrominance upsampling algorithm available in"); - System.out.println(" the underlying codec.\n"); + System.out.println("-fastupsample = Use the fastest chrominance upsampling algorithm available\n"); - System.out.println("-fastdct = Use the fastest DCT/IDCT algorithms available in the underlying"); - System.out.println(" codec.\n"); + System.out.println("-fastdct = Use the fastest DCT/IDCT algorithm available\n"); - System.out.println("-accuratedct = Use the most accurate DCT/IDCT algorithms available in the"); - System.out.println(" underlying codec.\n"); + System.out.println("-accuratedct = Use the most accurate DCT/IDCT algorithm available\n"); System.exit(1); } diff --git a/third-party/mozjpeg/mozjpeg/java/TJUnitTest.java b/third-party/mozjpeg/mozjpeg/java/TJUnitTest.java index 91ad5fd951a..20de6dfd45d 100644 --- a/third-party/mozjpeg/mozjpeg/java/TJUnitTest.java +++ b/third-party/mozjpeg/mozjpeg/java/TJUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C)2011-2018 D. R. Commander. All Rights Reserved. + * Copyright (C)2011-2018, 2023 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -48,10 +48,10 @@ private TJUnitTest() {} static void usage() { System.out.println("\nUSAGE: java " + CLASS_NAME + " [options]\n"); System.out.println("Options:"); - System.out.println("-yuv = test YUV encoding/decoding support"); - System.out.println("-noyuvpad = do not pad each line of each Y, U, and V plane to the nearest"); - System.out.println(" 4-byte boundary"); - System.out.println("-bi = test BufferedImage support\n"); + System.out.println("-yuv = test YUV encoding/compression/decompression/decoding"); + System.out.println("-noyuvpad = do not pad each row in each Y, U, and V plane to the nearest"); + System.out.println(" multiple of 4 bytes"); + System.out.println("-bi = test BufferedImage I/O\n"); System.exit(1); } @@ -92,7 +92,7 @@ static void usage() { }; private static boolean doYUV = false; - private static int pad = 4; + private static int yuvAlign = 4; private static boolean bi = false; private static int exitStatus = 0; @@ -532,7 +532,7 @@ static int checkBufYUV(byte[] buf, int size, int w, int h, int subsamp, int hsf = TJ.getMCUWidth(subsamp) / 8, vsf = TJ.getMCUHeight(subsamp) / 8; int pw = pad(w, hsf), ph = pad(h, vsf); int cw = pw / hsf, ch = ph / vsf; - int ypitch = pad(pw, pad), uvpitch = pad(cw, pad); + int ypitch = pad(pw, yuvAlign), uvpitch = pad(cw, yuvAlign); int retval = 1; int correctsize = ypitch * ph + (subsamp == TJ.SAMP_GRAY ? 0 : uvpitch * ch * 2); @@ -668,7 +668,7 @@ static int compTest(TJCompressor tjc, byte[] dstBuf, int w, int h, int pf, if (doYUV) { System.out.format("%s %s -> YUV %s ... ", pfStrLong, buStrLong, SUBNAME_LONG[subsamp]); - YUVImage yuvImage = tjc.encodeYUV(pad, flags); + YUVImage yuvImage = tjc.encodeYUV(yuvAlign, flags); if (checkBufYUV(yuvImage.getBuf(), yuvImage.getSize(), w, h, subsamp, new TJScalingFactor(1, 1)) == 1) System.out.print("Passed.\n"); @@ -733,8 +733,8 @@ static void decompTest(TJDecompressor tjd, byte[] jpegBuf, int jpegSize, if (!sf.isOne()) System.out.format("%d/%d ... ", sf.getNum(), sf.getDenom()); else System.out.print("... "); - YUVImage yuvImage = tjd.decompressToYUV(scaledWidth, pad, scaledHeight, - flags); + YUVImage yuvImage = tjd.decompressToYUV(scaledWidth, yuvAlign, + scaledHeight, flags); if (checkBufYUV(yuvImage.getBuf(), yuvImage.getSize(), scaledWidth, scaledHeight, subsamp, sf) == 1) System.out.print("Passed.\n"); @@ -837,6 +837,55 @@ static void doTest(int w, int h, int[] formats, int subsamp, String baseName) if (tjd != null) tjd.close(); } + static void overflowTest() throws Exception { + /* Ensure that the various buffer size methods don't overflow */ + int size = 0; + boolean exception = false; + + try { + exception = false; + size = TJ.bufSize(18919, 18919, TJ.SAMP_444); + } catch (Exception e) { exception = true; } + if (!exception || size != 0) + throw new Exception("TJ.bufSize() overflow"); + try { + exception = false; + size = TJ.bufSizeYUV(26755, 1, 26755, TJ.SAMP_444); + } catch (Exception e) { exception = true; } + if (!exception || size != 0) + throw new Exception("TJ.bufSizeYUV() overflow"); + try { + exception = false; + size = TJ.bufSizeYUV(26754, 3, 26754, TJ.SAMP_444); + } catch (Exception e) { exception = true; } + if (!exception || size != 0) + throw new Exception("TJ.bufSizeYUV() overflow"); + try { + exception = false; + size = TJ.bufSizeYUV(26754, -1, 26754, TJ.SAMP_444); + } catch (Exception e) { exception = true; } + if (!exception || size != 0) + throw new Exception("TJ.bufSizeYUV() overflow"); + try { + exception = false; + size = TJ.planeSizeYUV(0, 46341, 0, 46341, TJ.SAMP_444); + } catch (Exception e) { exception = true; } + if (!exception || size != 0) + throw new Exception("TJ.planeSizeYUV() overflow"); + try { + exception = false; + size = TJ.planeWidth(0, Integer.MAX_VALUE, TJ.SAMP_420); + } catch (Exception e) { exception = true; } + if (!exception || size != 0) + throw new Exception("TJ.planeWidth() overflow"); + try { + exception = false; + size = TJ.planeHeight(0, Integer.MAX_VALUE, TJ.SAMP_420); + } catch (Exception e) { exception = true; } + if (!exception || size != 0) + throw new Exception("TJ.planeHeight() overflow"); + } + static void bufSizeTest() throws Exception { int w, h, i, subsamp; byte[] srcBuf, dstBuf = null; @@ -855,7 +904,7 @@ static void bufSizeTest() throws Exception { System.out.format("%04d x %04d\b\b\b\b\b\b\b\b\b\b\b", w, h); srcBuf = new byte[w * h * 4]; if (doYUV) - dstImage = new YUVImage(w, pad, h, subsamp); + dstImage = new YUVImage(w, yuvAlign, h, subsamp); else dstBuf = new byte[TJ.bufSize(w, h, subsamp)]; for (i = 0; i < w * h * 4; i++) { @@ -871,7 +920,7 @@ static void bufSizeTest() throws Exception { srcBuf = new byte[h * w * 4]; if (doYUV) - dstImage = new YUVImage(h, pad, w, subsamp); + dstImage = new YUVImage(h, yuvAlign, w, subsamp); else dstBuf = new byte[TJ.bufSize(h, w, subsamp)]; for (i = 0; i < h * w * 4; i++) { @@ -903,7 +952,7 @@ public static void main(String[] argv) { if (argv[i].equalsIgnoreCase("-yuv")) doYUV = true; else if (argv[i].equalsIgnoreCase("-noyuvpad")) - pad = 1; + yuvAlign = 1; else if (argv[i].equalsIgnoreCase("-bi")) { bi = true; testName = "javabitest"; @@ -912,6 +961,7 @@ else if (argv[i].equalsIgnoreCase("-bi")) { } if (doYUV) FORMATS_4BYTE[4] = -1; + overflowTest(); doTest(35, 39, bi ? FORMATS_3BYTEBI : FORMATS_3BYTE, TJ.SAMP_444, testName); doTest(39, 41, bi ? FORMATS_4BYTEBI : FORMATS_4BYTE, TJ.SAMP_444, diff --git a/third-party/mozjpeg/mozjpeg/java/doc/constant-values.html b/third-party/mozjpeg/mozjpeg/java/doc/constant-values.html index fb33327f447..07ba05270d3 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/constant-values.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/constant-values.html @@ -189,167 +189,174 @@

    org.libjpegturbo.*

    + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/third-party/mozjpeg/mozjpeg/java/doc/index-all.html b/third-party/mozjpeg/mozjpeg/java/doc/index-all.html index 366c7ea1fc8..02245366a37 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/index-all.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/index-all.html @@ -74,8 +74,9 @@

    B

    bufSizeYUV(int, int, int, int) - Static method in class org.libjpegturbo.turbojpeg.TJ
    -
    Returns the size of the buffer (in bytes) required to hold a YUV planar - image with the given width, height, and level of chrominance subsampling.
    +
    Returns the size of the buffer (in bytes) required to hold a unified + planar YUV image with the given width, height, and level of chrominance + subsampling.
    bufSizeYUV(int, int, int) - Static method in class org.libjpegturbo.turbojpeg.TJ
    @@ -103,13 +104,14 @@

    C

    compress(byte[], int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Compress the uncompressed source image associated with this compressor - instance and output a JPEG image to the given destination buffer.
    +
    Compress the packed-pixel or planar YUV source image associated with this + compressor instance and output a JPEG image to the given destination + buffer.
    compress(int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Compress the uncompressed source image associated with this compressor - instance and return a buffer containing a JPEG image.
    +
    Compress the packed-pixel or planar YUV source image associated with this + compressor instance and return a buffer containing a JPEG image.
    compress(BufferedImage, byte[], int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    @@ -161,9 +163,9 @@

    D

    decompress(byte[], int, int, int, int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a grayscale, RGB, or CMYK image - to the given destination buffer.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + grayscale, RGB, or CMYK image to the given destination buffer.
    decompress(byte[], int, int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -174,32 +176,35 @@

    D

    decompress(int, int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Decompress the JPEG source image associated with this decompressor - instance and return a buffer containing the decompressed image.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and return a buffer containing + the packed-pixel decompressed image.
    decompress(int[], int, int, int, int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a grayscale, RGB, or CMYK image - to the given destination buffer.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + grayscale, RGB, or CMYK image to the given destination buffer.
    decompress(BufferedImage, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a decompressed/decoded image to - the given BufferedImage instance.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + decompressed/decoded image to the given BufferedImage + instance.
    decompress(int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and return a BufferedImage - instance containing the decompressed/decoded image.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and return a + BufferedImage instance containing the packed-pixel + decompressed/decoded image.
    decompressToYUV(YUVImage, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    Decompress the JPEG source image associated with this decompressor - instance into a YUV planar image and store it in the given - YUVImage instance.
    + instance into a planar YUV image and store it in the given + YUVImage instance.
    decompressToYUV(byte[], int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -211,13 +216,13 @@

    D

    Decompress the JPEG source image associated with this decompressor instance into a set of Y, U (Cb), and V (Cr) image planes and return a - YUVImage instance containing the decompressed image planes.
    + YUVImage instance containing the decompressed image planes.
    decompressToYUV(int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    Decompress the JPEG source image associated with this decompressor - instance into a unified YUV planar image buffer and return a - YUVImage instance containing the decompressed image.
    + instance into a unified planar YUV image and return a YUVImage + instance containing the decompressed image.
    decompressToYUV(int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -233,9 +238,9 @@

    E

    encodeYUV(YUVImage, int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Encode the uncompressed source image associated with this compressor - instance into a YUV planar image and store it in the given - YUVImage instance.
    +
    Encode the packed-pixel source image associated with this compressor + instance into a planar YUV image and store it in the given + YUVImage instance.
    encodeYUV(byte[], int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    @@ -245,15 +250,15 @@

    E

    encodeYUV(int, int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Encode the uncompressed source image associated with this compressor - instance into a unified YUV planar image buffer and return a - YUVImage instance containing the encoded image.
    +
    Encode the packed-pixel source image associated with this compressor + instance into a unified planar YUV image and return a YUVImage + instance containing the encoded image.
    encodeYUV(int[], int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Encode the uncompressed source image associated with this compressor +
    Encode the packed-pixel source image associated with this compressor instance into separate Y, U (Cb), and V (Cr) image planes and return a - YUVImage instance containing the encoded image planes.
    + YUVImage instance containing the encoded image planes.
    encodeYUV(int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    @@ -288,8 +293,8 @@

    E

    ERR_WARNING - Static variable in class org.libjpegturbo.turbojpeg.TJ
    -
    The error was non-fatal and recoverable, but the image may still be - corrupt.
    +
    The error was non-fatal and recoverable, but the destination image may + still be corrupt.
    @@ -303,23 +308,21 @@

    F

     
    FLAG_ACCURATEDCT - Static variable in class org.libjpegturbo.turbojpeg.TJ
    -
    Use the most accurate DCT/IDCT algorithm available in the underlying - codec.
    +
    Use the most accurate DCT/IDCT algorithm available.
    FLAG_BOTTOMUP - Static variable in class org.libjpegturbo.turbojpeg.TJ
    -
    The uncompressed source/destination image is stored in bottom-up (Windows, - OpenGL) order, not top-down (X11) order.
    +
    Rows in the packed-pixel source/destination image are stored in bottom-up + (Windows, OpenGL) order rather than in top-down (X11) order.
    FLAG_FASTDCT - Static variable in class org.libjpegturbo.turbojpeg.TJ
    -
    Use the fastest DCT/IDCT algorithm available in the underlying codec.
    +
    Use the fastest DCT/IDCT algorithm available.
    FLAG_FASTUPSAMPLE - Static variable in class org.libjpegturbo.turbojpeg.TJ
    When decompressing an image that was compressed using chrominance - subsampling, use the fastest chrominance upsampling algorithm available in - the underlying codec.
    + subsampling, use the fastest chrominance upsampling algorithm available.
    FLAG_FORCEMMX - Static variable in class org.libjpegturbo.turbojpeg.TJ
    @@ -337,6 +340,11 @@

    F

    Deprecated.
    +
    FLAG_LIMITSCANS - Static variable in class org.libjpegturbo.turbojpeg.TJ
    +
    +
    Limit the number of progressive JPEG scans that the decompression and + transform operations will process.
    +
    FLAG_PROGRESSIVE - Static variable in class org.libjpegturbo.turbojpeg.TJ
    Use progressive entropy coding in JPEG images generated by compression and @@ -345,7 +353,7 @@

    F

    FLAG_STOPONWARNING - Static variable in class org.libjpegturbo.turbojpeg.TJ
    Immediately discontinue the current compression/decompression/transform - operation if the underlying codec throws a warning (non-fatal error).
    + operation if a warning (non-fatal error) occurs.
    @@ -365,8 +373,8 @@

    G

    getBuf() - Method in class org.libjpegturbo.turbojpeg.YUVImage
    -
    Returns the YUV image buffer (if this image is stored in a unified - buffer rather than separate image planes.)
    +
    Returns the YUV buffer (if this image is stored in a unified buffer rather + than separate image planes.)
    getColorspace() - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -403,7 +411,7 @@

    G

    getJPEGBuf() - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Returns the JPEG image buffer associated with this decompressor instance.
    +
    Returns the JPEG buffer associated with this decompressor instance.
    getJPEGSize() - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -431,7 +439,7 @@

    G

    getPad() - Method in class org.libjpegturbo.turbojpeg.YUVImage
    -
    Returns the line padding used in the YUV image buffer (if this image is +
    Returns the row alignment (in bytes) of the YUV buffer (if this image is stored in a unified buffer rather than separate image planes.)
    getPixelSize(int) - Static method in class org.libjpegturbo.turbojpeg.TJ
    @@ -465,17 +473,17 @@

    G

    getScalingFactors() - Static method in class org.libjpegturbo.turbojpeg.TJ
    -
    Returns a list of fractional scaling factors that the JPEG decompressor in - this implementation of TurboJPEG supports.
    +
    Returns a list of fractional scaling factors that the JPEG decompressor + supports.
    getSize() - Method in class org.libjpegturbo.turbojpeg.YUVImage
    -
    Returns the size (in bytes) of the YUV image buffer (if this image is - stored in a unified buffer rather than separate image planes.)
    +
    Returns the size (in bytes) of the YUV buffer (if this image is stored in + a unified buffer rather than separate image planes.)
    getStrides() - Method in class org.libjpegturbo.turbojpeg.YUVImage
    -
    Returns the number of bytes per line of each plane in the YUV image.
    +
    Returns the number of bytes per row of each plane in the YUV image.
    getSubsamp() - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -489,7 +497,7 @@

    G

    getTransformedSizes() - Method in class org.libjpegturbo.turbojpeg.TJTransformer
    Returns an array containing the sizes of the transformed JPEG images - generated by the most recent transform operation.
    + (in bytes) generated by the most recent transform operation.
    getWidth() - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -573,7 +581,7 @@

    O

    op - Variable in class org.libjpegturbo.turbojpeg.TJTransform
    -
    Transform operation (one of OP_*)
    +
    Transform operation (one of OP_*)
    OP_HFLIP - Static variable in class org.libjpegturbo.turbojpeg.TJTransform
    @@ -611,7 +619,7 @@

    O

    OPT_COPYNONE - Static variable in class org.libjpegturbo.turbojpeg.TJTransform
    This option will prevent TJTransformer.transform() from copying any extra markers (including EXIF - and ICC profile data) from the source image to the output image.
    + and ICC profile data) from the source image to the destination image.
    OPT_CROP - Static variable in class org.libjpegturbo.turbojpeg.TJTransform
    @@ -619,8 +627,8 @@

    O

    OPT_GRAY - Static variable in class org.libjpegturbo.turbojpeg.TJTransform
    -
    This option will discard the color data in the input image and produce - a grayscale output image.
    +
    This option will discard the color data in the source image and produce a + grayscale destination image.
    OPT_NOOUTPUT - Static variable in class org.libjpegturbo.turbojpeg.TJTransform
    @@ -634,7 +642,7 @@

    O

    OPT_PROGRESSIVE - Static variable in class org.libjpegturbo.turbojpeg.TJTransform
    -
    This option will enable progressive entropy coding in the output image +
    This option will enable progressive entropy coding in the JPEG image generated by this particular transform.
    OPT_TRIM - Static variable in class org.libjpegturbo.turbojpeg.TJTransform
    @@ -644,7 +652,8 @@

    O

    options - Variable in class org.libjpegturbo.turbojpeg.TJTransform
    -
    Transform options (bitwise OR of one or more of OPT_*)
    +
    Transform options (bitwise OR of one or more of + OPT_*)
    org.libjpegturbo.turbojpeg - package org.libjpegturbo.turbojpeg
     
    @@ -751,7 +760,7 @@

    S

    setBuf(byte[], int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.YUVImage
    -
    Assign a unified image buffer to this YUVImage instance.
    +
    Assign a unified buffer to this YUVImage instance.
    setJPEGImage(byte[], int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    @@ -765,7 +774,7 @@

    S

    setSourceImage(byte[], int, int, int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Associate an uncompressed RGB, grayscale, or CMYK source image with this +
    Associate a packed-pixel RGB, grayscale, or CMYK source image with this compressor instance.
    setSourceImage(byte[], int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    @@ -777,22 +786,22 @@

    S

    setSourceImage(BufferedImage, int, int, int, int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Associate an uncompressed RGB or grayscale source image with this +
    Associate a packed-pixel RGB or grayscale source image with this compressor instance.
    setSourceImage(YUVImage) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Associate an uncompressed YUV planar source image with this compressor - instance.
    +
    Associate a planar YUV source image with this compressor instance.
    setSourceImage(byte[], int) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Associate the JPEG image of length imageSize bytes stored in +
    Associate the JPEG image or "abbreviated table specification" (AKA + "tables-only") datastream of length imageSize bytes stored in jpegImage with this decompressor instance.
    setSourceImage(YUVImage) - Method in class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Associate the specified YUV planar source image with this decompressor +
    Associate the specified planar YUV source image with this decompressor instance.
    setSubsamp(int) - Method in class org.libjpegturbo.turbojpeg.TJCompressor
    @@ -820,7 +829,7 @@

    T

    TJCompressor(byte[], int, int, int, int, int, int) - Constructor for class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Create a TurboJPEG compressor instance and associate the uncompressed +
    Create a TurboJPEG compressor instance and associate the packed-pixel source image stored in srcImage with the newly created instance.
    @@ -833,7 +842,7 @@

    T

    TJCompressor(BufferedImage, int, int, int, int) - Constructor for class org.libjpegturbo.turbojpeg.TJCompressor
    -
    Create a TurboJPEG compressor instance and associate the uncompressed +
    Create a TurboJPEG compressor instance and associate the packed-pixel source image stored in srcImage with the newly created instance.
    @@ -852,17 +861,19 @@

    T

    TJDecompressor(byte[]) - Constructor for class org.libjpegturbo.turbojpeg.TJDecompressor
    Create a TurboJPEG decompressor instance and associate the JPEG source - image stored in jpegImage with the newly created instance.
    + image or "abbreviated table specification" (AKA "tables-only") datastream + stored in jpegImage with the newly created instance.
    TJDecompressor(byte[], int) - Constructor for class org.libjpegturbo.turbojpeg.TJDecompressor
    Create a TurboJPEG decompressor instance and associate the JPEG source - image of length imageSize bytes stored in - jpegImage with the newly created instance.
    + image or "abbreviated table specification" (AKA "tables-only") datastream + of length imageSize bytes stored in jpegImage + with the newly created instance.
    TJDecompressor(YUVImage) - Constructor for class org.libjpegturbo.turbojpeg.TJDecompressor
    -
    Create a TurboJPEG decompressor instance and associate the YUV planar +
    Create a TurboJPEG decompressor instance and associate the planar YUV source image stored in yuvImage with the newly created instance.
    @@ -913,25 +924,26 @@

    T

    TJTransformer(byte[]) - Constructor for class org.libjpegturbo.turbojpeg.TJTransformer
    Create a TurboJPEG lossless transformer instance and associate the JPEG - image stored in jpegImage with the newly created instance.
    + source image stored in jpegImage with the newly created + instance.
    TJTransformer(byte[], int) - Constructor for class org.libjpegturbo.turbojpeg.TJTransformer
    Create a TurboJPEG lossless transformer instance and associate the JPEG - image of length imageSize bytes stored in + source image of length imageSize bytes stored in jpegImage with the newly created instance.
    transform(byte[][], TJTransform[], int) - Method in class org.libjpegturbo.turbojpeg.TJTransformer
    -
    Losslessly transform the JPEG image associated with this transformer - instance into one or more JPEG images stored in the given destination - buffers.
    +
    Losslessly transform the JPEG source image associated with this + transformer instance into one or more JPEG images stored in the given + destination buffers.
    transform(TJTransform[], int) - Method in class org.libjpegturbo.turbojpeg.TJTransformer
    -
    Losslessly transform the JPEG image associated with this transformer - instance and return an array of TJDecompressor instances, each of - which has a transformed JPEG image associated with it.
    +
    Losslessly transform the JPEG source image associated with this + transformer instance and return an array of TJDecompressor + instances, each of which has a transformed JPEG image associated with it.
    @@ -939,13 +951,15 @@

    T

    Y

    +
    yuvAlign - Variable in class org.libjpegturbo.turbojpeg.YUVImage
    +
     
    yuvHeight - Variable in class org.libjpegturbo.turbojpeg.YUVImage
     
    yuvImage - Variable in class org.libjpegturbo.turbojpeg.TJDecompressor
     
    YUVImage - Class in org.libjpegturbo.turbojpeg
    -
    This class encapsulates a YUV planar image and the metadata +
    This class encapsulates a planar YUV image and the metadata associated with it.
    YUVImage(int, int[], int, int) - Constructor for class org.libjpegturbo.turbojpeg.YUVImage
    @@ -955,8 +969,8 @@

    Y

    YUVImage(int, int, int, int) - Constructor for class org.libjpegturbo.turbojpeg.YUVImage
    -
    Create a new YUVImage instance backed by a unified image - buffer, and allocate memory for the image buffer.
    +
    Create a new YUVImage instance backed by a unified buffer, + and allocate memory for the buffer.
    YUVImage(byte[][], int[], int, int[], int, int) - Constructor for class org.libjpegturbo.turbojpeg.YUVImage
    @@ -965,13 +979,11 @@

    Y

    YUVImage(byte[], int, int, int, int) - Constructor for class org.libjpegturbo.turbojpeg.YUVImage
    -
    Create a new YUVImage instance from an existing unified image +
    Create a new YUVImage instance from an existing unified buffer.
    yuvOffsets - Variable in class org.libjpegturbo.turbojpeg.YUVImage
     
    -
    yuvPad - Variable in class org.libjpegturbo.turbojpeg.YUVImage
    -
     
    yuvPlanes - Variable in class org.libjpegturbo.turbojpeg.YUVImage
     
    yuvStrides - Variable in class org.libjpegturbo.turbojpeg.YUVImage
    diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJ.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJ.html index 79f9fcfe0b6..f57baa7c440 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJ.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJ.html @@ -156,36 +156,34 @@

    Field Summary

    @@ -214,145 +212,152 @@

    Field Summary

    + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -453,8 +459,8 @@

    Method Summary

    @@ -771,8 +777,8 @@

    PF_CMYK

    vice versa, but the mapping is typically not 1:1 or reversible, nor can it be defined with a simple formula. Thus, such a conversion is out of scope for a codec library. However, the TurboJPEG API allows for compressing - CMYK pixels into a YCCK JPEG image (see CS_YCCK) and - decompressing YCCK JPEG images into CMYK pixels. + packed-pixel CMYK images into YCCK JPEG images (see CS_YCCK) and + decompressing YCCK JPEG images into packed-pixel CMYK images.
    See Also:
    Constant Field Values
    @@ -797,8 +803,9 @@

    CS_RGB

    RGB colorspace. When compressing the JPEG image, the R, G, and B components in the source image are reordered into image planes, but no colorspace conversion or subsampling is performed. RGB JPEG images can be - decompressed to any of the extended RGB pixel formats or grayscale, but - they cannot be decompressed to YUV images.
    + decompressed to packed-pixel images with any of the extended RGB or + grayscale pixel formats, but they cannot be decompressed to planar YUV + images.
    See Also:
    Constant Field Values
    @@ -813,15 +820,17 @@

    CS_YCbCr

    mathematical transformation of RGB designed solely for storage and transmission. YCbCr images must be converted to RGB before they can actually be displayed. In the YCbCr colorspace, the Y (luminance) - component represents the black & white portion of the original image, and - the Cb and Cr (chrominance) components represent the color portion of the - original image. Originally, the analog equivalent of this transformation - allowed the same signal to drive both black & white and color televisions, - but JPEG images use YCbCr primarily because it allows the color data to be - optionally subsampled for the purposes of reducing bandwidth or disk - space. YCbCr is the most common JPEG colorspace, and YCbCr JPEG images - can be compressed from and decompressed to any of the extended RGB pixel - formats or grayscale, or they can be decompressed to YUV planar images. + component represents the black & white portion of the original image, + and the Cb and Cr (chrominance) components represent the color portion of + the original image. Originally, the analog equivalent of this + transformation allowed the same signal to drive both black & white and + color televisions, but JPEG images use YCbCr primarily because it allows + the color data to be optionally subsampled for the purposes of reducing + network or disk usage. YCbCr is the most common JPEG colorspace, and + YCbCr JPEG images can be compressed from and decompressed to packed-pixel + images with any of the extended RGB or grayscale pixel formats. YCbCr + JPEG images can also be compressed from and decompressed to planar YUV + images.
    See Also:
    Constant Field Values
    @@ -834,9 +843,10 @@

    CS_GRAY

    public static final int CS_GRAY
    Grayscale colorspace. The JPEG image retains only the luminance data (Y component), and any color data from the source image is discarded. - Grayscale JPEG images can be compressed from and decompressed to any of - the extended RGB pixel formats or grayscale, or they can be decompressed - to YUV planar images.
    + Grayscale JPEG images can be compressed from and decompressed to + packed-pixel images with any of the extended RGB or grayscale pixel + formats, or they can be compressed from and decompressed to planar YUV + images.
    See Also:
    Constant Field Values
    @@ -850,7 +860,7 @@

    CS_CMYK

    CMYK colorspace. When compressing the JPEG image, the C, M, Y, and K components in the source image are reordered into image planes, but no colorspace conversion or subsampling is performed. CMYK JPEG images can - only be decompressed to CMYK pixels.
    + only be decompressed to packed-pixel images with the CMYK pixel format.
    See Also:
    Constant Field Values
    @@ -867,7 +877,7 @@

    CS_YCCK

    reversibly transformed into YCCK, and as with YCbCr, the chrominance components in the YCCK pixels can be subsampled without incurring major perceptual loss. YCCK JPEG images can only be compressed from and - decompressed to CMYK pixels. + decompressed to packed-pixel images with the CMYK pixel format.
    See Also:
    Constant Field Values
    @@ -878,8 +888,8 @@

    CS_YCCK

  • FLAG_BOTTOMUP

    public static final int FLAG_BOTTOMUP
    -
    The uncompressed source/destination image is stored in bottom-up (Windows, - OpenGL) order, not top-down (X11) order.
    +
    Rows in the packed-pixel source/destination image are stored in bottom-up + (Windows, OpenGL) order rather than in top-down (X11) order.
    See Also:
    Constant Field Values
  • @@ -939,10 +949,10 @@

    FLAG_FORCESSE3

    FLAG_FASTUPSAMPLE

    public static final int FLAG_FASTUPSAMPLE
    When decompressing an image that was compressed using chrominance - subsampling, use the fastest chrominance upsampling algorithm available in - the underlying codec. The default is to use smooth upsampling, which - creates a smooth transition between neighboring chrominance components in - order to reduce upsampling artifacts in the decompressed image.
    + subsampling, use the fastest chrominance upsampling algorithm available. + The default is to use smooth upsampling, which creates a smooth transition + between neighboring chrominance components in order to reduce upsampling + artifacts in the decompressed image.
    See Also:
    Constant Field Values
    @@ -953,12 +963,12 @@

    FLAG_FASTUPSAMPLE

  • FLAG_FASTDCT

    public static final int FLAG_FASTDCT
    -
    Use the fastest DCT/IDCT algorithm available in the underlying codec. The - default if this flag is not specified is implementation-specific. For - example, the implementation of TurboJPEG for libjpeg[-turbo] uses the fast - algorithm by default when compressing, because this has been shown to have - only a very slight effect on accuracy, but it uses the accurate algorithm - when decompressing, because this has been shown to have a larger effect.
    +
    Use the fastest DCT/IDCT algorithm available. The default if this flag is + not specified is implementation-specific. For example, the implementation + of the TurboJPEG API in libjpeg-turbo uses the fast algorithm by default + when compressing, because this has been shown to have only a very slight + effect on accuracy, but it uses the accurate algorithm when decompressing, + because this has been shown to have a larger effect.
    See Also:
    Constant Field Values
  • @@ -969,13 +979,12 @@

    FLAG_FASTDCT

  • FLAG_ACCURATEDCT

    public static final int FLAG_ACCURATEDCT
    -
    Use the most accurate DCT/IDCT algorithm available in the underlying - codec. The default if this flag is not specified is - implementation-specific. For example, the implementation of TurboJPEG for - libjpeg[-turbo] uses the fast algorithm by default when compressing, - because this has been shown to have only a very slight effect on accuracy, - but it uses the accurate algorithm when decompressing, because this has - been shown to have a larger effect.
    +
    Use the most accurate DCT/IDCT algorithm available. The default if this + flag is not specified is implementation-specific. For example, the + implementation of the TurboJPEG API in libjpeg-turbo uses the fast + algorithm by default when compressing, because this has been shown to have + only a very slight effect on accuracy, but it uses the accurate algorithm + when decompressing, because this has been shown to have a larger effect.
    See Also:
    Constant Field Values
  • @@ -987,14 +996,13 @@

    FLAG_ACCURATEDCT

    FLAG_STOPONWARNING

    public static final int FLAG_STOPONWARNING
    Immediately discontinue the current compression/decompression/transform - operation if the underlying codec throws a warning (non-fatal error). The - default behavior is to allow the operation to complete unless a fatal - error is encountered. + operation if a warning (non-fatal error) occurs. The default behavior is + to allow the operation to complete unless a fatal error is encountered.

    NOTE: due to the design of the TurboJPEG Java API, only certain methods (specifically, TJDecompressor.decompress*() methods - with a void return type) will complete and leave the output image in a - fully recoverable state after a non-fatal error occurs.

    + with a void return type) will complete and leave the destination image in + a fully recoverable state after a non-fatal error occurs.
    See Also:
    Constant Field Values
    @@ -1012,6 +1020,23 @@

    FLAG_PROGRESSIVE

    See Also:
    Constant Field Values
    + + + +
      +
    • +

      FLAG_LIMITSCANS

      +
      public static final int FLAG_LIMITSCANS
      +
      Limit the number of progressive JPEG scans that the decompression and + transform operations will process. If a progressive JPEG image contains + an unreasonably large number of scans, then this flag will cause the + decompression and transform operations to throw an error. The primary + purpose of this is to allow security-critical applications to guard + against an exploit of the progressive JPEG format described in + this report.
      +
      See Also:
      Constant Field Values
      +
    • +
    @@ -1030,13 +1055,13 @@

    NUMERR

  • ERR_WARNING

    public static final int ERR_WARNING
    -
    The error was non-fatal and recoverable, but the image may still be - corrupt. +
    The error was non-fatal and recoverable, but the destination image may + still be corrupt.

    NOTE: due to the design of the TurboJPEG Java API, only certain methods (specifically, TJDecompressor.decompress*() methods - with a void return type) will complete and leave the output image in a - fully recoverable state after a non-fatal error occurs.

    + with a void return type) will complete and leave the destination image in + a fully recoverable state after a non-fatal error occurs.
    See Also:
    Constant Field Values
  • @@ -1069,7 +1094,7 @@

    getMCUWidth

    Returns the MCU block width for the given level of chrominance subsampling.
    Parameters:
    subsamp - the level of chrominance subsampling (one of - SAMP_*)
    + SAMP_*)
    Returns:
    the MCU block width for the given level of chrominance subsampling.
    @@ -1084,7 +1109,7 @@

    getMCUHeight

    Returns the MCU block height for the given level of chrominance subsampling.
    Parameters:
    subsamp - the level of chrominance subsampling (one of - SAMP_*)
    + SAMP_*)
    Returns:
    the MCU block height for the given level of chrominance subsampling.
    @@ -1097,7 +1122,7 @@

    getMCUHeight

    getPixelSize

    public static int getPixelSize(int pixelFormat)
    Returns the pixel size (in bytes) for the given pixel format.
    -
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    +
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    Returns:
    the pixel size (in bytes) for the given pixel format.
    @@ -1113,7 +1138,7 @@

    getRedOffset

    of format TJ.PF_BGRX is stored in char pixel[], then the red component will be pixel[TJ.getRedOffset(TJ.PF_BGRX)]. -
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    +
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    Returns:
    the red offset for the given pixel format, or -1 if the pixel format does not have a red component.
    @@ -1130,7 +1155,7 @@

    getGreenOffset

    of format TJ.PF_BGRX is stored in char pixel[], then the green component will be pixel[TJ.getGreenOffset(TJ.PF_BGRX)]. -
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    +
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    Returns:
    the green offset for the given pixel format, or -1 if the pixel format does not have a green component.
    @@ -1147,7 +1172,7 @@

    getBlueOffset

    of format TJ.PF_BGRX is stored in char pixel[], then the blue component will be pixel[TJ.getBlueOffset(TJ.PF_BGRX)]. -
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    +
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    Returns:
    the blue offset for the given pixel format, or -1 if the pixel format does not have a blue component.
    @@ -1164,7 +1189,7 @@

    getAlphaOffset

    of format TJ.PF_BGRA is stored in char pixel[], then the alpha component will be pixel[TJ.getAlphaOffset(TJ.PF_BGRA)]. -
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    +
    Parameters:
    pixelFormat - the pixel format (one of PF_*)
    Returns:
    the alpha offset for the given pixel format, or -1 if the pixel format does not have a alpha component.
    @@ -1181,7 +1206,7 @@

    bufSize

    Returns the maximum size of the buffer (in bytes) required to hold a JPEG image with the given width, height, and level of chrominance subsampling.
    Parameters:
    width - the width (in pixels) of the JPEG image
    height - the height (in pixels) of the JPEG image
    jpegSubsamp - the level of chrominance subsampling to be used when - generating the JPEG image (one of TJ.SAMP_*)
    + generating the JPEG image (one of TJ.SAMP_*)
    Returns:
    the maximum size of the buffer (in bytes) required to hold a JPEG image with the given width, height, and level of chrominance subsampling.
    @@ -1193,16 +1218,20 @@

    bufSize

  • bufSizeYUV

    public static int bufSizeYUV(int width,
    -             int pad,
    +             int align,
                  int height,
                  int subsamp)
    -
    Returns the size of the buffer (in bytes) required to hold a YUV planar - image with the given width, height, and level of chrominance subsampling.
    -
    Parameters:
    width - the width (in pixels) of the YUV image
    pad - the width of each line in each plane of the image is padded to - the nearest multiple of this number of bytes (must be a power of 2.)
    height - the height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV - image (one of TJ.SAMP_*)
    -
    Returns:
    the size of the buffer (in bytes) required to hold a YUV planar - image with the given width, height, and level of chrominance subsampling.
    +
    Returns the size of the buffer (in bytes) required to hold a unified + planar YUV image with the given width, height, and level of chrominance + subsampling.
    +
    Parameters:
    width - the width (in pixels) of the YUV image
    align - row alignment (in bytes) of the YUV image (must be a power of + 2.) Setting this parameter to n specifies that each row in each plane of + the YUV image will be padded to the nearest multiple of n bytes + (1 = unpadded.)
    height - the height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV + image (one of TJ.SAMP_*)
    +
    Returns:
    the size of the buffer (in bytes) required to hold a unified + planar YUV image with the given width, height, and level of chrominance + subsampling.
  • @@ -1233,11 +1262,11 @@

    planeSizeYUV

    plane with the given parameters.
    Parameters:
    componentID - ID number of the image plane (0 = Y, 1 = U/Cb, 2 = V/Cr)
    width - width (in pixels) of the YUV image. NOTE: this is the width - of the whole image, not the plane width.
    stride - bytes per line in the image plane.
    height - height (in pixels) of the YUV image. NOTE: this is the + of the whole image, not the plane width.
    stride - bytes per row in the image plane.
    height - height (in pixels) of the YUV image. NOTE: this is the height of the whole image, not the plane height.
    subsamp - the level of chrominance subsampling used in the YUV - image (one of TJ.SAMP_*)
    -
    Returns:
    the size of the buffer (in bytes) required to hold a YUV planar - image with the given parameters.
    + image (one of TJ.SAMP_*) +
    Returns:
    the size of the buffer (in bytes) required to hold a YUV image + plane with the given parameters.
    @@ -1253,7 +1282,7 @@

    planeWidth

    Refer to
    YUVImage for a description of plane width.
    Parameters:
    componentID - ID number of the image plane (0 = Y, 1 = U/Cb, 2 = V/Cr)
    width - width (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV image - (one of TJ.SAMP_*)
    + (one of TJ.SAMP_*)
    Returns:
    the plane width of a YUV image plane with the given parameters.
    @@ -1270,7 +1299,7 @@

    planeHeight

    Refer to YUVImage for a description of plane height.
    Parameters:
    componentID - ID number of the image plane (0 = Y, 1 = U/Cb, 2 = V/Cr)
    height - height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV image - (one of TJ.SAMP_*)
    + (one of TJ.SAMP_*)
    Returns:
    the plane height of a YUV image plane with the given parameters.
    @@ -1281,10 +1310,10 @@

    planeHeight

  • getScalingFactors

    public static TJScalingFactor[] getScalingFactors()
    -
    Returns a list of fractional scaling factors that the JPEG decompressor in - this implementation of TurboJPEG supports.
    -
    Returns:
    a list of fractional scaling factors that the JPEG decompressor in - this implementation of TurboJPEG supports.
    +
    Returns a list of fractional scaling factors that the JPEG decompressor + supports.
    +
    Returns:
    a list of fractional scaling factors that the JPEG decompressor + supports.
  • diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCompressor.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCompressor.html index a53f87973ea..440247b3cb8 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCompressor.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCompressor.html @@ -132,7 +132,7 @@

    Constructor Summary

    int y, int width, int height) -
    Create a TurboJPEG compressor instance and associate the uncompressed +
    Create a TurboJPEG compressor instance and associate the packed-pixel source image stored in srcImage with the newly created instance.
    @@ -157,7 +157,7 @@

    Constructor Summary

    int pitch, int height, int pixelFormat) -
    Create a TurboJPEG compressor instance and associate the uncompressed +
    Create a TurboJPEG compressor instance and associate the packed-pixel source image stored in srcImage with the newly created instance.
    @@ -210,15 +210,16 @@

    Method Summary

    @@ -265,27 +266,27 @@

    Method Summary

    - @@ -312,7 +313,7 @@

    Method Summary

    int y, int width, int height) -
    Associate an uncompressed RGB or grayscale source image with this +
    Associate a packed-pixel RGB or grayscale source image with this compressor instance.
    @@ -338,15 +339,14 @@

    Method Summary

    int pitch, int height, int pixelFormat) -
    Associate an uncompressed RGB, grayscale, or CMYK source image with this +
    Associate a packed-pixel RGB, grayscale, or CMYK source image with this compressor instance.
    @@ -405,7 +405,7 @@

    TJCompressor

    int height, int pixelFormat) throws TJException -
    Create a TurboJPEG compressor instance and associate the uncompressed +
    Create a TurboJPEG compressor instance and associate the packed-pixel source image stored in srcImage with the newly created instance.
    Parameters:
    srcImage - see setSourceImage(byte[], int, int, int, int, int, int) for description
    x - see setSourceImage(byte[], int, int, int, int, int, int) for description
    y - see setSourceImage(byte[], int, int, int, int, int, int) for description
    width - see setSourceImage(byte[], int, int, int, int, int, int) for description
    pitch - see setSourceImage(byte[], int, int, int, int, int, int) for description
    height - see setSourceImage(byte[], int, int, int, int, int, int) for description
    pixelFormat - pixel format of the source image (one of @@ -445,7 +445,7 @@

    TJCompressor

    int width, int height) throws TJException -
    Create a TurboJPEG compressor instance and associate the uncompressed +
    Create a TurboJPEG compressor instance and associate the packed-pixel source image stored in srcImage with the newly created instance.
    Parameters:
    srcImage - see @@ -480,20 +480,22 @@

    setSourceImage

    int height, int pixelFormat) throws TJException -
    Associate an uncompressed RGB, grayscale, or CMYK source image with this +
    Associate a packed-pixel RGB, grayscale, or CMYK source image with this compressor instance.
    -
    Parameters:
    srcImage - image buffer containing RGB, grayscale, or CMYK pixels to - be compressed or encoded. This buffer is not modified.
    x - x offset (in pixels) of the region in the source image from which +
    Parameters:
    srcImage - buffer containing a packed-pixel RGB, grayscale, or CMYK + source image to be compressed or encoded. This buffer is not modified.
    x - x offset (in pixels) of the region in the source image from which the JPEG or YUV image should be compressed/encoded
    y - y offset (in pixels) of the region in the source image from which the JPEG or YUV image should be compressed/encoded
    width - width (in pixels) of the region in the source image from - which the JPEG or YUV image should be compressed/encoded
    pitch - bytes per line of the source image. Normally, this should be - width * TJ.pixelSize(pixelFormat) if the source image is - unpadded, but you can use this parameter to, for instance, specify that - the scanlines in the source image are padded to a 4-byte boundary or to - compress/encode a JPEG or YUV image from a region of a larger source - image. You can also be clever and use this parameter to skip lines, etc. - Setting this parameter to 0 is the equivalent of setting it to - width * TJ.pixelSize(pixelFormat).
    height - height (in pixels) of the region in the source image from + which the JPEG or YUV image should be compressed/encoded
    pitch - bytes per row in the source image. Normally this should be + width * + TJ.getPixelSize(pixelFormat), + if the source image is unpadded. However, you can use this parameter to, + for instance, specify that the rows in the source image are padded to the + nearest multiple of 4 bytes or to compress/encode a JPEG or YUV image from + a region of a larger source image. You can also be clever and use this + parameter to skip rows, etc. Setting this parameter to 0 is the + equivalent of setting it to width * + TJ.getPixelSize(pixelFormat).
    height - height (in pixels) of the region in the source image from which the JPEG or YUV image should be compressed/encoded
    pixelFormat - pixel format of the source image (one of TJ.PF_*)
    Throws:
    @@ -531,10 +533,11 @@

    setSourceImage

    int width, int height) throws TJException -
    Associate an uncompressed RGB or grayscale source image with this +
    Associate a packed-pixel RGB or grayscale source image with this compressor instance.
    -
    Parameters:
    srcImage - a BufferedImage instance containing RGB or - grayscale pixels to be compressed or encoded. This image is not modified.
    x - x offset (in pixels) of the region in the source image from which +
    Parameters:
    srcImage - a BufferedImage instance containing a + packed-pixel RGB or grayscale source image to be compressed or encoded. + This image is not modified.
    x - x offset (in pixels) of the region in the source image from which the JPEG or YUV image should be compressed/encoded
    y - y offset (in pixels) of the region in the source image from which the JPEG or YUV image should be compressed/encoded
    width - width (in pixels) of the region in the source image from which the JPEG or YUV image should be compressed/encoded (0 = use the @@ -553,10 +556,9 @@

    setSourceImage

    setSourceImage

    public void setSourceImage(YUVImage srcImage)
                         throws TJException
    -
    Associate an uncompressed YUV planar source image with this compressor - instance.
    -
    Parameters:
    srcImage - YUV planar image to be compressed. This image is not - modified.
    +
    Associate a planar YUV source image with this compressor instance.
    +
    Parameters:
    srcImage - planar YUV source image to be compressed. This image is + not modified.
    Throws:
    TJException
    @@ -573,16 +575,16 @@

    setSubsamp

    TJ.CS_YCbCr) or from CMYK to YCCK (see TJ.CS_YCCK) as part of the JPEG compression process, some of the Cb and Cr (chrominance) components can be discarded or averaged together to produce a smaller - image with little perceptible loss of image clarity (the human eye is more - sensitive to small changes in brightness than to small changes in color.) - This is called "chrominance subsampling". + image with little perceptible loss of image clarity. (The human eye is + more sensitive to small changes in brightness than to small changes in + color.) This is called "chrominance subsampling".

    - NOTE: This method has no effect when compressing a JPEG image from a YUV - planar source. In that case, the level of chrominance subsampling in - the JPEG image is determined by the source. Furthermore, this method has - no effect when encoding to a pre-allocated YUVImage instance. In - that case, the level of chrominance subsampling is determined by the - destination.

    + NOTE: This method has no effect when compressing a JPEG image from a + planar YUV source image. In that case, the level of chrominance + subsampling in the JPEG image is determined by the source image. + Furthermore, this method has no effect when encoding to a pre-allocated + YUVImage instance. In that case, the level of chrominance + subsampling is determined by the destination image.
    Parameters:
    newSubsamp - the level of chrominance subsampling to use in subsequent compress/encode oeprations (one of TJ.SAMP_*)
    @@ -609,8 +611,9 @@

    compress

    public void compress(byte[] dstBuf,
                 int flags)
                   throws TJException
    -
    Compress the uncompressed source image associated with this compressor - instance and output a JPEG image to the given destination buffer.
    +
    Compress the packed-pixel or planar YUV source image associated with this + compressor instance and output a JPEG image to the given destination + buffer.
    Parameters:
    dstBuf - buffer that will receive the JPEG image. Use TJ.bufSize(int, int, int) to determine the maximum size for this buffer based on the source image's width and height and the desired level of chrominance @@ -628,8 +631,8 @@

    compress

    compress

    public byte[] compress(int flags)
                     throws TJException
    -
    Compress the uncompressed source image associated with this compressor - instance and return a buffer containing a JPEG image.
    +
    Compress the packed-pixel or planar YUV source image associated with this + compressor instance and return a buffer containing a JPEG image.
    Parameters:
    flags - the bitwise OR of one or more of TJ.FLAG_*
    Returns:
    a buffer containing a JPEG image. The length of this buffer will @@ -682,13 +685,13 @@

    encodeYUV

    public void encodeYUV(YUVImage dstImage,
                  int flags)
                    throws TJException
    -
    Encode the uncompressed source image associated with this compressor - instance into a YUV planar image and store it in the given - YUVImage instance. This method uses the accelerated color - conversion routines in TurboJPEG's underlying codec but does not execute - any of the other steps in the JPEG compression process. Encoding - CMYK source images to YUV is not supported.
    -
    Parameters:
    dstImage - YUVImage instance that will receive the YUV planar +
    Encode the packed-pixel source image associated with this compressor + instance into a planar YUV image and store it in the given + YUVImage instance. This method performs color conversion (which + is accelerated in the libjpeg-turbo implementation) but does not execute + any of the other steps in the JPEG compression process. Encoding CMYK + source images into YUV images is not supported.
    +
    Parameters:
    dstImage - YUVImage instance that will receive the planar YUV image
    flags - the bitwise OR of one or more of TJ.FLAG_*
    Throws:
    @@ -716,20 +719,21 @@

    encodeYUV

    • encodeYUV

      -
      public YUVImage encodeYUV(int pad,
      +
      public YUVImage encodeYUV(int align,
                        int flags)
                          throws TJException
      -
      Encode the uncompressed source image associated with this compressor - instance into a unified YUV planar image buffer and return a - YUVImage instance containing the encoded image. This method - uses the accelerated color conversion routines in TurboJPEG's underlying - codec but does not execute any of the other steps in the JPEG compression - process. Encoding CMYK source images to YUV is not supported.
      -
      Parameters:
      pad - the width of each line in each plane of the YUV image will be - padded to the nearest multiple of this number of bytes (must be a power of - 2.)
      flags - the bitwise OR of one or more of +
      Encode the packed-pixel source image associated with this compressor + instance into a unified planar YUV image and return a YUVImage + instance containing the encoded image. This method performs color + conversion (which is accelerated in the libjpeg-turbo implementation) but + does not execute any of the other steps in the JPEG compression process. + Encoding CMYK source images into YUV images is not supported.
      +
      Parameters:
      align - row alignment (in bytes) of the YUV image (must be a power of + 2.) Setting this parameter to n will cause each row in each plane of the + YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.)
      flags - the bitwise OR of one or more of TJ.FLAG_*
      -
      Returns:
      a YUV planar image.
      +
      Returns:
      a YUVImage instance containing the unified planar YUV + encoded image
      Throws:
      TJException
    • @@ -743,21 +747,22 @@

      encodeYUV

      public YUVImage encodeYUV(int[] strides,
                        int flags)
                          throws TJException
      -
      Encode the uncompressed source image associated with this compressor +
      Encode the packed-pixel source image associated with this compressor instance into separate Y, U (Cb), and V (Cr) image planes and return a - YUVImage instance containing the encoded image planes. This - method uses the accelerated color conversion routines in TurboJPEG's - underlying codec but does not execute any of the other steps in the JPEG - compression process. Encoding CMYK source images to YUV is not supported.
      + YUVImage instance containing the encoded image planes. This + method performs color conversion (which is accelerated in the + libjpeg-turbo implementation) but does not execute any of the other steps + in the JPEG compression process. Encoding CMYK source images into YUV + images is not supported.
      Parameters:
      strides - an array of integers, each specifying the number of bytes - per line in the corresponding plane of the output image. Setting the - stride for any plane to 0 is the same as setting it to the component width - of the plane. If strides is null, then the strides for all - planes will be set to their respective component widths. You can adjust - the strides in order to add an arbitrary amount of line padding to each - plane.
      flags - the bitwise OR of one or more of + per row in the corresponding plane of the YUV source image. Setting the + stride for any plane to 0 is the same as setting it to the plane width + (see YUVImage.) If strides is null, then the strides + for all planes will be set to their respective plane widths. You can + adjust the strides in order to add an arbitrary amount of row padding to + each plane.
      flags - the bitwise OR of one or more of TJ.FLAG_*
      -
      Returns:
      a YUV planar image.
      +
      Returns:
      a YUVImage instance containing the encoded image planes
      Throws:
      TJException
      diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCustomFilter.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCustomFilter.html index 412dcd46794..982079c47cc 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCustomFilter.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJCustomFilter.html @@ -163,7 +163,7 @@

      customFilter

      into multiple DCT coefficient buffers and call the callback function once for each buffer.
    planeRegion - rectangle containing the width and height of the component plane to which coeffBuffer belongs
    componentID - ID number of the component plane to which - coeffBuffer belongs (Y, Cb, and Cr have, respectively, ID's + coeffBuffer belongs. (Y, Cb, and Cr have, respectively, ID's of 0, 1, and 2 in typical JPEG images.)
    transformID - ID number of the transformed image to which coeffBuffer belongs. This is the same as the index of the transform in the transforms array that was passed to TJTransformer.transform().
    transform - a TJTransform instance that specifies the diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJDecompressor.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJDecompressor.html index b281e327a2e..77a7ab6965c 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJDecompressor.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJDecompressor.html @@ -180,20 +180,22 @@

    Constructor Summary

    @@ -223,9 +225,10 @@

    Method Summary

    @@ -252,9 +255,9 @@

    Method Summary

    int desiredHeight, int pixelFormat, int flags) -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a grayscale, RGB, or CMYK image - to the given destination buffer.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + grayscale, RGB, or CMYK image to the given destination buffer.
    @@ -267,9 +270,9 @@

    Method Summary

    int desiredHeight, int pixelFormat, int flags) -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a grayscale, RGB, or CMYK image - to the given destination buffer.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + grayscale, RGB, or CMYK image to the given destination buffer.
    @@ -278,9 +281,10 @@

    Method Summary

    int desiredHeight, int bufferedImageType, int flags) -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and return a BufferedImage - instance containing the decompressed/decoded image.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and return a + BufferedImage instance containing the packed-pixel + decompressed/decoded image.
    @@ -290,8 +294,9 @@

    Method Summary

    int desiredHeight, int pixelFormat, int flags) -
    Decompress the JPEG source image associated with this decompressor - instance and return a buffer containing the decompressed image.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and return a buffer containing + the packed-pixel decompressed image.
    @@ -319,18 +324,18 @@

    Method Summary

    int flags)
    Decompress the JPEG source image associated with this decompressor instance into a set of Y, U (Cb), and V (Cr) image planes and return a - YUVImage instance containing the decompressed image planes.
    + YUVImage instance containing the decompressed image planes.
    @@ -338,8 +343,8 @@

    Method Summary

    @@ -363,7 +368,7 @@

    Method Summary

    @@ -418,14 +423,15 @@

    Method Summary

    @@ -553,9 +559,11 @@

    TJDecompressor

    public TJDecompressor(byte[] jpegImage)
                    throws TJException
    Create a TurboJPEG decompressor instance and associate the JPEG source - image stored in jpegImage with the newly created instance.
    -
    Parameters:
    jpegImage - JPEG image buffer (size of the JPEG image is assumed to - be the length of the array.) This buffer is not modified.
    + image or "abbreviated table specification" (AKA "tables-only") datastream + stored in jpegImage with the newly created instance. +
    Parameters:
    jpegImage - buffer containing a JPEG source image or tables-only + datastream. (The size of the JPEG image or datastream is assumed to be + the length of the array.) This buffer is not modified.
    Throws:
    TJException
    @@ -570,9 +578,12 @@

    TJDecompressor

    int imageSize) throws TJException
    Create a TurboJPEG decompressor instance and associate the JPEG source - image of length imageSize bytes stored in - jpegImage with the newly created instance.
    -
    Parameters:
    jpegImage - JPEG image buffer. This buffer is not modified.
    imageSize - size of the JPEG image (in bytes)
    + image or "abbreviated table specification" (AKA "tables-only") datastream + of length imageSize bytes stored in jpegImage + with the newly created instance. +
    Parameters:
    jpegImage - buffer containing a JPEG source image or tables-only + datastream. This buffer is not modified.
    imageSize - size of the JPEG source image or tables-only datastream + (in bytes)
    Throws:
    TJException
    @@ -585,10 +596,10 @@

    TJDecompressor

    TJDecompressor

    public TJDecompressor(YUVImage yuvImage)
                    throws TJException
    -
    Create a TurboJPEG decompressor instance and associate the YUV planar +
    Create a TurboJPEG decompressor instance and associate the planar YUV source image stored in yuvImage with the newly created instance.
    -
    Parameters:
    yuvImage - YUVImage instance containing a YUV planar +
    Parameters:
    yuvImage - YUVImage instance containing a planar YUV source image to be decoded. This image is not modified.
    Throws:
    TJException
    @@ -611,10 +622,19 @@

    setSourceImage

    public void setSourceImage(byte[] jpegImage,
                       int imageSize)
                         throws TJException
    -
    Associate the JPEG image of length imageSize bytes stored in - jpegImage with this decompressor instance. This image will - be used as the source image for subsequent decompress operations.
    -
    Parameters:
    jpegImage - JPEG image buffer. This buffer is not modified.
    imageSize - size of the JPEG image (in bytes)
    +
    Associate the JPEG image or "abbreviated table specification" (AKA + "tables-only") datastream of length imageSize bytes stored in + jpegImage with this decompressor instance. If + jpegImage contains a JPEG image, then this image will be used + as the source image for subsequent decompression operations. Passing a + tables-only datastream to this method primes the decompressor with + quantization and Huffman tables that can be used when decompressing + subsequent "abbreviated image" datastreams. This is useful, for instance, + when decompressing video streams in which all frames share the same + quantization and Huffman tables.
    +
    Parameters:
    jpegImage - buffer containing a JPEG source image or tables-only + datastream. This buffer is not modified.
    imageSize - size of the JPEG source image or tables-only datastream + (in bytes)
    Throws:
    TJException
    @@ -641,11 +661,11 @@

    setJPEGImage

  • setSourceImage

    public void setSourceImage(YUVImage srcImage)
    -
    Associate the specified YUV planar source image with this decompressor - instance. Subsequent decompress operations will decode this image into an - RGB or grayscale destination image.
    -
    Parameters:
    srcImage - YUVImage instance containing a YUV planar image to - be decoded. This image is not modified.
    +
    Associate the specified planar YUV source image with this decompressor + instance. Subsequent decompression operations will decode this image into + a packed-pixel RGB or grayscale destination image.
    +
    Parameters:
    srcImage - YUVImage instance containing a planar YUV source + image to be decoded. This image is not modified.
  • @@ -709,8 +729,8 @@

    getColorspace

  • getJPEGBuf

    public byte[] getJPEGBuf()
    -
    Returns the JPEG image buffer associated with this decompressor instance.
    -
    Returns:
    the JPEG image buffer associated with this decompressor instance.
    +
    Returns the JPEG buffer associated with this decompressor instance.
    +
    Returns:
    the JPEG buffer associated with this decompressor instance.
  • @@ -738,12 +758,12 @@

    getScaledWidth

    decompressor can generate without exceeding the desired image width and height.
    Parameters:
    desiredWidth - desired width (in pixels) of the decompressed image. - Setting this to 0 is the same as setting it to the width of the JPEG image - (in other words, the width will not be considered when determining the - scaled image size.)
    desiredHeight - desired height (in pixels) of the decompressed image. + Setting this to 0 is the same as setting it to the width of the JPEG + image. (In other words, the width will not be considered when determining + the scaled image size.)
    desiredHeight - desired height (in pixels) of the decompressed image. Setting this to 0 is the same as setting it to the height of the JPEG - image (in other words, the height will not be considered when determining - the scaled image size.)
    + image. (In other words, the height will not be considered when + determining the scaled image size.)
    Returns:
    the width of the largest scaled-down image that the TurboJPEG decompressor can generate without exceeding the desired image width and height.
    @@ -761,12 +781,12 @@

    getScaledHeight

    decompressor can generate without exceeding the desired image width and height.
    Parameters:
    desiredWidth - desired width (in pixels) of the decompressed image. - Setting this to 0 is the same as setting it to the width of the JPEG image - (in other words, the width will not be considered when determining the - scaled image size.)
    desiredHeight - desired height (in pixels) of the decompressed image. + Setting this to 0 is the same as setting it to the width of the JPEG + image. (In other words, the width will not be considered when determining + the scaled image size.)
    desiredHeight - desired height (in pixels) of the decompressed image. Setting this to 0 is the same as setting it to the height of the JPEG - image (in other words, the height will not be considered when determining - the scaled image size.)
    + image. (In other words, the height will not be considered when + determining the scaled image size.)
    Returns:
    the height of the largest scaled-down image that the TurboJPEG decompressor can generate without exceeding the desired image width and height.
    @@ -787,25 +807,26 @@

    decompress

    int pixelFormat, int flags) throws
    TJException -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a grayscale, RGB, or CMYK image - to the given destination buffer. +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + grayscale, RGB, or CMYK image to the given destination buffer.

    - NOTE: The output image is fully recoverable if this method throws a + NOTE: The destination image is fully recoverable if this method throws a non-fatal TJException (unless TJ.FLAG_STOPONWARNING is specified.)

    -
    Parameters:
    dstBuf - buffer that will receive the decompressed/decoded image. - If the source image is a JPEG image, then this buffer should normally be - pitch * scaledHeight bytes in size, where - scaledHeight can be determined by calling - scalingFactor.getScaled(jpegHeight) - with one of the scaling factors returned from TJ.getScalingFactors() or by calling getScaledHeight(int, int). If the - source image is a YUV image, then this buffer should normally be - pitch * height bytes in size, where height is - the height of the YUV image. However, the buffer may also be larger than - the dimensions of the source image, in which case the x, - y, and pitch parameters can be used to specify - the region into which the source image should be decompressed/decoded.
    x - x offset (in pixels) of the region in the destination image into +
    Parameters:
    dstBuf - buffer that will receive the packed-pixel + decompressed/decoded image. If the source image is a JPEG image, then + this buffer should normally be pitch * scaledHeight bytes in + size, where scaledHeight can be determined by calling + scalingFactor.getScaled(jpegHeight) + with one of the scaling factors returned from TJ.getScalingFactors() + or by calling getScaledHeight(int, int). If the source image is a YUV + image, then this buffer should normally be pitch * height + bytes in size, where height is the height of the YUV image. + However, the buffer may also be larger than the dimensions of the source + image, in which case the x, y, and + pitch parameters can be used to specify the region into which + the source image should be decompressed/decoded.
    x - x offset (in pixels) of the region in the destination image into which the source image should be decompressed/decoded
    y - y offset (in pixels) of the region in the destination image into which the source image should be decompressed/decoded
    desiredWidth - If the source image is a JPEG image, then this specifies the desired width (in pixels) of the decompressed image (or @@ -813,27 +834,29 @@

    decompress

    than the source image dimensions, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting - it to the width of the JPEG image (in other words, the width will not be + it to the width of the JPEG image. (In other words, the width will not be considered when determining the scaled image size.) This parameter is - ignored if the source image is a YUV image.
    pitch - bytes per line of the destination image. Normally, this - should be set to scaledWidth * TJ.pixelSize(pixelFormat) if - the destination image is unpadded, but you can use this to, for instance, - pad each line of the destination image to a 4-byte boundary or to - decompress/decode the source image into a region of a larger image. NOTE: - if the source image is a JPEG image, then scaledWidth can be - determined by calling - scalingFactor.getScaled(jpegWidth) - or by calling getScaledWidth(int, int). If the source image is a - YUV image, then scaledWidth is the width of the YUV image. + ignored if the source image is a YUV image.
    pitch - bytes per row in the destination image. Normally this should + be set to scaledWidth * + TJ.getPixelSize(pixelFormat), + if the destination image will be unpadded. However, you can use this to, + for instance, pad each row of the destination image to the nearest + multiple of 4 bytes or to decompress/decode the source image into a region + of a larger image. NOTE: if the source image is a JPEG image, then + scaledWidth can be determined by calling + scalingFactor.getScaled(jpegWidth) + or by calling getScaledWidth(int, int). If the source image is a YUV + image, then scaledWidth is the width of the YUV image. Setting this parameter to 0 is the equivalent of setting it to - scaledWidth * TJ.pixelSize(pixelFormat).
    desiredHeight - If the source image is a JPEG image, then this + scaledWidth * + TJ.getPixelSize(pixelFormat).
    desiredHeight - If the source image is a JPEG image, then this specifies the desired height (in pixels) of the decompressed image (or image region.) If the desired destination image dimensions are different than the source image dimensions, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting - it to the height of the JPEG image (in other words, the height will not be - considered when determining the scaled image size.) This parameter is + it to the height of the JPEG image. (In other words, the height will not + be considered when determining the scaled image size.) This parameter is ignored if the source image is a YUV image.
    pixelFormat - pixel format of the decompressed/decoded image (one of TJ.PF_*)
    flags - the bitwise OR of one or more of TJ.FLAG_*
    @@ -873,8 +896,9 @@

    decompress

    int pixelFormat, int flags) throws TJException -
    Decompress the JPEG source image associated with this decompressor - instance and return a buffer containing the decompressed image.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and return a buffer containing + the packed-pixel decompressed image.
    Parameters:
    desiredWidth - see decompress(byte[], int, int, int, int, int, int, int) for description
    pitch - see @@ -884,7 +908,7 @@

    decompress

    for description
    pixelFormat - pixel format of the decompressed image (one of TJ.PF_*)
    flags - the bitwise OR of one or more of TJ.FLAG_*
    -
    Returns:
    a buffer containing the decompressed image.
    +
    Returns:
    a buffer containing the packed-pixel decompressed image.
    Throws:
    TJException
    @@ -899,21 +923,21 @@

    decompressToYUV

    int flags) throws TJException
    Decompress the JPEG source image associated with this decompressor - instance into a YUV planar image and store it in the given - YUVImage instance. This method performs JPEG decompression - but leaves out the color conversion step, so a planar YUV image is - generated instead of an RGB or grayscale image. This method cannot be - used to decompress JPEG source images with the CMYK or YCCK colorspace. + instance into a planar YUV image and store it in the given + YUVImage instance. This method performs JPEG decompression but + leaves out the color conversion step, so a planar YUV image is generated + instead of a packed-pixel image. This method cannot be used to decompress + JPEG source images with the CMYK or YCCK colorspace.

    - NOTE: The YUV planar output image is fully recoverable if this method + NOTE: The planar YUV destination image is fully recoverable if this method throws a non-fatal TJException (unless TJ.FLAG_STOPONWARNING is specified.)

    -
    Parameters:
    dstImage - YUVImage instance that will receive the YUV planar - image. The level of subsampling specified in this YUVImage - instance must match that of the JPEG image, and the width and height - specified in the YUVImage instance must match one of the - scaled image sizes that TurboJPEG is capable of generating from the JPEG - source image.
    flags - the bitwise OR of one or more of +
    Parameters:
    dstImage - YUVImage instance that will receive the planar YUV + decompressed image. The level of subsampling specified in this + YUVImage instance must match that of the JPEG image, and the width + and height specified in the YUVImage instance must match one of + the scaled image sizes that the decompressor is capable of generating from + the JPEG source image.
    flags - the bitwise OR of one or more of TJ.FLAG_*
    Throws:
    TJException
    @@ -947,32 +971,33 @@

    decompressToYUV

    throws TJException
    Decompress the JPEG source image associated with this decompressor instance into a set of Y, U (Cb), and V (Cr) image planes and return a - YUVImage instance containing the decompressed image planes. - This method performs JPEG decompression but leaves out the color - conversion step, so a planar YUV image is generated instead of an RGB or - grayscale image. This method cannot be used to decompress JPEG source - images with the CMYK or YCCK colorspace.
    + YUVImage instance containing the decompressed image planes. This + method performs JPEG decompression but leaves out the color conversion + step, so a planar YUV image is generated instead of a packed-pixel image. + This method cannot be used to decompress JPEG source images with the CMYK + or YCCK colorspace.
    Parameters:
    desiredWidth - desired width (in pixels) of the YUV image. If the desired image dimensions are different than the dimensions of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting it to - the width of the JPEG image (in other words, the width will not be + the width of the JPEG image. (In other words, the width will not be considered when determining the scaled image size.)
    strides - an array of integers, each specifying the number of bytes - per line in the corresponding plane of the output image. Setting the - stride for any plane to 0 is the same as setting it to the scaled - component width of the plane. If strides is NULL, then the - strides for all planes will be set to their respective scaled component - widths. You can adjust the strides in order to add an arbitrary amount of - line padding to each plane.
    desiredHeight - desired height (in pixels) of the YUV image. If the + per row in the corresponding plane of the YUV image. Setting the stride + for any plane to 0 is the same as setting it to the scaled plane width + (see YUVImage.) If strides is null, then the strides + for all planes will be set to their respective scaled plane widths. You + can adjust the strides in order to add an arbitrary amount of row padding + to each plane.
    desiredHeight - desired height (in pixels) of the YUV image. If the desired image dimensions are different than the dimensions of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting it to - the height of the JPEG image (in other words, the height will not be + the height of the JPEG image. (In other words, the height will not be considered when determining the scaled image size.)
    flags - the bitwise OR of one or more of TJ.FLAG_*
    -
    Returns:
    a YUV planar image.
    +
    Returns:
    a YUVImage instance containing the decompressed image + planes
    Throws:
    TJException
    @@ -984,34 +1009,34 @@

    decompressToYUV

  • decompressToYUV

    public YUVImage decompressToYUV(int desiredWidth,
    -                       int pad,
    +                       int align,
                            int desiredHeight,
                            int flags)
                              throws TJException
    Decompress the JPEG source image associated with this decompressor - instance into a unified YUV planar image buffer and return a - YUVImage instance containing the decompressed image. This - method performs JPEG decompression but leaves out the color conversion - step, so a planar YUV image is generated instead of an RGB or grayscale - image. This method cannot be used to decompress JPEG source images with - the CMYK or YCCK colorspace.
    + instance into a unified planar YUV image and return a YUVImage + instance containing the decompressed image. This method performs JPEG + decompression but leaves out the color conversion step, so a planar YUV + image is generated instead of a packed-pixel image. This method cannot be + used to decompress JPEG source images with the CMYK or YCCK colorspace.
    Parameters:
    desiredWidth - desired width (in pixels) of the YUV image. If the desired image dimensions are different than the dimensions of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting it to - the width of the JPEG image (in other words, the width will not be - considered when determining the scaled image size.)
    pad - the width of each line in each plane of the YUV image will be - padded to the nearest multiple of this number of bytes (must be a power of - 2.)
    desiredHeight - desired height (in pixels) of the YUV image. If the + the width of the JPEG image. (In other words, the width will not be + considered when determining the scaled image size.)
    align - row alignment (in bytes) of the YUV image (must be a power of + 2.) Setting this parameter to n will cause each row in each plane of the + YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.)
    desiredHeight - desired height (in pixels) of the YUV image. If the desired image dimensions are different than the dimensions of the JPEG image being decompressed, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting it to - the height of the JPEG image (in other words, the height will not be + the height of the JPEG image. (In other words, the height will not be considered when determining the scaled image size.)
    flags - the bitwise OR of one or more of TJ.FLAG_*
    -
    Returns:
    a YUV planar image.
    +
    Returns:
    a YUVImage instance containing the unified planar YUV + decompressed image
    Throws:
    TJException
  • @@ -1045,25 +1070,26 @@

    decompress

    int pixelFormat, int flags) throws TJException -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a grayscale, RGB, or CMYK image - to the given destination buffer. +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + grayscale, RGB, or CMYK image to the given destination buffer.

    - NOTE: The output image is fully recoverable if this method throws a + NOTE: The destination image is fully recoverable if this method throws a non-fatal TJException (unless TJ.FLAG_STOPONWARNING is specified.)

    -
    Parameters:
    dstBuf - buffer that will receive the decompressed/decoded image. - If the source image is a JPEG image, then this buffer should normally be - stride * scaledHeight pixels in size, where - scaledHeight can be determined by calling - scalingFactor.getScaled(jpegHeight) - with one of the scaling factors returned from TJ.getScalingFactors() or by calling getScaledHeight(int, int). If the - source image is a YUV image, then this buffer should normally be - stride * height pixels in size, where height is - the height of the YUV image. However, the buffer may also be larger than - the dimensions of the JPEG image, in which case the x, - y, and stride parameters can be used to specify - the region into which the source image should be decompressed.
    x - x offset (in pixels) of the region in the destination image into +
    Parameters:
    dstBuf - buffer that will receive the packed-pixel + decompressed/decoded image. If the source image is a JPEG image, then + this buffer should normally be stride * scaledHeight pixels + in size, where scaledHeight can be determined by calling + scalingFactor.getScaled(jpegHeight) + with one of the scaling factors returned from TJ.getScalingFactors() + or by calling getScaledHeight(int, int). If the source image is a YUV + image, then this buffer should normally be stride * height + pixels in size, where height is the height of the YUV image. + However, the buffer may also be larger than the dimensions of the JPEG + image, in which case the x, y, and + stride parameters can be used to specify the region into + which the source image should be decompressed.
    x - x offset (in pixels) of the region in the destination image into which the source image should be decompressed/decoded
    y - y offset (in pixels) of the region in the destination image into which the source image should be decompressed/decoded
    desiredWidth - If the source image is a JPEG image, then this specifies the desired width (in pixels) of the decompressed image (or @@ -1071,16 +1097,16 @@

    decompress

    than the source image dimensions, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting - it to the width of the JPEG image (in other words, the width will not be + it to the width of the JPEG image. (In other words, the width will not be considered when determining the scaled image size.) This parameter is - ignored if the source image is a YUV image.
    stride - pixels per line of the destination image. Normally, this + ignored if the source image is a YUV image.
    stride - pixels per row in the destination image. Normally this should be set to scaledWidth, but you can use this to, for instance, decompress the JPEG image into a region of a larger image. NOTE: if the source image is a JPEG image, then scaledWidth - can be determined by calling - scalingFactor.getScaled(jpegWidth) - or by calling getScaledWidth(int, int). If the source image is a - YUV image, then scaledWidth is the width of the YUV image. + can be determined by calling + scalingFactor.getScaled(jpegWidth) + or by calling getScaledWidth(int, int). If the source image is a YUV + image, then scaledWidth is the width of the YUV image. Setting this parameter to 0 is the equivalent of setting it to scaledWidth.
    desiredHeight - If the source image is a JPEG image, then this specifies the desired height (in pixels) of the decompressed image (or @@ -1088,8 +1114,8 @@

    decompress

    than the source image dimensions, then TurboJPEG will use scaling in the JPEG decompressor to generate the largest possible image that will fit within the desired dimensions. Setting this to 0 is the same as setting - it to the height of the JPEG image (in other words, the height will not be - considered when determining the scaled image size.) This parameter is + it to the height of the JPEG image. (In other words, the height will not + be considered when determining the scaled image size.) This parameter is ignored if the source image is a YUV image.
    pixelFormat - pixel format of the decompressed image (one of TJ.PF_*)
    flags - the bitwise OR of one or more of TJ.FLAG_*
    @@ -1106,20 +1132,21 @@

    decompress

    public void decompress(java.awt.image.BufferedImage dstImage,
                   int flags)
                     throws TJException
    -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a decompressed/decoded image to - the given BufferedImage instance. +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + decompressed/decoded image to the given BufferedImage + instance.

    - NOTE: The output image is fully recoverable if this method throws a + NOTE: The destination image is fully recoverable if this method throws a non-fatal TJException (unless TJ.FLAG_STOPONWARNING is specified.)

    Parameters:
    dstImage - a BufferedImage instance that will receive - the decompressed/decoded image. If the source image is a JPEG image, then - the width and height of the BufferedImage instance must match - one of the scaled image sizes that TurboJPEG is capable of generating from - the JPEG image. If the source image is a YUV image, then the width and - height of the BufferedImage instance must match the width and - height of the YUV image.
    flags - the bitwise OR of one or more of + the packed-pixel decompressed/decoded image. If the source image is a + JPEG image, then the width and height of the BufferedImage + instance must match one of the scaled image sizes that the decompressor is + capable of generating from the JPEG image. If the source image is a YUV + image, then the width and height of the BufferedImage + instance must match the width and height of the YUV image.
    flags - the bitwise OR of one or more of TJ.FLAG_*
    Throws:
    TJException
    @@ -1136,9 +1163,10 @@

    decompress

    int bufferedImageType, int flags) throws TJException -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and return a BufferedImage - instance containing the decompressed/decoded image.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and return a + BufferedImage instance containing the packed-pixel + decompressed/decoded image.
    Parameters:
    desiredWidth - see decompress(byte[], int, int, int, int, int, int, int) for description
    desiredHeight - see @@ -1147,7 +1175,7 @@

    decompress

    instance that will be created (for instance, BufferedImage.TYPE_INT_RGB)
    flags - the bitwise OR of one or more of TJ.FLAG_*
    -
    Returns:
    a BufferedImage instance containing the +
    Returns:
    a BufferedImage instance containing the packed-pixel decompressed/decoded image.
    Throws:
    TJException
    diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransform.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransform.html index 5f22691efbb..e528d791d8d 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransform.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransform.html @@ -167,7 +167,7 @@

    Field Summary

    @@ -223,7 +223,7 @@

    Field Summary

    @@ -235,8 +235,8 @@

    Field Summary

    @@ -256,7 +256,7 @@

    Field Summary

    @@ -270,7 +270,8 @@

    Field Summary

    128
    + +public static final intFLAG_LIMITSCANS32768
    public static final int FLAG_PROGRESSIVE 16384
    public static final int FLAG_STOPONWARNING 8192
    public static final int NUMCS 5
    public static final int NUMERR 2
    public static final int NUMPF 12
    public static final int NUMSAMP 6
    public static final int PF_ABGR 9
    public static final int PF_ARGB 10
    public static final int PF_BGR 1
    public static final int PF_BGRA 8
    public static final int PF_BGRX 3
    public static final int PF_CMYK 11
    public static final int PF_GRAY 6
    public static final int PF_RGB 0
    public static final int PF_RGBA 7
    public static final int PF_RGBX 2
    public static final int PF_XBGR 4
    public static final int PF_XRGB 5
    public static final int SAMP_411 5
    public static final int SAMP_420 2
    public static final int SAMP_422 1
    public static final int SAMP_440 4
    public static final int SAMP_444 0
    public static final int
    static int ERR_WARNING -
    The error was non-fatal and recoverable, but the image may still be - corrupt.
    +
    The error was non-fatal and recoverable, but the destination image may + still be corrupt.
    static int FLAG_ACCURATEDCT -
    Use the most accurate DCT/IDCT algorithm available in the underlying - codec.
    +
    Use the most accurate DCT/IDCT algorithm available.
    static int FLAG_BOTTOMUP -
    The uncompressed source/destination image is stored in bottom-up (Windows, - OpenGL) order, not top-down (X11) order.
    +
    Rows in the packed-pixel source/destination image are stored in bottom-up + (Windows, OpenGL) order rather than in top-down (X11) order.
    static int FLAG_FASTDCT -
    Use the fastest DCT/IDCT algorithm available in the underlying codec.
    +
    Use the fastest DCT/IDCT algorithm available.
    static int FLAG_FASTUPSAMPLE
    When decompressing an image that was compressed using chrominance - subsampling, use the fastest chrominance upsampling algorithm available in - the underlying codec.
    + subsampling, use the fastest chrominance upsampling algorithm available.
    static intFLAG_LIMITSCANS +
    Limit the number of progressive JPEG scans that the decompression and + transform operations will process.
    +
    static int FLAG_PROGRESSIVE
    Use progressive entropy coding in JPEG images generated by compression and transform operations.
    static int FLAG_STOPONWARNING
    Immediately discontinue the current compression/decompression/transform - operation if the underlying codec throws a warning (non-fatal error).
    + operation if a warning (non-fatal error) occurs.
    static int NUMCS
    The number of JPEG colorspaces
    static int NUMERR
    The number of error codes
    static int NUMPF
    The number of pixel formats
    static int NUMSAMP
    The number of chrominance subsampling options
    static int PF_ABGR
    ABGR pixel format.
    static int PF_ARGB
    ARGB pixel format.
    static int PF_BGR
    BGR pixel format.
    static int PF_BGRA
    BGRA pixel format.
    static int PF_BGRX
    BGRX pixel format.
    static int PF_CMYK
    CMYK pixel format.
    static int PF_GRAY
    Grayscale pixel format.
    static int PF_RGB
    RGB pixel format.
    static int PF_RGBA
    RGBA pixel format.
    static int PF_RGBX
    RGBX pixel format.
    static int PF_XBGR
    XBGR pixel format.
    static int PF_XRGB
    XRGB pixel format.
    static int SAMP_411
    4:1:1 chrominance subsampling.
    static int SAMP_420
    4:2:0 chrominance subsampling.
    static int SAMP_422
    4:2:2 chrominance subsampling.
    static int SAMP_440
    4:4:0 chrominance subsampling.
    static int SAMP_444
    4:4:4 chrominance subsampling (no chrominance subsampling).
    static int SAMP_GRAY
    Grayscale.
    @@ -395,11 +400,12 @@

    Method Summary

    static int bufSizeYUV(int width, - int pad, + int align, int height, int subsamp) -
    Returns the size of the buffer (in bytes) required to hold a YUV planar - image with the given width, height, and level of chrominance subsampling.
    +
    Returns the size of the buffer (in bytes) required to hold a unified + planar YUV image with the given width, height, and level of chrominance + subsampling.
    static TJScalingFactor[] getScalingFactors() -
    Returns a list of fractional scaling factors that the JPEG decompressor in - this implementation of TurboJPEG supports.
    +
    Returns a list of fractional scaling factors that the JPEG decompressor + supports.
    void compress(byte[] dstBuf, int flags) -
    Compress the uncompressed source image associated with this compressor - instance and output a JPEG image to the given destination buffer.
    +
    Compress the packed-pixel or planar YUV source image associated with this + compressor instance and output a JPEG image to the given destination + buffer.
    byte[] compress(int flags) -
    Compress the uncompressed source image associated with this compressor - instance and return a buffer containing a JPEG image.
    +
    Compress the packed-pixel or planar YUV source image associated with this + compressor instance and return a buffer containing a JPEG image.
    YUVImage encodeYUV(int[] strides, int flags) -
    Encode the uncompressed source image associated with this compressor +
    Encode the packed-pixel source image associated with this compressor instance into separate Y, U (Cb), and V (Cr) image planes and return a - YUVImage instance containing the encoded image planes.
    + YUVImage instance containing the encoded image planes.
    YUVImageencodeYUV(int pad, +encodeYUV(int align, int flags) -
    Encode the uncompressed source image associated with this compressor - instance into a unified YUV planar image buffer and return a - YUVImage instance containing the encoded image.
    +
    Encode the packed-pixel source image associated with this compressor + instance into a unified planar YUV image and return a YUVImage + instance containing the encoded image.
    void encodeYUV(YUVImage dstImage, int flags) -
    Encode the uncompressed source image associated with this compressor - instance into a YUV planar image and store it in the given - YUVImage instance.
    +
    Encode the packed-pixel source image associated with this compressor + instance into a planar YUV image and store it in the given + YUVImage instance.
    void setSourceImage(YUVImage srcImage) -
    Associate an uncompressed YUV planar source image with this compressor - instance.
    +
    Associate a planar YUV source image with this compressor instance.
    TJDecompressor(byte[] jpegImage)
    Create a TurboJPEG decompressor instance and associate the JPEG source - image stored in jpegImage with the newly created instance.
    + image or "abbreviated table specification" (AKA "tables-only") datastream + stored in jpegImage with the newly created instance.
    TJDecompressor(byte[] jpegImage, int imageSize)
    Create a TurboJPEG decompressor instance and associate the JPEG source - image of length imageSize bytes stored in - jpegImage with the newly created instance.
    + image or "abbreviated table specification" (AKA "tables-only") datastream + of length imageSize bytes stored in jpegImage + with the newly created instance.
    TJDecompressor(YUVImage yuvImage) -
    Create a TurboJPEG decompressor instance and associate the YUV planar +
    Create a TurboJPEG decompressor instance and associate the planar YUV source image stored in yuvImage with the newly created instance.
    void decompress(java.awt.image.BufferedImage dstImage, int flags) -
    Decompress the JPEG source image or decode the YUV source image associated - with this decompressor instance and output a decompressed/decoded image to - the given BufferedImage instance.
    +
    Decompress the JPEG source image or decode the planar YUV source image + associated with this decompressor instance and output a packed-pixel + decompressed/decoded image to the given BufferedImage + instance.
    YUVImage decompressToYUV(int desiredWidth, - int pad, + int align, int desiredHeight, int flags)
    Decompress the JPEG source image associated with this decompressor - instance into a unified YUV planar image buffer and return a - YUVImage instance containing the decompressed image.
    + instance into a unified planar YUV image and return a YUVImage + instance containing the decompressed image.
    decompressToYUV(YUVImage dstImage, int flags)
    Decompress the JPEG source image associated with this decompressor - instance into a YUV planar image and store it in the given - YUVImage instance.
    + instance into a planar YUV image and store it in the given + YUVImage instance.
    byte[] getJPEGBuf() -
    Returns the JPEG image buffer associated with this decompressor instance.
    +
    Returns the JPEG buffer associated with this decompressor instance.
    void setSourceImage(byte[] jpegImage, int imageSize) -
    Associate the JPEG image of length imageSize bytes stored in +
    Associate the JPEG image or "abbreviated table specification" (AKA + "tables-only") datastream of length imageSize bytes stored in jpegImage with this decompressor instance.
    void setSourceImage(YUVImage srcImage) -
    Associate the specified YUV planar source image with this decompressor +
    Associate the specified planar YUV source image with this decompressor instance.
    int op -
    Transform operation (one of OP_*)
    +
    Transform operation (one of OP_*)
    static int OPT_COPYNONE
    This option will prevent TJTransformer.transform() from copying any extra markers (including EXIF - and ICC profile data) from the source image to the output image.
    + and ICC profile data) from the source image to the destination image.
    static int OPT_GRAY -
    This option will discard the color data in the input image and produce - a grayscale output image.
    +
    This option will discard the color data in the source image and produce a + grayscale destination image.
    static int OPT_PROGRESSIVE -
    This option will enable progressive entropy coding in the output image +
    This option will enable progressive entropy coding in the JPEG image generated by this particular transform.
    int options -
    Transform options (bitwise OR of one or more of OPT_*)
    +
    Transform options (bitwise OR of one or more of + OPT_*)
    @@ -509,7 +510,7 @@

    OPT_PERFECT

    the level of chrominance subsampling used. If the image's width or height is not evenly divisible by the MCU block size (see TJ.getMCUWidth(int) and TJ.getMCUHeight(int)), then there will be partial MCU blocks on the - right and/or bottom edges. It is not possible to move these partial MCU + right and/or bottom edges. It is not possible to move these partial MCU blocks to the top or left of the image, so any transform that would require that is "imperfect." If this option is not specified, then any partial MCU blocks that cannot be transformed will be left in place, which @@ -547,8 +548,8 @@

    OPT_CROP

  • OPT_GRAY

    public static final int OPT_GRAY
    -
    This option will discard the color data in the input image and produce - a grayscale output image.
    +
    This option will discard the color data in the source image and produce a + grayscale destination image.
    See Also:
    Constant Field Values
  • @@ -573,11 +574,10 @@

    OPT_NOOUTPUT

  • OPT_PROGRESSIVE

    public static final int OPT_PROGRESSIVE
    -
    This option will enable progressive entropy coding in the output image +
    This option will enable progressive entropy coding in the JPEG image generated by this particular transform. Progressive entropy coding will generally improve compression relative to baseline entropy coding (the - default), but it will reduce compression and decompression performance - considerably.
    + default), but it will reduce decompression performance considerably.
    See Also:
    Constant Field Values
  • @@ -589,7 +589,7 @@

    OPT_PROGRESSIVE

    OPT_COPYNONE

    public static final int OPT_COPYNONE
    This option will prevent TJTransformer.transform() from copying any extra markers (including EXIF - and ICC profile data) from the source image to the output image.
    + and ICC profile data) from the source image to the destination image.
    See Also:
    Constant Field Values
    @@ -600,7 +600,7 @@

    OPT_COPYNONE

  • op

    public int op
    -
    Transform operation (one of OP_*)
    +
    Transform operation (one of OP_*)
  • @@ -610,7 +610,8 @@

    op

  • options

    public int options
    -
    Transform options (bitwise OR of one or more of OPT_*)
    +
  • @@ -661,8 +662,8 @@

    TJTransform

    equivalent of setting it to (width of the source JPEG image - x).
    h - the height of the cropping region. Setting this to 0 is the equivalent of setting it to (height of the source JPEG image - - y).
    op - one of the transform operations (OP_*)
    options - the bitwise OR of one or more of the transform options - (OPT_*)
    cf - an instance of an object that implements the TJCustomFilter interface, or null if no custom filter is needed
    + y).
    op - one of the transform operations (OP_*)
    options - the bitwise OR of one or more of the transform options + (OPT_*)
    cf - an instance of an object that implements the TJCustomFilter interface, or null if no custom filter is needed
    @@ -678,8 +679,8 @@

    TJTransform

    Create a new lossless transform instance with the given parameters.
    Parameters:
    r - a Rectangle instance that specifies the cropping region. See TJTransform(int, int, int, int, int, int, TJCustomFilter) for more - detail.
    op - one of the transform operations (OP_*)
    options - the bitwise OR of one or more of the transform options - (OPT_*)
    cf - an instance of an object that implements the TJCustomFilter interface, or null if no custom filter is needed
    + detail.
    op - one of the transform operations (OP_*)
    options - the bitwise OR of one or more of the transform options + (OPT_*)
    cf - an instance of an object that implements the TJCustomFilter interface, or null if no custom filter is needed
    diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransformer.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransformer.html index a30fe30cab0..6436b7fb975 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransformer.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/TJTransformer.html @@ -148,14 +148,15 @@

    Constructor Summary

    TJTransformer(byte[] jpegImage)
    Create a TurboJPEG lossless transformer instance and associate the JPEG - image stored in jpegImage with the newly created instance.
    + source image stored in jpegImage with the newly created + instance. TJTransformer(byte[] jpegImage, int imageSize)
    Create a TurboJPEG lossless transformer instance and associate the JPEG - image of length imageSize bytes stored in + source image of length imageSize bytes stored in jpegImage with the newly created instance.
    @@ -178,7 +179,7 @@

    Method Summary

    int[] getTransformedSizes()
    Returns an array containing the sizes of the transformed JPEG images - generated by the most recent transform operation.
    + (in bytes) generated by the most recent transform operation. @@ -186,18 +187,18 @@

    Method Summary

    transform(byte[][] dstBufs, TJTransform[] transforms, int flags) -
    Losslessly transform the JPEG image associated with this transformer - instance into one or more JPEG images stored in the given destination - buffers.
    +
    Losslessly transform the JPEG source image associated with this + transformer instance into one or more JPEG images stored in the given + destination buffers.
    TJDecompressor[] transform(TJTransform[] transforms, int flags) -
    Losslessly transform the JPEG image associated with this transformer - instance and return an array of TJDecompressor instances, each of - which has a transformed JPEG image associated with it.
    +
    Losslessly transform the JPEG source image associated with this + transformer instance and return an array of TJDecompressor + instances, each of which has a transformed JPEG image associated with it.
    @@ -251,9 +252,11 @@

    TJTransformer

    public TJTransformer(byte[] jpegImage)
                   throws TJException
    Create a TurboJPEG lossless transformer instance and associate the JPEG - image stored in jpegImage with the newly created instance.
    -
    Parameters:
    jpegImage - JPEG image buffer (size of the JPEG image is assumed to - be the length of the array.) This buffer is not modified.
    + source image stored in jpegImage with the newly created + instance. +
    Parameters:
    jpegImage - buffer containing the JPEG source image to transform. + (The size of the JPEG image is assumed to be the length of the array.) + This buffer is not modified.
    Throws:
    TJException
    @@ -268,9 +271,10 @@

    TJTransformer

    int imageSize) throws TJException
    Create a TurboJPEG lossless transformer instance and associate the JPEG - image of length imageSize bytes stored in + source image of length imageSize bytes stored in jpegImage with the newly created instance.
    -
    Parameters:
    jpegImage - JPEG image buffer. This buffer is not modified.
    imageSize - size of the JPEG image (in bytes)
    +
    Parameters:
    jpegImage - buffer containing the JPEG source image to transform. + This buffer is not modified.
    imageSize - size of the JPEG source image (in bytes)
    Throws:
    TJException
    @@ -293,25 +297,26 @@

    transform

    TJTransform[] transforms, int flags) throws TJException -
    Losslessly transform the JPEG image associated with this transformer - instance into one or more JPEG images stored in the given destination - buffers. Lossless transforms work by moving the raw coefficients from one - JPEG image structure to another without altering the values of the - coefficients. While this is typically faster than decompressing the - image, transforming it, and re-compressing it, lossless transforms are not - free. Each lossless transform requires reading and performing Huffman - decoding on all of the coefficients in the source image, regardless of the - size of the destination image. Thus, this method provides a means of - generating multiple transformed images from the same source or of applying - multiple transformations simultaneously, in order to eliminate the need to - read the source coefficients multiple times.
    -
    Parameters:
    dstBufs - an array of image buffers. dstbufs[i] will - receive a JPEG image that has been transformed using the parameters in - transforms[i]. Use TJ.bufSize(int, int, int) to determine the - maximum size for each buffer based on the transformed or cropped width and - height and the level of subsampling used in the source image.
    transforms - an array of TJTransform instances, each of +
    Losslessly transform the JPEG source image associated with this + transformer instance into one or more JPEG images stored in the given + destination buffers. Lossless transforms work by moving the raw + coefficients from one JPEG image structure to another without altering the + values of the coefficients. While this is typically faster than + decompressing the image, transforming it, and re-compressing it, lossless + transforms are not free. Each lossless transform requires reading and + performing Huffman decoding on all of the coefficients in the source + image, regardless of the size of the destination image. Thus, this method + provides a means of generating multiple transformed images from the same + source or of applying multiple transformations simultaneously, in order to + eliminate the need to read the source coefficients multiple times.
    +
    Parameters:
    dstBufs - an array of JPEG destination buffers. + dstbufs[i] will receive a JPEG image that has been + transformed using the parameters in transforms[i]. Use + TJ.bufSize(int, int, int) to determine the maximum size for each buffer based on + the transformed or cropped width and height and the level of subsampling + used in the source image.
    transforms - an array of TJTransform instances, each of which specifies the transform parameters and/or cropping region for the - corresponding transformed output image
    flags - the bitwise OR of one or more of + corresponding transformed JPEG image
    flags - the bitwise OR of one or more of TJ.FLAG_*
    Throws:
    TJException
    @@ -326,12 +331,12 @@

    transform

    public TJDecompressor[] transform(TJTransform[] transforms,
                              int flags)
                                throws TJException
    -
    Losslessly transform the JPEG image associated with this transformer - instance and return an array of TJDecompressor instances, each of - which has a transformed JPEG image associated with it.
    +
    Losslessly transform the JPEG source image associated with this + transformer instance and return an array of TJDecompressor + instances, each of which has a transformed JPEG image associated with it.
    Parameters:
    transforms - an array of TJTransform instances, each of which specifies the transform parameters and/or cropping region for the - corresponding transformed output image
    flags - the bitwise OR of one or more of + corresponding transformed JPEG image
    flags - the bitwise OR of one or more of TJ.FLAG_*
    Returns:
    an array of TJDecompressor instances, each of which has a transformed JPEG image associated with it.
    @@ -347,9 +352,9 @@

    transform

    getTransformedSizes

    public int[] getTransformedSizes()
    Returns an array containing the sizes of the transformed JPEG images - generated by the most recent transform operation.
    + (in bytes) generated by the most recent transform operation.
    Returns:
    an array containing the sizes of the transformed JPEG images - generated by the most recent transform operation.
    + (in bytes) generated by the most recent transform operation.
    diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/YUVImage.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/YUVImage.html index d4485ed6b90..b08fcb3d674 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/YUVImage.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/YUVImage.html @@ -98,7 +98,7 @@

    Class YUVImage


    public class YUVImage
     extends java.lang.Object
    -
    This class encapsulates a YUV planar image and the metadata +
    This class encapsulates a planar YUV image and the metadata associated with it. The TurboJPEG API allows both the JPEG compression and decompression pipelines to be split into stages: YUV encode, compress from YUV, decompress to YUV, and YUV decode. A YUVImage instance @@ -106,30 +106,32 @@

    Class YUVImage

    operations and as the source image for compress-from-YUV and YUV decode operations.

    - Technically, the JPEG format uses the YCbCr colorspace (which technically is - not a "colorspace" but rather a "color transform"), but per the convention - of the digital video community, the TurboJPEG API uses "YUV" to refer to an - image format consisting of Y, Cb, and Cr image planes. + Technically, the JPEG format uses the YCbCr colorspace (which is technically + not a colorspace but a color transform), but per the convention of the + digital video community, the TurboJPEG API uses "YUV" to refer to an image + format consisting of Y, Cb, and Cr image planes.

    Each plane is simply a 2D array of bytes, each byte representing the value of one of the components (Y, Cb, or Cr) at a particular location in the image. The width and height of each plane are determined by the image width, height, and level of chrominance subsampling. The luminance plane width is the image width padded to the nearest multiple of the horizontal - subsampling factor (2 in the case of 4:2:0 and 4:2:2, 4 in the case of - 4:1:1, 1 in the case of 4:4:4 or grayscale.) Similarly, the luminance plane - height is the image height padded to the nearest multiple of the vertical - subsampling factor (2 in the case of 4:2:0 or 4:4:0, 1 in the case of 4:4:4 - or grayscale.) The chrominance plane width is equal to the luminance plane - width divided by the horizontal subsampling factor, and the chrominance - plane height is equal to the luminance plane height divided by the vertical - subsampling factor. + subsampling factor (1 in the case of 4:4:4, grayscale, or 4:4:0; 2 in the + case of 4:2:2 or 4:2:0; 4 in the case of 4:1:1.) Similarly, the luminance + plane height is the image height padded to the nearest multiple of the + vertical subsampling factor (1 in the case of 4:4:4, 4:2:2, grayscale, or + 4:1:1; 2 in the case of 4:2:0 or 4:4:0.) This is irrespective of any + additional padding that may be specified as an argument to the various + YUVImage methods. The chrominance plane width is equal to the luminance + plane width divided by the horizontal subsampling factor, and the + chrominance plane height is equal to the luminance plane height divided by + the vertical subsampling factor.

    For example, if the source image is 35 x 35 pixels and 4:2:2 subsampling is used, then the luminance plane would be 36 x 35 bytes, and each of the - chrominance planes would be 18 x 35 bytes. If you specify a line padding of - 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, and - each of the chrominance planes would be 20 x 35 bytes.

    + chrominance planes would be 18 x 35 bytes. If you specify a row alignment + of 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, + and each of the chrominance planes would be 20 x 35 bytes.
    @@ -154,15 +156,15 @@

    Field Summary

    protected int -yuvHeight  +yuvAlign  -protected int[] -yuvOffsets  +protected int +yuvHeight  -protected int -yuvPad  +protected int[] +yuvOffsets  protected byte[][] @@ -208,10 +210,10 @@

    Constructor Summary

    YUVImage(byte[] yuvImage, int width, - int pad, + int align, int height, int subsamp) -
    Create a new YUVImage instance from an existing unified image +
    Create a new YUVImage instance from an existing unified buffer.
    @@ -226,11 +228,11 @@

    Constructor Summary

    YUVImage(int width, - int pad, + int align, int height, int subsamp) -
    Create a new YUVImage instance backed by a unified image - buffer, and allocate memory for the image buffer.
    +
    Create a new YUVImage instance backed by a unified buffer, + and allocate memory for the buffer.
    @@ -251,8 +253,8 @@

    Method Summary

    byte[] getBuf() -
    Returns the YUV image buffer (if this image is stored in a unified - buffer rather than separate image planes.)
    +
    Returns the YUV buffer (if this image is stored in a unified buffer rather + than separate image planes.)
    @@ -271,7 +273,7 @@

    Method Summary

    int getPad() -
    Returns the line padding used in the YUV image buffer (if this image is +
    Returns the row alignment (in bytes) of the YUV buffer (if this image is stored in a unified buffer rather than separate image planes.)
    @@ -284,14 +286,14 @@

    Method Summary

    int getSize() -
    Returns the size (in bytes) of the YUV image buffer (if this image is - stored in a unified buffer rather than separate image planes.)
    +
    Returns the size (in bytes) of the YUV buffer (if this image is stored in + a unified buffer rather than separate image planes.)
    int[] getStrides() -
    Returns the number of bytes per line of each plane in the YUV image.
    +
    Returns the number of bytes per row of each plane in the YUV image.
    @@ -321,10 +323,10 @@

    Method Summary

    void setBuf(byte[] yuvImage, int width, - int pad, + int align, int height, int subsamp) -
    Assign a unified image buffer to this YUVImage instance.
    +
    Assign a unified buffer to this YUVImage instance.
    @@ -385,13 +387,13 @@

    yuvStrides

    protected int[] yuvStrides
    - +
    • -

      yuvPad

      -
      protected int yuvPad
      +

      yuvAlign

      +
      protected int yuvAlign
    @@ -442,7 +444,7 @@

    YUVImage

    Create a new YUVImage instance backed by separate image planes, and allocate memory for the image planes.
    Parameters:
    width - width (in pixels) of the YUV image
    strides - an array of integers, each specifying the number of bytes - per line in the corresponding plane of the YUV image. Setting the stride + per row in the corresponding plane of the YUV image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see above.) If strides is null, then the strides for all planes will be set to their respective plane widths. When @@ -458,13 +460,15 @@

    YUVImage

  • YUVImage

    public YUVImage(int width,
    -        int pad,
    +        int align,
             int height,
             int subsamp)
    -
    Create a new YUVImage instance backed by a unified image - buffer, and allocate memory for the image buffer.
    -
    Parameters:
    width - width (in pixels) of the YUV image
    pad - Each line of each plane in the YUV image buffer will be padded - to this number of bytes (must be a power of 2.)
    height - height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling to be used in the YUV +
    Create a new YUVImage instance backed by a unified buffer, + and allocate memory for the buffer.
    +
    Parameters:
    width - width (in pixels) of the YUV image
    align - row alignment (in bytes) of the YUV image (must be a power of + 2.) Setting this parameter to n specifies that each row in each plane of + the YUV image will be padded to the nearest multiple of n bytes + (1 = unpadded.)
    height - height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling to be used in the YUV image (one of TJ.SAMP_*)
  • @@ -485,18 +489,18 @@

    YUVImage

    Parameters:
    planes - an array of buffers representing the Y, U (Cb), and V (Cr) image planes (or just the Y plane, if the image is grayscale.) These planes can be contiguous or non-contiguous in memory. Plane - i should be at least offsets[i] + - TJ.planeSizeYUV(i, width, strides[i], height, subsamp) + i should be at least offsets[i] + + TJ.planeSizeYUV(i, width, strides[i], height, subsamp) bytes in size.
    offsets - If this YUVImage instance represents a subregion of a larger image, then offsets[i] specifies the offset (in bytes) of the subregion within plane i of the larger image. Setting this to null is the same as setting the offsets for all planes to 0.
    width - width (in pixels) of the new YUV image (or subregion)
    strides - an array of integers, each specifying the number of bytes - per line in the corresponding plane of the YUV image. Setting the stride + per row in the corresponding plane of the YUV image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see above.) If strides is null, then the strides for all planes will be set to their respective plane widths. You - can adjust the strides in order to add an arbitrary amount of line padding + can adjust the strides in order to add an arbitrary amount of row padding to each plane or to specify that this YUVImage instance is a subregion of a larger image (in which case, strides[i] should be set to the plane width of plane i in the larger image.)
    height - height (in pixels) of the new YUV image (or subregion)
    subsamp - the level of chrominance subsampling used in the YUV @@ -511,18 +515,19 @@

    YUVImage

    YUVImage

    public YUVImage(byte[] yuvImage,
             int width,
    -        int pad,
    +        int align,
             int height,
             int subsamp)
    -
    Create a new YUVImage instance from an existing unified image +
    Create a new YUVImage instance from an existing unified buffer.
    -
    Parameters:
    yuvImage - image buffer that contains or will contain YUV planar - image data. Use TJ.bufSizeYUV(int, int, int, int) to determine the minimum size for - this buffer. The Y, U (Cb), and V (Cr) image planes are stored - sequentially in the buffer (see above for a description - of the image format.)
    width - width (in pixels) of the YUV image
    pad - the line padding used in the YUV image buffer. For - instance, if each line in each plane of the buffer is padded to the - nearest multiple of 4 bytes, then pad should be set to 4.
    height - height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV +
    Parameters:
    yuvImage - buffer that contains or will receive a unified planar YUV + image. Use TJ.bufSizeYUV(int, int, int, int) to determine the minimum size for this + buffer. The Y, U (Cb), and V (Cr) image planes are stored sequentially in + the buffer. (See above for a description of the image + format.)
    width - width (in pixels) of the YUV image
    align - row alignment (in bytes) of the YUV image (must be a power of + 2.) Setting this parameter to n specifies that each row in each plane of + the YUV image will be padded to the nearest multiple of n bytes + (1 = unpadded.)
    height - height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV image (one of TJ.SAMP_*)
    @@ -550,19 +555,19 @@

    setBuf

    Parameters:
    planes - an array of buffers representing the Y, U (Cb), and V (Cr) image planes (or just the Y plane, if the image is grayscale.) These planes can be contiguous or non-contiguous in memory. Plane - i should be at least offsets[i] + - TJ.planeSizeYUV(i, width, strides[i], height, subsamp) + i should be at least offsets[i] + + TJ.planeSizeYUV(i, width, strides[i], height, subsamp) bytes in size.
    offsets - If this YUVImage instance represents a subregion of a larger image, then offsets[i] specifies the offset (in bytes) of the subregion within plane i of the larger image. Setting this to null is the same as setting the offsets for all planes to 0.
    width - width (in pixels) of the YUV image (or subregion)
    strides - an array of integers, each specifying the number of bytes - per line in the corresponding plane of the YUV image. Setting the stride + per row in the corresponding plane of the YUV image. Setting the stride for any plane to 0 is the same as setting it to the plane width (see above.) If strides is null, then the strides for all planes will be set to their respective plane widths. You - can adjust the strides in order to add an arbitrary amount of line padding - to each plane or to specify that this YUVImage image is a + can adjust the strides in order to add an arbitrary amount of row padding + to each plane or to specify that this YUVImage instance is a subregion of a larger image (in which case, strides[i] should be set to the plane width of plane i in the larger image.)
    height - height (in pixels) of the YUV image (or subregion)
    subsamp - the level of chrominance subsampling used in the YUV image (one of TJ.SAMP_*)
    @@ -576,17 +581,18 @@

    setBuf

    setBuf

    public void setBuf(byte[] yuvImage,
               int width,
    -          int pad,
    +          int align,
               int height,
               int subsamp)
    -
    Assign a unified image buffer to this YUVImage instance.
    -
    Parameters:
    yuvImage - image buffer that contains or will contain YUV planar - image data. Use TJ.bufSizeYUV(int, int, int, int) to determine the minimum size for - this buffer. The Y, U (Cb), and V (Cr) image planes are stored - sequentially in the buffer (see above for a description - of the image format.)
    width - width (in pixels) of the YUV image
    pad - the line padding used in the YUV image buffer. For - instance, if each line in each plane of the buffer is padded to the - nearest multiple of 4 bytes, then pad should be set to 4.
    height - height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV +
    Assign a unified buffer to this YUVImage instance.
    +
    Parameters:
    yuvImage - buffer that contains or will receive a unified planar YUV + image. Use TJ.bufSizeYUV(int, int, int, int) to determine the minimum size for this + buffer. The Y, U (Cb), and V (Cr) image planes are stored sequentially in + the buffer. (See above for a description of the image + format.)
    width - width (in pixels) of the YUV image
    align - row alignment (in bytes) of the YUV image (must be a power of + 2.) Setting this parameter to n specifies that each row in each plane of + the YUV image will be padded to the nearest multiple of n bytes + (1 = unpadded.)
    height - height (in pixels) of the YUV image
    subsamp - the level of chrominance subsampling used in the YUV image (one of TJ.SAMP_*)
    @@ -619,9 +625,9 @@

    getHeight

  • getPad

    public int getPad()
    -
    Returns the line padding used in the YUV image buffer (if this image is +
    Returns the row alignment (in bytes) of the YUV buffer (if this image is stored in a unified buffer rather than separate image planes.)
    -
    Returns:
    the line padding used in the YUV image buffer
    +
    Returns:
    the row alignment of the YUV buffer
  • @@ -631,8 +637,8 @@

    getPad

  • getStrides

    public int[] getStrides()
    -
    Returns the number of bytes per line of each plane in the YUV image.
    -
    Returns:
    the number of bytes per line of each plane in the YUV image
    +
    Returns the number of bytes per row of each plane in the YUV image.
    +
    Returns:
    the number of bytes per row of each plane in the YUV image
  • @@ -679,9 +685,9 @@

    getPlanes

  • getBuf

    public byte[] getBuf()
    -
    Returns the YUV image buffer (if this image is stored in a unified - buffer rather than separate image planes.)
    -
    Returns:
    the YUV image buffer
    +
    Returns the YUV buffer (if this image is stored in a unified buffer rather + than separate image planes.)
    +
    Returns:
    the YUV buffer
  • @@ -691,9 +697,9 @@

    getBuf

  • getSize

    public int getSize()
    -
    Returns the size (in bytes) of the YUV image buffer (if this image is - stored in a unified buffer rather than separate image planes.)
    -
    Returns:
    the size (in bytes) of the YUV image buffer
    +
    Returns the size (in bytes) of the YUV buffer (if this image is stored in + a unified buffer rather than separate image planes.)
    +
    Returns:
    the size (in bytes) of the YUV buffer
  • diff --git a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/package-summary.html b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/package-summary.html index dedcce5c2a8..89dbe05b137 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/package-summary.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/org/libjpegturbo/turbojpeg/package-summary.html @@ -131,7 +131,7 @@

    Package org.libjpegturbo.turbojpeg

    YUVImage -
    This class encapsulates a YUV planar image and the metadata +
    This class encapsulates a planar YUV image and the metadata associated with it.
    diff --git a/third-party/mozjpeg/mozjpeg/java/doc/serialized-form.html b/third-party/mozjpeg/mozjpeg/java/doc/serialized-form.html index 45bbc862581..e123f318910 100644 --- a/third-party/mozjpeg/mozjpeg/java/doc/serialized-form.html +++ b/third-party/mozjpeg/mozjpeg/java/doc/serialized-form.html @@ -109,12 +109,13 @@

    Serialized Fields

  • op

    int op
    -
    Transform operation (one of OP_*)
    +
    Transform operation (one of OP_*)
  • options

    int options
    -
    Transform options (bitwise OR of one or more of OPT_*)
    +
    Transform options (bitwise OR of one or more of + OPT_*)
  • cf

    diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJ.java b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJ.java index fbb49df0a85..3857087e367 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJ.java +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJ.java @@ -1,5 +1,6 @@ /* - * Copyright (C)2011-2013, 2017-2018 D. R. Commander. All Rights Reserved. + * Copyright (C)2011-2013, 2017-2018, 2020-2021, 2023 D. R. Commander. + * All Rights Reserved. * Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without @@ -84,7 +85,7 @@ private TJ() {} * subsampling. * * @param subsamp the level of chrominance subsampling (one of - * SAMP_*) + * {@link #SAMP_444 SAMP_*}) * * @return the MCU block width for the given level of chrominance * subsampling. @@ -104,7 +105,7 @@ public static int getMCUWidth(int subsamp) { * subsampling. * * @param subsamp the level of chrominance subsampling (one of - * SAMP_*) + * {@link #SAMP_444 SAMP_*}) * * @return the MCU block height for the given level of chrominance * subsampling. @@ -204,8 +205,8 @@ public static int getMCUHeight(int subsamp) { * vice versa, but the mapping is typically not 1:1 or reversible, nor can it * be defined with a simple formula. Thus, such a conversion is out of scope * for a codec library. However, the TurboJPEG API allows for compressing - * CMYK pixels into a YCCK JPEG image (see {@link #CS_YCCK}) and - * decompressing YCCK JPEG images into CMYK pixels. + * packed-pixel CMYK images into YCCK JPEG images (see {@link #CS_YCCK}) and + * decompressing YCCK JPEG images into packed-pixel CMYK images. */ public static final int PF_CMYK = 11; @@ -213,7 +214,7 @@ public static int getMCUHeight(int subsamp) { /** * Returns the pixel size (in bytes) for the given pixel format. * - * @param pixelFormat the pixel format (one of PF_*) + * @param pixelFormat the pixel format (one of {@link #PF_RGB PF_*}) * * @return the pixel size (in bytes) for the given pixel format. */ @@ -234,7 +235,7 @@ public static int getPixelSize(int pixelFormat) { * then the red component will be * pixel[TJ.getRedOffset(TJ.PF_BGRX)]. * - * @param pixelFormat the pixel format (one of PF_*) + * @param pixelFormat the pixel format (one of {@link #PF_RGB PF_*}) * * @return the red offset for the given pixel format, or -1 if the pixel * format does not have a red component. @@ -256,7 +257,7 @@ public static int getRedOffset(int pixelFormat) { * then the green component will be * pixel[TJ.getGreenOffset(TJ.PF_BGRX)]. * - * @param pixelFormat the pixel format (one of PF_*) + * @param pixelFormat the pixel format (one of {@link #PF_RGB PF_*}) * * @return the green offset for the given pixel format, or -1 if the pixel * format does not have a green component. @@ -278,7 +279,7 @@ public static int getGreenOffset(int pixelFormat) { * then the blue component will be * pixel[TJ.getBlueOffset(TJ.PF_BGRX)]. * - * @param pixelFormat the pixel format (one of PF_*) + * @param pixelFormat the pixel format (one of {@link #PF_RGB PF_*}) * * @return the blue offset for the given pixel format, or -1 if the pixel * format does not have a blue component. @@ -300,7 +301,7 @@ public static int getBlueOffset(int pixelFormat) { * then the alpha component will be * pixel[TJ.getAlphaOffset(TJ.PF_BGRA)]. * - * @param pixelFormat the pixel format (one of PF_*) + * @param pixelFormat the pixel format (one of {@link #PF_RGB PF_*}) * * @return the alpha offset for the given pixel format, or -1 if the pixel * format does not have a alpha component. @@ -323,8 +324,9 @@ public static int getAlphaOffset(int pixelFormat) { * RGB colorspace. When compressing the JPEG image, the R, G, and B * components in the source image are reordered into image planes, but no * colorspace conversion or subsampling is performed. RGB JPEG images can be - * decompressed to any of the extended RGB pixel formats or grayscale, but - * they cannot be decompressed to YUV images. + * decompressed to packed-pixel images with any of the extended RGB or + * grayscale pixel formats, but they cannot be decompressed to planar YUV + * images. */ public static final int CS_RGB = 0; /** @@ -332,31 +334,34 @@ public static int getAlphaOffset(int pixelFormat) { * mathematical transformation of RGB designed solely for storage and * transmission. YCbCr images must be converted to RGB before they can * actually be displayed. In the YCbCr colorspace, the Y (luminance) - * component represents the black & white portion of the original image, and - * the Cb and Cr (chrominance) components represent the color portion of the - * original image. Originally, the analog equivalent of this transformation - * allowed the same signal to drive both black & white and color televisions, - * but JPEG images use YCbCr primarily because it allows the color data to be - * optionally subsampled for the purposes of reducing bandwidth or disk - * space. YCbCr is the most common JPEG colorspace, and YCbCr JPEG images - * can be compressed from and decompressed to any of the extended RGB pixel - * formats or grayscale, or they can be decompressed to YUV planar images. + * component represents the black & white portion of the original image, + * and the Cb and Cr (chrominance) components represent the color portion of + * the original image. Originally, the analog equivalent of this + * transformation allowed the same signal to drive both black & white and + * color televisions, but JPEG images use YCbCr primarily because it allows + * the color data to be optionally subsampled for the purposes of reducing + * network or disk usage. YCbCr is the most common JPEG colorspace, and + * YCbCr JPEG images can be compressed from and decompressed to packed-pixel + * images with any of the extended RGB or grayscale pixel formats. YCbCr + * JPEG images can also be compressed from and decompressed to planar YUV + * images. */ @SuppressWarnings("checkstyle:ConstantName") public static final int CS_YCbCr = 1; /** * Grayscale colorspace. The JPEG image retains only the luminance data (Y * component), and any color data from the source image is discarded. - * Grayscale JPEG images can be compressed from and decompressed to any of - * the extended RGB pixel formats or grayscale, or they can be decompressed - * to YUV planar images. + * Grayscale JPEG images can be compressed from and decompressed to + * packed-pixel images with any of the extended RGB or grayscale pixel + * formats, or they can be compressed from and decompressed to planar YUV + * images. */ public static final int CS_GRAY = 2; /** * CMYK colorspace. When compressing the JPEG image, the C, M, Y, and K * components in the source image are reordered into image planes, but no * colorspace conversion or subsampling is performed. CMYK JPEG images can - * only be decompressed to CMYK pixels. + * only be decompressed to packed-pixel images with the CMYK pixel format. */ public static final int CS_CMYK = 3; /** @@ -366,14 +371,14 @@ public static int getAlphaOffset(int pixelFormat) { * reversibly transformed into YCCK, and as with YCbCr, the chrominance * components in the YCCK pixels can be subsampled without incurring major * perceptual loss. YCCK JPEG images can only be compressed from and - * decompressed to CMYK pixels. + * decompressed to packed-pixel images with the CMYK pixel format. */ public static final int CS_YCCK = 4; /** - * The uncompressed source/destination image is stored in bottom-up (Windows, - * OpenGL) order, not top-down (X11) order. + * Rows in the packed-pixel source/destination image are stored in bottom-up + * (Windows, OpenGL) order rather than in top-down (X11) order. */ public static final int FLAG_BOTTOMUP = 2; @@ -392,41 +397,39 @@ public static int getAlphaOffset(int pixelFormat) { /** * When decompressing an image that was compressed using chrominance - * subsampling, use the fastest chrominance upsampling algorithm available in - * the underlying codec. The default is to use smooth upsampling, which - * creates a smooth transition between neighboring chrominance components in - * order to reduce upsampling artifacts in the decompressed image. + * subsampling, use the fastest chrominance upsampling algorithm available. + * The default is to use smooth upsampling, which creates a smooth transition + * between neighboring chrominance components in order to reduce upsampling + * artifacts in the decompressed image. */ public static final int FLAG_FASTUPSAMPLE = 256; /** - * Use the fastest DCT/IDCT algorithm available in the underlying codec. The - * default if this flag is not specified is implementation-specific. For - * example, the implementation of TurboJPEG for libjpeg[-turbo] uses the fast - * algorithm by default when compressing, because this has been shown to have - * only a very slight effect on accuracy, but it uses the accurate algorithm - * when decompressing, because this has been shown to have a larger effect. + * Use the fastest DCT/IDCT algorithm available. The default if this flag is + * not specified is implementation-specific. For example, the implementation + * of the TurboJPEG API in libjpeg-turbo uses the fast algorithm by default + * when compressing, because this has been shown to have only a very slight + * effect on accuracy, but it uses the accurate algorithm when decompressing, + * because this has been shown to have a larger effect. */ public static final int FLAG_FASTDCT = 2048; /** - * Use the most accurate DCT/IDCT algorithm available in the underlying - * codec. The default if this flag is not specified is - * implementation-specific. For example, the implementation of TurboJPEG for - * libjpeg[-turbo] uses the fast algorithm by default when compressing, - * because this has been shown to have only a very slight effect on accuracy, - * but it uses the accurate algorithm when decompressing, because this has - * been shown to have a larger effect. + * Use the most accurate DCT/IDCT algorithm available. The default if this + * flag is not specified is implementation-specific. For example, the + * implementation of the TurboJPEG API in libjpeg-turbo uses the fast + * algorithm by default when compressing, because this has been shown to have + * only a very slight effect on accuracy, but it uses the accurate algorithm + * when decompressing, because this has been shown to have a larger effect. */ public static final int FLAG_ACCURATEDCT = 4096; /** * Immediately discontinue the current compression/decompression/transform - * operation if the underlying codec throws a warning (non-fatal error). The - * default behavior is to allow the operation to complete unless a fatal - * error is encountered. + * operation if a warning (non-fatal error) occurs. The default behavior is + * to allow the operation to complete unless a fatal error is encountered. *

    * NOTE: due to the design of the TurboJPEG Java API, only certain methods * (specifically, {@link TJDecompressor TJDecompressor.decompress*()} methods - * with a void return type) will complete and leave the output image in a - * fully recoverable state after a non-fatal error occurs. + * with a void return type) will complete and leave the destination image in + * a fully recoverable state after a non-fatal error occurs. */ public static final int FLAG_STOPONWARNING = 8192; /** @@ -436,6 +439,16 @@ public static int getAlphaOffset(int pixelFormat) { * reduce compression and decompression performance considerably. */ public static final int FLAG_PROGRESSIVE = 16384; + /** + * Limit the number of progressive JPEG scans that the decompression and + * transform operations will process. If a progressive JPEG image contains + * an unreasonably large number of scans, then this flag will cause the + * decompression and transform operations to throw an error. The primary + * purpose of this is to allow security-critical applications to guard + * against an exploit of the progressive JPEG format described in + * this report. + */ + public static final int FLAG_LIMITSCANS = 32768; /** @@ -443,13 +456,13 @@ public static int getAlphaOffset(int pixelFormat) { */ public static final int NUMERR = 2; /** - * The error was non-fatal and recoverable, but the image may still be - * corrupt. + * The error was non-fatal and recoverable, but the destination image may + * still be corrupt. *

    * NOTE: due to the design of the TurboJPEG Java API, only certain methods * (specifically, {@link TJDecompressor TJDecompressor.decompress*()} methods - * with a void return type) will complete and leave the output image in a - * fully recoverable state after a non-fatal error occurs. + * with a void return type) will complete and leave the destination image in + * a fully recoverable state after a non-fatal error occurs. */ public static final int ERR_WARNING = 0; /** @@ -467,7 +480,7 @@ public static int getAlphaOffset(int pixelFormat) { * @param height the height (in pixels) of the JPEG image * * @param jpegSubsamp the level of chrominance subsampling to be used when - * generating the JPEG image (one of {@link TJ TJ.SAMP_*}) + * generating the JPEG image (one of {@link #SAMP_444 TJ.SAMP_*}) * * @return the maximum size of the buffer (in bytes) required to hold a JPEG * image with the given width, height, and level of chrominance subsampling. @@ -475,23 +488,27 @@ public static int getAlphaOffset(int pixelFormat) { public static native int bufSize(int width, int height, int jpegSubsamp); /** - * Returns the size of the buffer (in bytes) required to hold a YUV planar - * image with the given width, height, and level of chrominance subsampling. + * Returns the size of the buffer (in bytes) required to hold a unified + * planar YUV image with the given width, height, and level of chrominance + * subsampling. * * @param width the width (in pixels) of the YUV image * - * @param pad the width of each line in each plane of the image is padded to - * the nearest multiple of this number of bytes (must be a power of 2.) + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n specifies that each row in each plane of + * the YUV image will be padded to the nearest multiple of n bytes + * (1 = unpadded.) * * @param height the height (in pixels) of the YUV image * * @param subsamp the level of chrominance subsampling used in the YUV - * image (one of {@link TJ TJ.SAMP_*}) + * image (one of {@link #SAMP_444 TJ.SAMP_*}) * - * @return the size of the buffer (in bytes) required to hold a YUV planar - * image with the given width, height, and level of chrominance subsampling. + * @return the size of the buffer (in bytes) required to hold a unified + * planar YUV image with the given width, height, and level of chrominance + * subsampling. */ - public static native int bufSizeYUV(int width, int pad, int height, + public static native int bufSizeYUV(int width, int align, int height, int subsamp); /** @@ -511,16 +528,16 @@ public static native int bufSizeYUV(int width, int pad, int height, * @param width width (in pixels) of the YUV image. NOTE: this is the width * of the whole image, not the plane width. * - * @param stride bytes per line in the image plane. + * @param stride bytes per row in the image plane. * * @param height height (in pixels) of the YUV image. NOTE: this is the * height of the whole image, not the plane height. * * @param subsamp the level of chrominance subsampling used in the YUV - * image (one of {@link TJ TJ.SAMP_*}) + * image (one of {@link #SAMP_444 TJ.SAMP_*}) * - * @return the size of the buffer (in bytes) required to hold a YUV planar - * image with the given parameters. + * @return the size of the buffer (in bytes) required to hold a YUV image + * plane with the given parameters. */ public static native int planeSizeYUV(int componentID, int width, int stride, int height, int subsamp); @@ -535,7 +552,7 @@ public static native int planeSizeYUV(int componentID, int width, int stride, * @param width width (in pixels) of the YUV image * * @param subsamp the level of chrominance subsampling used in the YUV image - * (one of {@link TJ TJ.SAMP_*}) + * (one of {@link #SAMP_444 TJ.SAMP_*}) * * @return the plane width of a YUV image plane with the given parameters. */ @@ -551,7 +568,7 @@ public static native int planeSizeYUV(int componentID, int width, int stride, * @param height height (in pixels) of the YUV image * * @param subsamp the level of chrominance subsampling used in the YUV image - * (one of {@link TJ TJ.SAMP_*}) + * (one of {@link #SAMP_444 TJ.SAMP_*}) * * @return the plane height of a YUV image plane with the given parameters. */ @@ -559,11 +576,11 @@ public static native int planeHeight(int componentID, int height, int subsamp); /** - * Returns a list of fractional scaling factors that the JPEG decompressor in - * this implementation of TurboJPEG supports. + * Returns a list of fractional scaling factors that the JPEG decompressor + * supports. * - * @return a list of fractional scaling factors that the JPEG decompressor in - * this implementation of TurboJPEG supports. + * @return a list of fractional scaling factors that the JPEG decompressor + * supports. */ public static native TJScalingFactor[] getScalingFactors(); diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCompressor.java b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCompressor.java index 74e5db9cd52..d5bbd826343 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCompressor.java +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCompressor.java @@ -1,5 +1,6 @@ /* - * Copyright (C)2011-2015, 2018 D. R. Commander. All Rights Reserved. + * Copyright (C)2011-2015, 2018, 2020, 2023 D. R. Commander. + * All Rights Reserved. * Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without @@ -49,7 +50,7 @@ public TJCompressor() throws TJException { } /** - * Create a TurboJPEG compressor instance and associate the uncompressed + * Create a TurboJPEG compressor instance and associate the packed-pixel * source image stored in srcImage with the newly created * instance. * @@ -85,7 +86,7 @@ public TJCompressor(byte[] srcImage, int width, int pitch, int height, } /** - * Create a TurboJPEG compressor instance and associate the uncompressed + * Create a TurboJPEG compressor instance and associate the packed-pixel * source image stored in srcImage with the newly created * instance. * @@ -110,11 +111,11 @@ public TJCompressor(BufferedImage srcImage, int x, int y, int width, } /** - * Associate an uncompressed RGB, grayscale, or CMYK source image with this + * Associate a packed-pixel RGB, grayscale, or CMYK source image with this * compressor instance. * - * @param srcImage image buffer containing RGB, grayscale, or CMYK pixels to - * be compressed or encoded. This buffer is not modified. + * @param srcImage buffer containing a packed-pixel RGB, grayscale, or CMYK + * source image to be compressed or encoded. This buffer is not modified. * * @param x x offset (in pixels) of the region in the source image from which * the JPEG or YUV image should be compressed/encoded @@ -125,14 +126,16 @@ public TJCompressor(BufferedImage srcImage, int x, int y, int width, * @param width width (in pixels) of the region in the source image from * which the JPEG or YUV image should be compressed/encoded * - * @param pitch bytes per line of the source image. Normally, this should be - * width * TJ.pixelSize(pixelFormat) if the source image is - * unpadded, but you can use this parameter to, for instance, specify that - * the scanlines in the source image are padded to a 4-byte boundary or to - * compress/encode a JPEG or YUV image from a region of a larger source - * image. You can also be clever and use this parameter to skip lines, etc. - * Setting this parameter to 0 is the equivalent of setting it to - * width * TJ.pixelSize(pixelFormat). + * @param pitch bytes per row in the source image. Normally this should be + * width * + * {@link TJ#getPixelSize TJ.getPixelSize}(pixelFormat), + * if the source image is unpadded. However, you can use this parameter to, + * for instance, specify that the rows in the source image are padded to the + * nearest multiple of 4 bytes or to compress/encode a JPEG or YUV image from + * a region of a larger source image. You can also be clever and use this + * parameter to skip rows, etc. Setting this parameter to 0 is the + * equivalent of setting it to width * + * {@link TJ#getPixelSize TJ.getPixelSize}(pixelFormat). * * @param height height (in pixels) of the region in the source image from * which the JPEG or YUV image should be compressed/encoded @@ -174,11 +177,12 @@ public void setSourceImage(byte[] srcImage, int width, int pitch, } /** - * Associate an uncompressed RGB or grayscale source image with this + * Associate a packed-pixel RGB or grayscale source image with this * compressor instance. * - * @param srcImage a BufferedImage instance containing RGB or - * grayscale pixels to be compressed or encoded. This image is not modified. + * @param srcImage a BufferedImage instance containing a + * packed-pixel RGB or grayscale source image to be compressed or encoded. + * This image is not modified. * * @param x x offset (in pixels) of the region in the source image from which * the JPEG or YUV image should be compressed/encoded @@ -260,11 +264,10 @@ public void setSourceImage(BufferedImage srcImage, int x, int y, int width, } /** - * Associate an uncompressed YUV planar source image with this compressor - * instance. + * Associate a planar YUV source image with this compressor instance. * - * @param srcImage YUV planar image to be compressed. This image is not - * modified. + * @param srcImage planar YUV source image to be compressed. This image is + * not modified. */ public void setSourceImage(YUVImage srcImage) throws TJException { if (handle == 0) init(); @@ -281,16 +284,16 @@ public void setSourceImage(YUVImage srcImage) throws TJException { * {@link TJ#CS_YCbCr}) or from CMYK to YCCK (see {@link TJ#CS_YCCK}) as part * of the JPEG compression process, some of the Cb and Cr (chrominance) * components can be discarded or averaged together to produce a smaller - * image with little perceptible loss of image clarity (the human eye is more - * sensitive to small changes in brightness than to small changes in color.) - * This is called "chrominance subsampling". + * image with little perceptible loss of image clarity. (The human eye is + * more sensitive to small changes in brightness than to small changes in + * color.) This is called "chrominance subsampling". *

    - * NOTE: This method has no effect when compressing a JPEG image from a YUV - * planar source. In that case, the level of chrominance subsampling in - * the JPEG image is determined by the source. Furthermore, this method has - * no effect when encoding to a pre-allocated {@link YUVImage} instance. In - * that case, the level of chrominance subsampling is determined by the - * destination. + * NOTE: This method has no effect when compressing a JPEG image from a + * planar YUV source image. In that case, the level of chrominance + * subsampling in the JPEG image is determined by the source image. + * Furthermore, this method has no effect when encoding to a pre-allocated + * {@link YUVImage} instance. In that case, the level of chrominance + * subsampling is determined by the destination image. * * @param newSubsamp the level of chrominance subsampling to use in * subsequent compress/encode oeprations (one of @@ -315,8 +318,9 @@ public void setJPEGQuality(int quality) { } /** - * Compress the uncompressed source image associated with this compressor - * instance and output a JPEG image to the given destination buffer. + * Compress the packed-pixel or planar YUV source image associated with this + * compressor instance and output a JPEG image to the given destination + * buffer. * * @param dstBuf buffer that will receive the JPEG image. Use * {@link TJ#bufSize} to determine the maximum size for this buffer based on @@ -366,8 +370,8 @@ else if (srcBuf != null) { } /** - * Compress the uncompressed source image associated with this compressor - * instance and return a buffer containing a JPEG image. + * Compress the packed-pixel or planar YUV source image associated with this + * compressor instance and return a buffer containing a JPEG image. * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} @@ -377,8 +381,15 @@ else if (srcBuf != null) { * #getCompressedSize} to obtain the size of the JPEG image. */ public byte[] compress(int flags) throws TJException { - checkSourceImage(); - byte[] buf = new byte[TJ.bufSize(srcWidth, srcHeight, subsamp)]; + byte[] buf; + if (srcYUVImage != null) { + buf = new byte[TJ.bufSize(srcYUVImage.getWidth(), + srcYUVImage.getHeight(), + srcYUVImage.getSubsamp())]; + } else { + checkSourceImage(); + buf = new byte[TJ.bufSize(srcWidth, srcHeight, subsamp)]; + } compress(buf, flags); return buf; } @@ -410,14 +421,14 @@ public byte[] compress(BufferedImage srcImage, int flags) } /** - * Encode the uncompressed source image associated with this compressor - * instance into a YUV planar image and store it in the given - * YUVImage instance. This method uses the accelerated color - * conversion routines in TurboJPEG's underlying codec but does not execute - * any of the other steps in the JPEG compression process. Encoding - * CMYK source images to YUV is not supported. - * - * @param dstImage {@link YUVImage} instance that will receive the YUV planar + * Encode the packed-pixel source image associated with this compressor + * instance into a planar YUV image and store it in the given + * {@link YUVImage} instance. This method performs color conversion (which + * is accelerated in the libjpeg-turbo implementation) but does not execute + * any of the other steps in the JPEG compression process. Encoding CMYK + * source images into YUV images is not supported. + * + * @param dstImage {@link YUVImage} instance that will receive the planar YUV * image * * @param flags the bitwise OR of one or more of @@ -462,52 +473,54 @@ public void encodeYUV(byte[] dstBuf, int flags) throws TJException { } /** - * Encode the uncompressed source image associated with this compressor - * instance into a unified YUV planar image buffer and return a - * YUVImage instance containing the encoded image. This method - * uses the accelerated color conversion routines in TurboJPEG's underlying - * codec but does not execute any of the other steps in the JPEG compression - * process. Encoding CMYK source images to YUV is not supported. + * Encode the packed-pixel source image associated with this compressor + * instance into a unified planar YUV image and return a {@link YUVImage} + * instance containing the encoded image. This method performs color + * conversion (which is accelerated in the libjpeg-turbo implementation) but + * does not execute any of the other steps in the JPEG compression process. + * Encoding CMYK source images into YUV images is not supported. * - * @param pad the width of each line in each plane of the YUV image will be - * padded to the nearest multiple of this number of bytes (must be a power of - * 2.) + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n will cause each row in each plane of the + * YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.) * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} * - * @return a YUV planar image. + * @return a {@link YUVImage} instance containing the unified planar YUV + * encoded image */ - public YUVImage encodeYUV(int pad, int flags) throws TJException { + public YUVImage encodeYUV(int align, int flags) throws TJException { checkSourceImage(); checkSubsampling(); - if (pad < 1 || ((pad & (pad - 1)) != 0)) + if (align < 1 || ((align & (align - 1)) != 0)) throw new IllegalStateException("Invalid argument in encodeYUV()"); - YUVImage dstYUVImage = new YUVImage(srcWidth, pad, srcHeight, subsamp); + YUVImage dstYUVImage = new YUVImage(srcWidth, align, srcHeight, subsamp); encodeYUV(dstYUVImage, flags); return dstYUVImage; } /** - * Encode the uncompressed source image associated with this compressor + * Encode the packed-pixel source image associated with this compressor * instance into separate Y, U (Cb), and V (Cr) image planes and return a - * YUVImage instance containing the encoded image planes. This - * method uses the accelerated color conversion routines in TurboJPEG's - * underlying codec but does not execute any of the other steps in the JPEG - * compression process. Encoding CMYK source images to YUV is not supported. + * {@link YUVImage} instance containing the encoded image planes. This + * method performs color conversion (which is accelerated in the + * libjpeg-turbo implementation) but does not execute any of the other steps + * in the JPEG compression process. Encoding CMYK source images into YUV + * images is not supported. * * @param strides an array of integers, each specifying the number of bytes - * per line in the corresponding plane of the output image. Setting the - * stride for any plane to 0 is the same as setting it to the component width - * of the plane. If strides is null, then the strides for all - * planes will be set to their respective component widths. You can adjust - * the strides in order to add an arbitrary amount of line padding to each - * plane. + * per row in the corresponding plane of the YUV source image. Setting the + * stride for any plane to 0 is the same as setting it to the plane width + * (see {@link YUVImage}.) If strides is null, then the strides + * for all planes will be set to their respective plane widths. You can + * adjust the strides in order to add an arbitrary amount of row padding to + * each plane. * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} * - * @return a YUV planar image. + * @return a {@link YUVImage} instance containing the encoded image planes */ public YUVImage encodeYUV(int[] strides, int flags) throws TJException { checkSourceImage(); @@ -672,6 +685,5 @@ private void checkSubsampling() { private int subsamp = -1; private int jpegQuality = -1; private int compressedSize = 0; - private int yuvPad = 4; private ByteOrder byteOrder = null; } diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCustomFilter.java b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCustomFilter.java index 9a34587a182..3a66fd9ed49 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCustomFilter.java +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJCustomFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (C)2011, 2013 D. R. Commander. All Rights Reserved. + * Copyright (C)2011, 2013, 2023 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -58,7 +58,7 @@ public interface TJCustomFilter { * component plane to which coeffBuffer belongs * * @param componentID ID number of the component plane to which - * coeffBuffer belongs (Y, Cb, and Cr have, respectively, ID's + * coeffBuffer belongs. (Y, Cb, and Cr have, respectively, ID's * of 0, 1, and 2 in typical JPEG images.) * * @param transformID ID number of the transformed image to which diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJDecompressor.java b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJDecompressor.java index cba9ff03265..e35f80a917e 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJDecompressor.java +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJDecompressor.java @@ -1,5 +1,6 @@ /* - * Copyright (C)2011-2015, 2018 D. R. Commander. All Rights Reserved. + * Copyright (C)2011-2015, 2018, 2022-2023 D. R. Commander. + * All Rights Reserved. * Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without @@ -50,10 +51,12 @@ public TJDecompressor() throws TJException { /** * Create a TurboJPEG decompressor instance and associate the JPEG source - * image stored in jpegImage with the newly created instance. + * image or "abbreviated table specification" (AKA "tables-only") datastream + * stored in jpegImage with the newly created instance. * - * @param jpegImage JPEG image buffer (size of the JPEG image is assumed to - * be the length of the array.) This buffer is not modified. + * @param jpegImage buffer containing a JPEG source image or tables-only + * datastream. (The size of the JPEG image or datastream is assumed to be + * the length of the array.) This buffer is not modified. */ public TJDecompressor(byte[] jpegImage) throws TJException { init(); @@ -62,12 +65,15 @@ public TJDecompressor(byte[] jpegImage) throws TJException { /** * Create a TurboJPEG decompressor instance and associate the JPEG source - * image of length imageSize bytes stored in - * jpegImage with the newly created instance. + * image or "abbreviated table specification" (AKA "tables-only") datastream + * of length imageSize bytes stored in jpegImage + * with the newly created instance. * - * @param jpegImage JPEG image buffer. This buffer is not modified. + * @param jpegImage buffer containing a JPEG source image or tables-only + * datastream. This buffer is not modified. * - * @param imageSize size of the JPEG image (in bytes) + * @param imageSize size of the JPEG source image or tables-only datastream + * (in bytes) */ public TJDecompressor(byte[] jpegImage, int imageSize) throws TJException { init(); @@ -75,11 +81,11 @@ public TJDecompressor(byte[] jpegImage, int imageSize) throws TJException { } /** - * Create a TurboJPEG decompressor instance and associate the YUV planar + * Create a TurboJPEG decompressor instance and associate the planar YUV * source image stored in yuvImage with the newly created * instance. * - * @param yuvImage {@link YUVImage} instance containing a YUV planar + * @param yuvImage {@link YUVImage} instance containing a planar YUV source * image to be decoded. This image is not modified. */ @SuppressWarnings("checkstyle:HiddenField") @@ -89,13 +95,22 @@ public TJDecompressor(YUVImage yuvImage) throws TJException { } /** - * Associate the JPEG image of length imageSize bytes stored in - * jpegImage with this decompressor instance. This image will - * be used as the source image for subsequent decompress operations. - * - * @param jpegImage JPEG image buffer. This buffer is not modified. - * - * @param imageSize size of the JPEG image (in bytes) + * Associate the JPEG image or "abbreviated table specification" (AKA + * "tables-only") datastream of length imageSize bytes stored in + * jpegImage with this decompressor instance. If + * jpegImage contains a JPEG image, then this image will be used + * as the source image for subsequent decompression operations. Passing a + * tables-only datastream to this method primes the decompressor with + * quantization and Huffman tables that can be used when decompressing + * subsequent "abbreviated image" datastreams. This is useful, for instance, + * when decompressing video streams in which all frames share the same + * quantization and Huffman tables. + * + * @param jpegImage buffer containing a JPEG source image or tables-only + * datastream. This buffer is not modified. + * + * @param imageSize size of the JPEG source image or tables-only datastream + * (in bytes) */ public void setSourceImage(byte[] jpegImage, int imageSize) throws TJException { @@ -118,12 +133,12 @@ public void setJPEGImage(byte[] jpegImage, int imageSize) } /** - * Associate the specified YUV planar source image with this decompressor - * instance. Subsequent decompress operations will decode this image into an - * RGB or grayscale destination image. + * Associate the specified planar YUV source image with this decompressor + * instance. Subsequent decompression operations will decode this image into + * a packed-pixel RGB or grayscale destination image. * - * @param srcImage {@link YUVImage} instance containing a YUV planar image to - * be decoded. This image is not modified. + * @param srcImage {@link YUVImage} instance containing a planar YUV source + * image to be decoded. This image is not modified. */ public void setSourceImage(YUVImage srcImage) { if (srcImage == null) @@ -201,9 +216,9 @@ public int getColorspace() { } /** - * Returns the JPEG image buffer associated with this decompressor instance. + * Returns the JPEG buffer associated with this decompressor instance. * - * @return the JPEG image buffer associated with this decompressor instance. + * @return the JPEG buffer associated with this decompressor instance. */ public byte[] getJPEGBuf() { if (jpegBuf == null) @@ -230,14 +245,14 @@ public int getJPEGSize() { * height. * * @param desiredWidth desired width (in pixels) of the decompressed image. - * Setting this to 0 is the same as setting it to the width of the JPEG image - * (in other words, the width will not be considered when determining the - * scaled image size.) + * Setting this to 0 is the same as setting it to the width of the JPEG + * image. (In other words, the width will not be considered when determining + * the scaled image size.) * * @param desiredHeight desired height (in pixels) of the decompressed image. * Setting this to 0 is the same as setting it to the height of the JPEG - * image (in other words, the height will not be considered when determining - * the scaled image size.) + * image. (In other words, the height will not be considered when + * determining the scaled image size.) * * @return the width of the largest scaled-down image that the TurboJPEG * decompressor can generate without exceeding the desired image width and @@ -271,14 +286,14 @@ public int getScaledWidth(int desiredWidth, int desiredHeight) { * height. * * @param desiredWidth desired width (in pixels) of the decompressed image. - * Setting this to 0 is the same as setting it to the width of the JPEG image - * (in other words, the width will not be considered when determining the - * scaled image size.) + * Setting this to 0 is the same as setting it to the width of the JPEG + * image. (In other words, the width will not be considered when determining + * the scaled image size.) * * @param desiredHeight desired height (in pixels) of the decompressed image. * Setting this to 0 is the same as setting it to the height of the JPEG - * image (in other words, the height will not be considered when determining - * the scaled image size.) + * image. (In other words, the height will not be considered when + * determining the scaled image size.) * * @return the height of the largest scaled-down image that the TurboJPEG * decompressor can generate without exceeding the desired image width and @@ -307,27 +322,27 @@ public int getScaledHeight(int desiredWidth, int desiredHeight) { } /** - * Decompress the JPEG source image or decode the YUV source image associated - * with this decompressor instance and output a grayscale, RGB, or CMYK image - * to the given destination buffer. + * Decompress the JPEG source image or decode the planar YUV source image + * associated with this decompressor instance and output a packed-pixel + * grayscale, RGB, or CMYK image to the given destination buffer. *

    - * NOTE: The output image is fully recoverable if this method throws a + * NOTE: The destination image is fully recoverable if this method throws a * non-fatal {@link TJException} (unless * {@link TJ#FLAG_STOPONWARNING TJ.FLAG_STOPONWARNING} is specified.) * - * @param dstBuf buffer that will receive the decompressed/decoded image. - * If the source image is a JPEG image, then this buffer should normally be - * pitch * scaledHeight bytes in size, where - * scaledHeight can be determined by calling - * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegHeight) - * with one of the scaling factors returned from {@link - * TJ#getScalingFactors} or by calling {@link #getScaledHeight}. If the - * source image is a YUV image, then this buffer should normally be - * pitch * height bytes in size, where height is - * the height of the YUV image. However, the buffer may also be larger than - * the dimensions of the source image, in which case the x, - * y, and pitch parameters can be used to specify - * the region into which the source image should be decompressed/decoded. + * @param dstBuf buffer that will receive the packed-pixel + * decompressed/decoded image. If the source image is a JPEG image, then + * this buffer should normally be pitch * scaledHeight bytes in + * size, where scaledHeight can be determined by calling + * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegHeight) + * with one of the scaling factors returned from {@link TJ#getScalingFactors} + * or by calling {@link #getScaledHeight}. If the source image is a YUV + * image, then this buffer should normally be pitch * height + * bytes in size, where height is the height of the YUV image. + * However, the buffer may also be larger than the dimensions of the source + * image, in which case the x, y, and + * pitch parameters can be used to specify the region into which + * the source image should be decompressed/decoded. * * @param x x offset (in pixels) of the region in the destination image into * which the source image should be decompressed/decoded @@ -341,22 +356,24 @@ public int getScaledHeight(int desiredWidth, int desiredHeight) { * than the source image dimensions, then TurboJPEG will use scaling in the * JPEG decompressor to generate the largest possible image that will fit * within the desired dimensions. Setting this to 0 is the same as setting - * it to the width of the JPEG image (in other words, the width will not be + * it to the width of the JPEG image. (In other words, the width will not be * considered when determining the scaled image size.) This parameter is * ignored if the source image is a YUV image. * - * @param pitch bytes per line of the destination image. Normally, this - * should be set to scaledWidth * TJ.pixelSize(pixelFormat) if - * the destination image is unpadded, but you can use this to, for instance, - * pad each line of the destination image to a 4-byte boundary or to - * decompress/decode the source image into a region of a larger image. NOTE: - * if the source image is a JPEG image, then scaledWidth can be - * determined by calling - * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegWidth) - * or by calling {@link #getScaledWidth}. If the source image is a - * YUV image, then scaledWidth is the width of the YUV image. + * @param pitch bytes per row in the destination image. Normally this should + * be set to scaledWidth * + * {@link TJ#getPixelSize TJ.getPixelSize}(pixelFormat), + * if the destination image will be unpadded. However, you can use this to, + * for instance, pad each row of the destination image to the nearest + * multiple of 4 bytes or to decompress/decode the source image into a region + * of a larger image. NOTE: if the source image is a JPEG image, then + * scaledWidth can be determined by calling + * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegWidth) + * or by calling {@link #getScaledWidth}. If the source image is a YUV + * image, then scaledWidth is the width of the YUV image. * Setting this parameter to 0 is the equivalent of setting it to - * scaledWidth * TJ.pixelSize(pixelFormat). + * scaledWidth * + * {@link TJ#getPixelSize TJ.getPixelSize}(pixelFormat). * * @param desiredHeight If the source image is a JPEG image, then this * specifies the desired height (in pixels) of the decompressed image (or @@ -364,8 +381,8 @@ public int getScaledHeight(int desiredWidth, int desiredHeight) { * than the source image dimensions, then TurboJPEG will use scaling in the * JPEG decompressor to generate the largest possible image that will fit * within the desired dimensions. Setting this to 0 is the same as setting - * it to the height of the JPEG image (in other words, the height will not be - * considered when determining the scaled image size.) This parameter is + * it to the height of the JPEG image. (In other words, the height will not + * be considered when determining the scaled image size.) This parameter is * ignored if the source image is a YUV image. * * @param pixelFormat pixel format of the decompressed/decoded image (one of @@ -378,7 +395,7 @@ public void decompress(byte[] dstBuf, int x, int y, int desiredWidth, int pitch, int desiredHeight, int pixelFormat, int flags) throws TJException { if (jpegBuf == null && yuvImage == null) - throw new IllegalStateException(NO_ASSOC_ERROR); + throw new IllegalStateException("No source image is associated with this instance"); if (dstBuf == null || x < 0 || y < 0 || pitch < 0 || (yuvImage != null && (desiredWidth < 0 || desiredHeight < 0)) || pixelFormat < 0 || pixelFormat >= TJ.NUMPF || flags < 0) @@ -412,8 +429,9 @@ public void decompress(byte[] dstBuf, int desiredWidth, int pitch, } /** - * Decompress the JPEG source image associated with this decompressor - * instance and return a buffer containing the decompressed image. + * Decompress the JPEG source image or decode the planar YUV source image + * associated with this decompressor instance and return a buffer containing + * the packed-pixel decompressed image. * * @param desiredWidth see * {@link #decompress(byte[], int, int, int, int, int, int, int)} @@ -433,7 +451,7 @@ public void decompress(byte[] dstBuf, int desiredWidth, int pitch, * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} * - * @return a buffer containing the decompressed image. + * @return a buffer containing the packed-pixel decompressed image. */ public byte[] decompress(int desiredWidth, int pitch, int desiredHeight, int pixelFormat, int flags) throws TJException { @@ -453,22 +471,22 @@ public byte[] decompress(int desiredWidth, int pitch, int desiredHeight, /** * Decompress the JPEG source image associated with this decompressor - * instance into a YUV planar image and store it in the given - * YUVImage instance. This method performs JPEG decompression - * but leaves out the color conversion step, so a planar YUV image is - * generated instead of an RGB or grayscale image. This method cannot be - * used to decompress JPEG source images with the CMYK or YCCK colorspace. + * instance into a planar YUV image and store it in the given + * {@link YUVImage} instance. This method performs JPEG decompression but + * leaves out the color conversion step, so a planar YUV image is generated + * instead of a packed-pixel image. This method cannot be used to decompress + * JPEG source images with the CMYK or YCCK colorspace. *

    - * NOTE: The YUV planar output image is fully recoverable if this method + * NOTE: The planar YUV destination image is fully recoverable if this method * throws a non-fatal {@link TJException} (unless * {@link TJ#FLAG_STOPONWARNING TJ.FLAG_STOPONWARNING} is specified.) * - * @param dstImage {@link YUVImage} instance that will receive the YUV planar - * image. The level of subsampling specified in this YUVImage - * instance must match that of the JPEG image, and the width and height - * specified in the YUVImage instance must match one of the - * scaled image sizes that TurboJPEG is capable of generating from the JPEG - * source image. + * @param dstImage {@link YUVImage} instance that will receive the planar YUV + * decompressed image. The level of subsampling specified in this + * {@link YUVImage} instance must match that of the JPEG image, and the width + * and height specified in the {@link YUVImage} instance must match one of + * the scaled image sizes that the decompressor is capable of generating from + * the JPEG source image. * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} @@ -485,7 +503,7 @@ public void decompressToYUV(YUVImage dstImage, int flags) dstImage.getHeight()); if (scaledWidth != dstImage.getWidth() || scaledHeight != dstImage.getHeight()) - throw new IllegalArgumentException("YUVImage dimensions do not match one of the scaled image sizes that TurboJPEG is capable of generating."); + throw new IllegalArgumentException("YUVImage dimensions do not match one of the scaled image sizes that the decompressor is capable of generating."); if (jpegSubsamp != dstImage.getSubsamp()) throw new IllegalArgumentException("YUVImage subsampling level does not match that of the JPEG image"); @@ -508,40 +526,41 @@ public void decompressToYUV(byte[] dstBuf, int flags) throws TJException { /** * Decompress the JPEG source image associated with this decompressor * instance into a set of Y, U (Cb), and V (Cr) image planes and return a - * YUVImage instance containing the decompressed image planes. - * This method performs JPEG decompression but leaves out the color - * conversion step, so a planar YUV image is generated instead of an RGB or - * grayscale image. This method cannot be used to decompress JPEG source - * images with the CMYK or YCCK colorspace. + * {@link YUVImage} instance containing the decompressed image planes. This + * method performs JPEG decompression but leaves out the color conversion + * step, so a planar YUV image is generated instead of a packed-pixel image. + * This method cannot be used to decompress JPEG source images with the CMYK + * or YCCK colorspace. * * @param desiredWidth desired width (in pixels) of the YUV image. If the * desired image dimensions are different than the dimensions of the JPEG * image being decompressed, then TurboJPEG will use scaling in the JPEG * decompressor to generate the largest possible image that will fit within * the desired dimensions. Setting this to 0 is the same as setting it to - * the width of the JPEG image (in other words, the width will not be + * the width of the JPEG image. (In other words, the width will not be * considered when determining the scaled image size.) * * @param strides an array of integers, each specifying the number of bytes - * per line in the corresponding plane of the output image. Setting the - * stride for any plane to 0 is the same as setting it to the scaled - * component width of the plane. If strides is NULL, then the - * strides for all planes will be set to their respective scaled component - * widths. You can adjust the strides in order to add an arbitrary amount of - * line padding to each plane. + * per row in the corresponding plane of the YUV image. Setting the stride + * for any plane to 0 is the same as setting it to the scaled plane width + * (see {@link YUVImage}.) If strides is null, then the strides + * for all planes will be set to their respective scaled plane widths. You + * can adjust the strides in order to add an arbitrary amount of row padding + * to each plane. * * @param desiredHeight desired height (in pixels) of the YUV image. If the * desired image dimensions are different than the dimensions of the JPEG * image being decompressed, then TurboJPEG will use scaling in the JPEG * decompressor to generate the largest possible image that will fit within * the desired dimensions. Setting this to 0 is the same as setting it to - * the height of the JPEG image (in other words, the height will not be + * the height of the JPEG image. (In other words, the height will not be * considered when determining the scaled image size.) * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} * - * @return a YUV planar image. + * @return a {@link YUVImage} instance containing the decompressed image + * planes */ public YUVImage decompressToYUV(int desiredWidth, int[] strides, int desiredHeight, @@ -565,40 +584,41 @@ public YUVImage decompressToYUV(int desiredWidth, int[] strides, /** * Decompress the JPEG source image associated with this decompressor - * instance into a unified YUV planar image buffer and return a - * YUVImage instance containing the decompressed image. This - * method performs JPEG decompression but leaves out the color conversion - * step, so a planar YUV image is generated instead of an RGB or grayscale - * image. This method cannot be used to decompress JPEG source images with - * the CMYK or YCCK colorspace. + * instance into a unified planar YUV image and return a {@link YUVImage} + * instance containing the decompressed image. This method performs JPEG + * decompression but leaves out the color conversion step, so a planar YUV + * image is generated instead of a packed-pixel image. This method cannot be + * used to decompress JPEG source images with the CMYK or YCCK colorspace. * * @param desiredWidth desired width (in pixels) of the YUV image. If the * desired image dimensions are different than the dimensions of the JPEG * image being decompressed, then TurboJPEG will use scaling in the JPEG * decompressor to generate the largest possible image that will fit within * the desired dimensions. Setting this to 0 is the same as setting it to - * the width of the JPEG image (in other words, the width will not be + * the width of the JPEG image. (In other words, the width will not be * considered when determining the scaled image size.) * - * @param pad the width of each line in each plane of the YUV image will be - * padded to the nearest multiple of this number of bytes (must be a power of - * 2.) + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n will cause each row in each plane of the + * YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.) * * @param desiredHeight desired height (in pixels) of the YUV image. If the * desired image dimensions are different than the dimensions of the JPEG * image being decompressed, then TurboJPEG will use scaling in the JPEG * decompressor to generate the largest possible image that will fit within * the desired dimensions. Setting this to 0 is the same as setting it to - * the height of the JPEG image (in other words, the height will not be + * the height of the JPEG image. (In other words, the height will not be * considered when determining the scaled image size.) * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} * - * @return a YUV planar image. + * @return a {@link YUVImage} instance containing the unified planar YUV + * decompressed image */ - public YUVImage decompressToYUV(int desiredWidth, int pad, int desiredHeight, - int flags) throws TJException { + public YUVImage decompressToYUV(int desiredWidth, int align, + int desiredHeight, int flags) + throws TJException { if (flags < 0) throw new IllegalArgumentException("Invalid argument in decompressToYUV()"); if (jpegWidth < 1 || jpegHeight < 1 || jpegSubsamp < 0) @@ -610,7 +630,7 @@ public YUVImage decompressToYUV(int desiredWidth, int pad, int desiredHeight, int scaledWidth = getScaledWidth(desiredWidth, desiredHeight); int scaledHeight = getScaledHeight(desiredWidth, desiredHeight); - YUVImage dstYUVImage = new YUVImage(scaledWidth, pad, scaledHeight, + YUVImage dstYUVImage = new YUVImage(scaledWidth, align, scaledHeight, jpegSubsamp); decompressToYUV(dstYUVImage, flags); return dstYUVImage; @@ -628,27 +648,27 @@ public byte[] decompressToYUV(int flags) throws TJException { } /** - * Decompress the JPEG source image or decode the YUV source image associated - * with this decompressor instance and output a grayscale, RGB, or CMYK image - * to the given destination buffer. + * Decompress the JPEG source image or decode the planar YUV source image + * associated with this decompressor instance and output a packed-pixel + * grayscale, RGB, or CMYK image to the given destination buffer. *

    - * NOTE: The output image is fully recoverable if this method throws a + * NOTE: The destination image is fully recoverable if this method throws a * non-fatal {@link TJException} (unless * {@link TJ#FLAG_STOPONWARNING TJ.FLAG_STOPONWARNING} is specified.) * - * @param dstBuf buffer that will receive the decompressed/decoded image. - * If the source image is a JPEG image, then this buffer should normally be - * stride * scaledHeight pixels in size, where - * scaledHeight can be determined by calling - * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegHeight) - * with one of the scaling factors returned from {@link - * TJ#getScalingFactors} or by calling {@link #getScaledHeight}. If the - * source image is a YUV image, then this buffer should normally be - * stride * height pixels in size, where height is - * the height of the YUV image. However, the buffer may also be larger than - * the dimensions of the JPEG image, in which case the x, - * y, and stride parameters can be used to specify - * the region into which the source image should be decompressed. + * @param dstBuf buffer that will receive the packed-pixel + * decompressed/decoded image. If the source image is a JPEG image, then + * this buffer should normally be stride * scaledHeight pixels + * in size, where scaledHeight can be determined by calling + * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegHeight) + * with one of the scaling factors returned from {@link TJ#getScalingFactors} + * or by calling {@link #getScaledHeight}. If the source image is a YUV + * image, then this buffer should normally be stride * height + * pixels in size, where height is the height of the YUV image. + * However, the buffer may also be larger than the dimensions of the JPEG + * image, in which case the x, y, and + * stride parameters can be used to specify the region into + * which the source image should be decompressed. * * @param x x offset (in pixels) of the region in the destination image into * which the source image should be decompressed/decoded @@ -662,18 +682,18 @@ public byte[] decompressToYUV(int flags) throws TJException { * than the source image dimensions, then TurboJPEG will use scaling in the * JPEG decompressor to generate the largest possible image that will fit * within the desired dimensions. Setting this to 0 is the same as setting - * it to the width of the JPEG image (in other words, the width will not be + * it to the width of the JPEG image. (In other words, the width will not be * considered when determining the scaled image size.) This parameter is * ignored if the source image is a YUV image. * - * @param stride pixels per line of the destination image. Normally, this + * @param stride pixels per row in the destination image. Normally this * should be set to scaledWidth, but you can use this to, for * instance, decompress the JPEG image into a region of a larger image. * NOTE: if the source image is a JPEG image, then scaledWidth - * can be determined by calling - * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegWidth) - * or by calling {@link #getScaledWidth}. If the source image is a - * YUV image, then scaledWidth is the width of the YUV image. + * can be determined by calling + * scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegWidth) + * or by calling {@link #getScaledWidth}. If the source image is a YUV + * image, then scaledWidth is the width of the YUV image. * Setting this parameter to 0 is the equivalent of setting it to * scaledWidth. * @@ -683,8 +703,8 @@ public byte[] decompressToYUV(int flags) throws TJException { * than the source image dimensions, then TurboJPEG will use scaling in the * JPEG decompressor to generate the largest possible image that will fit * within the desired dimensions. Setting this to 0 is the same as setting - * it to the height of the JPEG image (in other words, the height will not be - * considered when determining the scaled image size.) This parameter is + * it to the height of the JPEG image. (In other words, the height will not + * be considered when determining the scaled image size.) This parameter is * ignored if the source image is a YUV image. * * @param pixelFormat pixel format of the decompressed image (one of @@ -697,7 +717,7 @@ public void decompress(int[] dstBuf, int x, int y, int desiredWidth, int stride, int desiredHeight, int pixelFormat, int flags) throws TJException { if (jpegBuf == null && yuvImage == null) - throw new IllegalStateException(NO_ASSOC_ERROR); + throw new IllegalStateException("No source image is associated with this instance"); if (dstBuf == null || x < 0 || y < 0 || stride < 0 || (yuvImage != null && (desiredWidth < 0 || desiredHeight < 0)) || pixelFormat < 0 || pixelFormat >= TJ.NUMPF || flags < 0) @@ -713,21 +733,22 @@ public void decompress(int[] dstBuf, int x, int y, int desiredWidth, } /** - * Decompress the JPEG source image or decode the YUV source image associated - * with this decompressor instance and output a decompressed/decoded image to - * the given BufferedImage instance. + * Decompress the JPEG source image or decode the planar YUV source image + * associated with this decompressor instance and output a packed-pixel + * decompressed/decoded image to the given BufferedImage + * instance. *

    - * NOTE: The output image is fully recoverable if this method throws a + * NOTE: The destination image is fully recoverable if this method throws a * non-fatal {@link TJException} (unless * {@link TJ#FLAG_STOPONWARNING TJ.FLAG_STOPONWARNING} is specified.) * * @param dstImage a BufferedImage instance that will receive - * the decompressed/decoded image. If the source image is a JPEG image, then - * the width and height of the BufferedImage instance must match - * one of the scaled image sizes that TurboJPEG is capable of generating from - * the JPEG image. If the source image is a YUV image, then the width and - * height of the BufferedImage instance must match the width and - * height of the YUV image. + * the packed-pixel decompressed/decoded image. If the source image is a + * JPEG image, then the width and height of the BufferedImage + * instance must match one of the scaled image sizes that the decompressor is + * capable of generating from the JPEG image. If the source image is a YUV + * image, then the width and height of the BufferedImage + * instance must match the width and height of the YUV image. * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} @@ -750,7 +771,7 @@ public void decompress(BufferedImage dstImage, int flags) scaledWidth = getScaledWidth(desiredWidth, desiredHeight); scaledHeight = getScaledHeight(desiredWidth, desiredHeight); if (scaledWidth != desiredWidth || scaledHeight != desiredHeight) - throw new IllegalArgumentException("BufferedImage dimensions do not match one of the scaled image sizes that TurboJPEG is capable of generating."); + throw new IllegalArgumentException("BufferedImage dimensions do not match one of the scaled image sizes that the decompressor is capable of generating."); } int pixelFormat; boolean intPixels = false; if (byteOrder == null) @@ -818,9 +839,10 @@ public void decompress(BufferedImage dstImage, int flags) } /** - * Decompress the JPEG source image or decode the YUV source image associated - * with this decompressor instance and return a BufferedImage - * instance containing the decompressed/decoded image. + * Decompress the JPEG source image or decode the planar YUV source image + * associated with this decompressor instance and return a + * BufferedImage instance containing the packed-pixel + * decompressed/decoded image. * * @param desiredWidth see * {@link #decompress(byte[], int, int, int, int, int, int, int)} for @@ -837,7 +859,7 @@ public void decompress(BufferedImage dstImage, int flags) * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} * - * @return a BufferedImage instance containing the + * @return a BufferedImage instance containing the packed-pixel * decompressed/decoded image. */ public BufferedImage decompress(int desiredWidth, int desiredHeight, diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJLoader-unix.java.in b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJLoader-unix.java.in index 65884e8df78..d8cc49505ea 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJLoader-unix.java.in +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJLoader-unix.java.in @@ -1,5 +1,5 @@ /* - * Copyright (C)2011-2013, 2016 D. R. Commander. All Rights Reserved. + * Copyright (C)2011-2013, 2016, 2020 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,9 +36,9 @@ final class TJLoader { String os = System.getProperty("os.name").toLowerCase(); if (os.indexOf("mac") >= 0) { try { - System.load("@CMAKE_INSTALL_FULL_LIBDIR@/libturbojpeg.jnilib"); + System.load("@CMAKE_INSTALL_FULL_LIBDIR@/libturbojpeg.dylib"); } catch (java.lang.UnsatisfiedLinkError e2) { - System.load("/usr/lib/libturbojpeg.jnilib"); + System.load("/usr/lib/libturbojpeg.dylib"); } } else { try { diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransform.java b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransform.java index 41c4b45ed73..91bcc6bd347 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransform.java +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransform.java @@ -1,5 +1,5 @@ /* - * Copyright (C)2011, 2013, 2018 D. R. Commander. All Rights Reserved. + * Copyright (C)2011, 2013, 2018, 2023 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -97,7 +97,7 @@ public class TJTransform extends Rectangle { * the level of chrominance subsampling used. If the image's width or height * is not evenly divisible by the MCU block size (see {@link TJ#getMCUWidth} * and {@link TJ#getMCUHeight}), then there will be partial MCU blocks on the - * right and/or bottom edges. It is not possible to move these partial MCU + * right and/or bottom edges. It is not possible to move these partial MCU * blocks to the top or left of the image, so any transform that would * require that is "imperfect." If this option is not specified, then any * partial MCU blocks that cannot be transformed will be left in place, which @@ -114,8 +114,8 @@ public class TJTransform extends Rectangle { */ public static final int OPT_CROP = 4; /** - * This option will discard the color data in the input image and produce - * a grayscale output image. + * This option will discard the color data in the source image and produce a + * grayscale destination image. */ public static final int OPT_GRAY = 8; /** @@ -127,17 +127,16 @@ public class TJTransform extends Rectangle { */ public static final int OPT_NOOUTPUT = 16; /** - * This option will enable progressive entropy coding in the output image + * This option will enable progressive entropy coding in the JPEG image * generated by this particular transform. Progressive entropy coding will * generally improve compression relative to baseline entropy coding (the - * default), but it will reduce compression and decompression performance - * considerably. + * default), but it will reduce decompression performance considerably. */ public static final int OPT_PROGRESSIVE = 32; /** * This option will prevent {@link TJTransformer#transform * TJTransformer.transform()} from copying any extra markers (including EXIF - * and ICC profile data) from the source image to the output image. + * and ICC profile data) from the source image to the destination image. */ public static final int OPT_COPYNONE = 64; @@ -165,10 +164,10 @@ public TJTransform() { * equivalent of setting it to (height of the source JPEG image - * y). * - * @param op one of the transform operations (OP_*) + * @param op one of the transform operations ({@link #OP_NONE OP_*}) * * @param options the bitwise OR of one or more of the transform options - * (OPT_*) + * ({@link #OPT_PERFECT OPT_*}) * * @param cf an instance of an object that implements the {@link * TJCustomFilter} interface, or null if no custom filter is needed @@ -190,10 +189,10 @@ public TJTransform(int x, int y, int w, int h, int op, int options, * #TJTransform(int, int, int, int, int, int, TJCustomFilter)} for more * detail. * - * @param op one of the transform operations (OP_*) + * @param op one of the transform operations ({@link #OP_NONE OP_*}) * * @param options the bitwise OR of one or more of the transform options - * (OPT_*) + * ({@link #OPT_PERFECT OPT_*}) * * @param cf an instance of an object that implements the {@link * TJCustomFilter} interface, or null if no custom filter is needed @@ -208,13 +207,14 @@ public TJTransform(Rectangle r, int op, int options, } /** - * Transform operation (one of OP_*) + * Transform operation (one of {@link #OP_NONE OP_*}) */ @SuppressWarnings("checkstyle:VisibilityModifier") public int op = 0; /** - * Transform options (bitwise OR of one or more of OPT_*) + * Transform options (bitwise OR of one or more of + * {@link #OPT_PERFECT OPT_*}) */ @SuppressWarnings("checkstyle:VisibilityModifier") public int options = 0; diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransformer.java b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransformer.java index d7a56f35a6a..2cbf0bfb969 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransformer.java +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/TJTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C)2011, 2013-2015 D. R. Commander. All Rights Reserved. + * Copyright (C)2011, 2013-2015, 2023 D. R. Commander. All Rights Reserved. * Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without @@ -43,10 +43,12 @@ public TJTransformer() throws TJException { /** * Create a TurboJPEG lossless transformer instance and associate the JPEG - * image stored in jpegImage with the newly created instance. + * source image stored in jpegImage with the newly created + * instance. * - * @param jpegImage JPEG image buffer (size of the JPEG image is assumed to - * be the length of the array.) This buffer is not modified. + * @param jpegImage buffer containing the JPEG source image to transform. + * (The size of the JPEG image is assumed to be the length of the array.) + * This buffer is not modified. */ public TJTransformer(byte[] jpegImage) throws TJException { init(); @@ -55,12 +57,13 @@ public TJTransformer(byte[] jpegImage) throws TJException { /** * Create a TurboJPEG lossless transformer instance and associate the JPEG - * image of length imageSize bytes stored in + * source image of length imageSize bytes stored in * jpegImage with the newly created instance. * - * @param jpegImage JPEG image buffer. This buffer is not modified. + * @param jpegImage buffer containing the JPEG source image to transform. + * This buffer is not modified. * - * @param imageSize size of the JPEG image (in bytes) + * @param imageSize size of the JPEG source image (in bytes) */ public TJTransformer(byte[] jpegImage, int imageSize) throws TJException { init(); @@ -68,28 +71,29 @@ public TJTransformer(byte[] jpegImage, int imageSize) throws TJException { } /** - * Losslessly transform the JPEG image associated with this transformer - * instance into one or more JPEG images stored in the given destination - * buffers. Lossless transforms work by moving the raw coefficients from one - * JPEG image structure to another without altering the values of the - * coefficients. While this is typically faster than decompressing the - * image, transforming it, and re-compressing it, lossless transforms are not - * free. Each lossless transform requires reading and performing Huffman - * decoding on all of the coefficients in the source image, regardless of the - * size of the destination image. Thus, this method provides a means of - * generating multiple transformed images from the same source or of applying - * multiple transformations simultaneously, in order to eliminate the need to - * read the source coefficients multiple times. + * Losslessly transform the JPEG source image associated with this + * transformer instance into one or more JPEG images stored in the given + * destination buffers. Lossless transforms work by moving the raw + * coefficients from one JPEG image structure to another without altering the + * values of the coefficients. While this is typically faster than + * decompressing the image, transforming it, and re-compressing it, lossless + * transforms are not free. Each lossless transform requires reading and + * performing Huffman decoding on all of the coefficients in the source + * image, regardless of the size of the destination image. Thus, this method + * provides a means of generating multiple transformed images from the same + * source or of applying multiple transformations simultaneously, in order to + * eliminate the need to read the source coefficients multiple times. * - * @param dstBufs an array of image buffers. dstbufs[i] will - * receive a JPEG image that has been transformed using the parameters in - * transforms[i]. Use {@link TJ#bufSize} to determine the - * maximum size for each buffer based on the transformed or cropped width and - * height and the level of subsampling used in the source image. + * @param dstBufs an array of JPEG destination buffers. + * dstbufs[i] will receive a JPEG image that has been + * transformed using the parameters in transforms[i]. Use + * {@link TJ#bufSize} to determine the maximum size for each buffer based on + * the transformed or cropped width and height and the level of subsampling + * used in the source image. * * @param transforms an array of {@link TJTransform} instances, each of * which specifies the transform parameters and/or cropping region for the - * corresponding transformed output image + * corresponding transformed JPEG image * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} @@ -103,13 +107,13 @@ public void transform(byte[][] dstBufs, TJTransform[] transforms, } /** - * Losslessly transform the JPEG image associated with this transformer - * instance and return an array of {@link TJDecompressor} instances, each of - * which has a transformed JPEG image associated with it. + * Losslessly transform the JPEG source image associated with this + * transformer instance and return an array of {@link TJDecompressor} + * instances, each of which has a transformed JPEG image associated with it. * * @param transforms an array of {@link TJTransform} instances, each of * which specifies the transform parameters and/or cropping region for the - * corresponding transformed output image + * corresponding transformed JPEG image * * @param flags the bitwise OR of one or more of * {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*} @@ -139,10 +143,10 @@ public TJDecompressor[] transform(TJTransform[] transforms, int flags) /** * Returns an array containing the sizes of the transformed JPEG images - * generated by the most recent transform operation. + * (in bytes) generated by the most recent transform operation. * * @return an array containing the sizes of the transformed JPEG images - * generated by the most recent transform operation. + * (in bytes) generated by the most recent transform operation. */ public int[] getTransformedSizes() { if (transformedSizes == null) diff --git a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/YUVImage.java b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/YUVImage.java index 4da9843a8a5..94830464752 100644 --- a/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/YUVImage.java +++ b/third-party/mozjpeg/mozjpeg/java/org/libjpegturbo/turbojpeg/YUVImage.java @@ -1,5 +1,5 @@ /* - * Copyright (C)2014, 2017 D. R. Commander. All Rights Reserved. + * Copyright (C)2014, 2017, 2023 D. R. Commander. All Rights Reserved. * Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without @@ -30,7 +30,7 @@ package org.libjpegturbo.turbojpeg; /** - * This class encapsulates a YUV planar image and the metadata + * This class encapsulates a planar YUV image and the metadata * associated with it. The TurboJPEG API allows both the JPEG compression and * decompression pipelines to be split into stages: YUV encode, compress from * YUV, decompress to YUV, and YUV decode. A YUVImage instance @@ -38,30 +38,32 @@ * operations and as the source image for compress-from-YUV and YUV decode * operations. *

    - * Technically, the JPEG format uses the YCbCr colorspace (which technically is - * not a "colorspace" but rather a "color transform"), but per the convention - * of the digital video community, the TurboJPEG API uses "YUV" to refer to an - * image format consisting of Y, Cb, and Cr image planes. + * Technically, the JPEG format uses the YCbCr colorspace (which is technically + * not a colorspace but a color transform), but per the convention of the + * digital video community, the TurboJPEG API uses "YUV" to refer to an image + * format consisting of Y, Cb, and Cr image planes. *

    * Each plane is simply a 2D array of bytes, each byte representing the value * of one of the components (Y, Cb, or Cr) at a particular location in the * image. The width and height of each plane are determined by the image * width, height, and level of chrominance subsampling. The luminance plane * width is the image width padded to the nearest multiple of the horizontal - * subsampling factor (2 in the case of 4:2:0 and 4:2:2, 4 in the case of - * 4:1:1, 1 in the case of 4:4:4 or grayscale.) Similarly, the luminance plane - * height is the image height padded to the nearest multiple of the vertical - * subsampling factor (2 in the case of 4:2:0 or 4:4:0, 1 in the case of 4:4:4 - * or grayscale.) The chrominance plane width is equal to the luminance plane - * width divided by the horizontal subsampling factor, and the chrominance - * plane height is equal to the luminance plane height divided by the vertical - * subsampling factor. + * subsampling factor (1 in the case of 4:4:4, grayscale, or 4:4:0; 2 in the + * case of 4:2:2 or 4:2:0; 4 in the case of 4:1:1.) Similarly, the luminance + * plane height is the image height padded to the nearest multiple of the + * vertical subsampling factor (1 in the case of 4:4:4, 4:2:2, grayscale, or + * 4:1:1; 2 in the case of 4:2:0 or 4:4:0.) This is irrespective of any + * additional padding that may be specified as an argument to the various + * YUVImage methods. The chrominance plane width is equal to the luminance + * plane width divided by the horizontal subsampling factor, and the + * chrominance plane height is equal to the luminance plane height divided by + * the vertical subsampling factor. *

    * For example, if the source image is 35 x 35 pixels and 4:2:2 subsampling is * used, then the luminance plane would be 36 x 35 bytes, and each of the - * chrominance planes would be 18 x 35 bytes. If you specify a line padding of - * 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, and - * each of the chrominance planes would be 20 x 35 bytes. + * chrominance planes would be 18 x 35 bytes. If you specify a row alignment + * of 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, + * and each of the chrominance planes would be 20 x 35 bytes. */ public class YUVImage { @@ -75,7 +77,7 @@ public class YUVImage { * @param width width (in pixels) of the YUV image * * @param strides an array of integers, each specifying the number of bytes - * per line in the corresponding plane of the YUV image. Setting the stride + * per row in the corresponding plane of the YUV image. Setting the stride * for any plane to 0 is the same as setting it to the plane width (see * {@link YUVImage above}.) If strides is null, then the * strides for all planes will be set to their respective plane widths. When @@ -92,22 +94,24 @@ public YUVImage(int width, int[] strides, int height, int subsamp) { } /** - * Create a new YUVImage instance backed by a unified image - * buffer, and allocate memory for the image buffer. + * Create a new YUVImage instance backed by a unified buffer, + * and allocate memory for the buffer. * * @param width width (in pixels) of the YUV image * - * @param pad Each line of each plane in the YUV image buffer will be padded - * to this number of bytes (must be a power of 2.) + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n specifies that each row in each plane of + * the YUV image will be padded to the nearest multiple of n bytes + * (1 = unpadded.) * * @param height height (in pixels) of the YUV image * * @param subsamp the level of chrominance subsampling to be used in the YUV * image (one of {@link TJ#SAMP_444 TJ.SAMP_*}) */ - public YUVImage(int width, int pad, int height, int subsamp) { - setBuf(new byte[TJ.bufSizeYUV(width, pad, height, subsamp)], width, pad, - height, subsamp); + public YUVImage(int width, int align, int height, int subsamp) { + setBuf(new byte[TJ.bufSizeYUV(width, align, height, subsamp)], width, + align, height, subsamp); } /** @@ -117,8 +121,8 @@ public YUVImage(int width, int pad, int height, int subsamp) { * @param planes an array of buffers representing the Y, U (Cb), and V (Cr) * image planes (or just the Y plane, if the image is grayscale.) These * planes can be contiguous or non-contiguous in memory. Plane - * i should be at least offsets[i] + - * {@link TJ#planeSizeYUV TJ.planeSizeYUV}(i, width, strides[i], height, subsamp) + * i should be at least offsets[i] + + * {@link TJ#planeSizeYUV TJ.planeSizeYUV}(i, width, strides[i], height, subsamp) * bytes in size. * * @param offsets If this YUVImage instance represents a @@ -130,11 +134,11 @@ public YUVImage(int width, int pad, int height, int subsamp) { * @param width width (in pixels) of the new YUV image (or subregion) * * @param strides an array of integers, each specifying the number of bytes - * per line in the corresponding plane of the YUV image. Setting the stride + * per row in the corresponding plane of the YUV image. Setting the stride * for any plane to 0 is the same as setting it to the plane width (see * {@link YUVImage above}.) If strides is null, then the * strides for all planes will be set to their respective plane widths. You - * can adjust the strides in order to add an arbitrary amount of line padding + * can adjust the strides in order to add an arbitrary amount of row padding * to each plane or to specify that this YUVImage instance is a * subregion of a larger image (in which case, strides[i] should * be set to the plane width of plane i in the larger image.) @@ -150,29 +154,30 @@ public YUVImage(byte[][] planes, int[] offsets, int width, int[] strides, } /** - * Create a new YUVImage instance from an existing unified image + * Create a new YUVImage instance from an existing unified * buffer. * - * @param yuvImage image buffer that contains or will contain YUV planar - * image data. Use {@link TJ#bufSizeYUV} to determine the minimum size for - * this buffer. The Y, U (Cb), and V (Cr) image planes are stored - * sequentially in the buffer (see {@link YUVImage above} for a description - * of the image format.) + * @param yuvImage buffer that contains or will receive a unified planar YUV + * image. Use {@link TJ#bufSizeYUV} to determine the minimum size for this + * buffer. The Y, U (Cb), and V (Cr) image planes are stored sequentially in + * the buffer. (See {@link YUVImage above} for a description of the image + * format.) * * @param width width (in pixels) of the YUV image * - * @param pad the line padding used in the YUV image buffer. For - * instance, if each line in each plane of the buffer is padded to the - * nearest multiple of 4 bytes, then pad should be set to 4. + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n specifies that each row in each plane of + * the YUV image will be padded to the nearest multiple of n bytes + * (1 = unpadded.) * * @param height height (in pixels) of the YUV image * * @param subsamp the level of chrominance subsampling used in the YUV * image (one of {@link TJ#SAMP_444 TJ.SAMP_*}) */ - public YUVImage(byte[] yuvImage, int width, int pad, int height, + public YUVImage(byte[] yuvImage, int width, int align, int height, int subsamp) { - setBuf(yuvImage, width, pad, height, subsamp); + setBuf(yuvImage, width, align, height, subsamp); } /** @@ -181,8 +186,8 @@ public YUVImage(byte[] yuvImage, int width, int pad, int height, * @param planes an array of buffers representing the Y, U (Cb), and V (Cr) * image planes (or just the Y plane, if the image is grayscale.) These * planes can be contiguous or non-contiguous in memory. Plane - * i should be at least offsets[i] + - * {@link TJ#planeSizeYUV TJ.planeSizeYUV}(i, width, strides[i], height, subsamp) + * i should be at least offsets[i] + + * {@link TJ#planeSizeYUV TJ.planeSizeYUV}(i, width, strides[i], height, subsamp) * bytes in size. * * @param offsets If this YUVImage instance represents a @@ -194,12 +199,12 @@ public YUVImage(byte[] yuvImage, int width, int pad, int height, * @param width width (in pixels) of the YUV image (or subregion) * * @param strides an array of integers, each specifying the number of bytes - * per line in the corresponding plane of the YUV image. Setting the stride + * per row in the corresponding plane of the YUV image. Setting the stride * for any plane to 0 is the same as setting it to the plane width (see * {@link YUVImage above}.) If strides is null, then the * strides for all planes will be set to their respective plane widths. You - * can adjust the strides in order to add an arbitrary amount of line padding - * to each plane or to specify that this YUVImage image is a + * can adjust the strides in order to add an arbitrary amount of row padding + * to each plane or to specify that this YUVImage instance is a * subregion of a larger image (in which case, strides[i] should * be set to the plane width of plane i in the larger image.) * @@ -263,32 +268,34 @@ private void setBuf(byte[][] planes, int[] offsets, int width, int[] strides, } /** - * Assign a unified image buffer to this YUVImage instance. + * Assign a unified buffer to this YUVImage instance. * - * @param yuvImage image buffer that contains or will contain YUV planar - * image data. Use {@link TJ#bufSizeYUV} to determine the minimum size for - * this buffer. The Y, U (Cb), and V (Cr) image planes are stored - * sequentially in the buffer (see {@link YUVImage above} for a description - * of the image format.) + * @param yuvImage buffer that contains or will receive a unified planar YUV + * image. Use {@link TJ#bufSizeYUV} to determine the minimum size for this + * buffer. The Y, U (Cb), and V (Cr) image planes are stored sequentially in + * the buffer. (See {@link YUVImage above} for a description of the image + * format.) * * @param width width (in pixels) of the YUV image * - * @param pad the line padding used in the YUV image buffer. For - * instance, if each line in each plane of the buffer is padded to the - * nearest multiple of 4 bytes, then pad should be set to 4. + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n specifies that each row in each plane of + * the YUV image will be padded to the nearest multiple of n bytes + * (1 = unpadded.) * * @param height height (in pixels) of the YUV image * * @param subsamp the level of chrominance subsampling used in the YUV * image (one of {@link TJ#SAMP_444 TJ.SAMP_*}) */ - public void setBuf(byte[] yuvImage, int width, int pad, int height, + public void setBuf(byte[] yuvImage, int width, int align, int height, int subsamp) { - if (yuvImage == null || width < 1 || pad < 1 || ((pad & (pad - 1)) != 0) || - height < 1 || subsamp < 0 || subsamp >= TJ.NUMSAMP) + if (yuvImage == null || width < 1 || align < 1 || + ((align & (align - 1)) != 0) || height < 1 || subsamp < 0 || + subsamp >= TJ.NUMSAMP) throw new IllegalArgumentException("Invalid argument in YUVImage::setBuf()"); - if (yuvImage.length < TJ.bufSizeYUV(width, pad, height, subsamp)) - throw new IllegalArgumentException("YUV image buffer is not large enough"); + if (yuvImage.length < TJ.bufSizeYUV(width, align, height, subsamp)) + throw new IllegalArgumentException("YUV buffer is not large enough"); int nc = (subsamp == TJ.SAMP_GRAY ? 1 : 3); byte[][] planes = new byte[nc][]; @@ -296,9 +303,9 @@ public void setBuf(byte[] yuvImage, int width, int pad, int height, int[] offsets = new int[nc]; planes[0] = yuvImage; - strides[0] = pad(TJ.planeWidth(0, width, subsamp), pad); + strides[0] = pad(TJ.planeWidth(0, width, subsamp), align); if (subsamp != TJ.SAMP_GRAY) { - strides[1] = strides[2] = pad(TJ.planeWidth(1, width, subsamp), pad); + strides[1] = strides[2] = pad(TJ.planeWidth(1, width, subsamp), align); planes[1] = planes[2] = yuvImage; offsets[1] = offsets[0] + strides[0] * TJ.planeHeight(0, height, subsamp); @@ -306,7 +313,7 @@ public void setBuf(byte[] yuvImage, int width, int pad, int height, strides[1] * TJ.planeHeight(1, height, subsamp); } - yuvPad = pad; + yuvAlign = align; setBuf(planes, offsets, width, strides, height, subsamp); } @@ -333,23 +340,23 @@ public int getHeight() { } /** - * Returns the line padding used in the YUV image buffer (if this image is + * Returns the row alignment (in bytes) of the YUV buffer (if this image is * stored in a unified buffer rather than separate image planes.) * - * @return the line padding used in the YUV image buffer + * @return the row alignment of the YUV buffer */ public int getPad() { if (yuvPlanes == null) throw new IllegalStateException(NO_ASSOC_ERROR); - if (yuvPad < 1 || ((yuvPad & (yuvPad - 1)) != 0)) + if (yuvAlign < 1 || ((yuvAlign & (yuvAlign - 1)) != 0)) throw new IllegalStateException("Image is not stored in a unified buffer"); - return yuvPad; + return yuvAlign; } /** - * Returns the number of bytes per line of each plane in the YUV image. + * Returns the number of bytes per row of each plane in the YUV image. * - * @return the number of bytes per line of each plane in the YUV image + * @return the number of bytes per row of each plane in the YUV image */ public int[] getStrides() { if (yuvStrides == null) @@ -395,10 +402,10 @@ public byte[][] getPlanes() { } /** - * Returns the YUV image buffer (if this image is stored in a unified - * buffer rather than separate image planes.) + * Returns the YUV buffer (if this image is stored in a unified buffer rather + * than separate image planes.) * - * @return the YUV image buffer + * @return the YUV buffer */ public byte[] getBuf() { if (yuvPlanes == null || yuvSubsamp < 0 || yuvSubsamp >= TJ.NUMSAMP) @@ -412,22 +419,22 @@ public byte[] getBuf() { } /** - * Returns the size (in bytes) of the YUV image buffer (if this image is - * stored in a unified buffer rather than separate image planes.) + * Returns the size (in bytes) of the YUV buffer (if this image is stored in + * a unified buffer rather than separate image planes.) * - * @return the size (in bytes) of the YUV image buffer + * @return the size (in bytes) of the YUV buffer */ public int getSize() { if (yuvPlanes == null || yuvSubsamp < 0 || yuvSubsamp >= TJ.NUMSAMP) throw new IllegalStateException(NO_ASSOC_ERROR); int nc = (yuvSubsamp == TJ.SAMP_GRAY ? 1 : 3); - if (yuvPad < 1) + if (yuvAlign < 1) throw new IllegalStateException("Image is not stored in a unified buffer"); for (int i = 1; i < nc; i++) { if (yuvPlanes[i] != yuvPlanes[0]) throw new IllegalStateException("Image is not stored in a unified buffer"); } - return TJ.bufSizeYUV(yuvWidth, yuvPad, yuvHeight, yuvSubsamp); + return TJ.bufSizeYUV(yuvWidth, yuvAlign, yuvHeight, yuvSubsamp); } private static int pad(int v, int p) { @@ -438,7 +445,7 @@ private static int pad(int v, int p) { protected byte[][] yuvPlanes = null; protected int[] yuvOffsets = null; protected int[] yuvStrides = null; - protected int yuvPad = 0; + protected int yuvAlign = 1; protected int yuvWidth = 0; protected int yuvHeight = 0; protected int yuvSubsamp = -1; diff --git a/third-party/mozjpeg/mozjpeg/java/org_libjpegturbo_turbojpeg_TJ.h b/third-party/mozjpeg/mozjpeg/java/org_libjpegturbo_turbojpeg_TJ.h index 84ee871134d..8ec4ecd62da 100644 --- a/third-party/mozjpeg/mozjpeg/java/org_libjpegturbo_turbojpeg_TJ.h +++ b/third-party/mozjpeg/mozjpeg/java/org_libjpegturbo_turbojpeg_TJ.h @@ -61,12 +61,32 @@ extern "C" { #define org_libjpegturbo_turbojpeg_TJ_CS_YCCK 4L #undef org_libjpegturbo_turbojpeg_TJ_FLAG_BOTTOMUP #define org_libjpegturbo_turbojpeg_TJ_FLAG_BOTTOMUP 2L +#undef org_libjpegturbo_turbojpeg_TJ_FLAG_FORCEMMX +#define org_libjpegturbo_turbojpeg_TJ_FLAG_FORCEMMX 8L +#undef org_libjpegturbo_turbojpeg_TJ_FLAG_FORCESSE +#define org_libjpegturbo_turbojpeg_TJ_FLAG_FORCESSE 16L +#undef org_libjpegturbo_turbojpeg_TJ_FLAG_FORCESSE2 +#define org_libjpegturbo_turbojpeg_TJ_FLAG_FORCESSE2 32L +#undef org_libjpegturbo_turbojpeg_TJ_FLAG_FORCESSE3 +#define org_libjpegturbo_turbojpeg_TJ_FLAG_FORCESSE3 128L #undef org_libjpegturbo_turbojpeg_TJ_FLAG_FASTUPSAMPLE #define org_libjpegturbo_turbojpeg_TJ_FLAG_FASTUPSAMPLE 256L #undef org_libjpegturbo_turbojpeg_TJ_FLAG_FASTDCT #define org_libjpegturbo_turbojpeg_TJ_FLAG_FASTDCT 2048L #undef org_libjpegturbo_turbojpeg_TJ_FLAG_ACCURATEDCT #define org_libjpegturbo_turbojpeg_TJ_FLAG_ACCURATEDCT 4096L +#undef org_libjpegturbo_turbojpeg_TJ_FLAG_STOPONWARNING +#define org_libjpegturbo_turbojpeg_TJ_FLAG_STOPONWARNING 8192L +#undef org_libjpegturbo_turbojpeg_TJ_FLAG_PROGRESSIVE +#define org_libjpegturbo_turbojpeg_TJ_FLAG_PROGRESSIVE 16384L +#undef org_libjpegturbo_turbojpeg_TJ_FLAG_LIMITSCANS +#define org_libjpegturbo_turbojpeg_TJ_FLAG_LIMITSCANS 32768L +#undef org_libjpegturbo_turbojpeg_TJ_NUMERR +#define org_libjpegturbo_turbojpeg_TJ_NUMERR 2L +#undef org_libjpegturbo_turbojpeg_TJ_ERR_WARNING +#define org_libjpegturbo_turbojpeg_TJ_ERR_WARNING 0L +#undef org_libjpegturbo_turbojpeg_TJ_ERR_FATAL +#define org_libjpegturbo_turbojpeg_TJ_ERR_FATAL 1L /* * Class: org_libjpegturbo_turbojpeg_TJ * Method: bufSize diff --git a/third-party/mozjpeg/mozjpeg/jcapimin.c b/third-party/mozjpeg/mozjpeg/jcapimin.c index d0698f9b701..6e609e9763e 100644 --- a/third-party/mozjpeg/mozjpeg/jcapimin.c +++ b/third-party/mozjpeg/mozjpeg/jcapimin.c @@ -5,8 +5,9 @@ * Copyright (C) 1994-1998, Thomas G. Lane. * Modified 2003-2010 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2014, D. R. Commander. - * For conditions of distribution and use, see the accompanying README file. + * Copyright (C) 2022, D. R. Commander. + * For conditions of distribution and use, see the accompanying README.ijg + * file. * * This file contains application interface code for the compression half * of the JPEG library. These are the "minimum" API routines that may be @@ -53,7 +54,7 @@ jpeg_CreateCompress(j_compress_ptr cinfo, int version, size_t structsize) { struct jpeg_error_mgr *err = cinfo->err; void *client_data = cinfo->client_data; /* ignore Purify complaint here */ - MEMZERO(cinfo, sizeof(struct jpeg_compress_struct)); + memset(cinfo, 0, sizeof(struct jpeg_compress_struct)); cinfo->err = err; cinfo->client_data = client_data; } @@ -100,7 +101,7 @@ jpeg_CreateCompress(j_compress_ptr cinfo, int version, size_t structsize) cinfo->master = (struct jpeg_comp_master *) (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT, sizeof(my_comp_master)); - MEMZERO(cinfo->master, sizeof(my_comp_master)); + memset(cinfo->master, 0, sizeof(my_comp_master)); cinfo->master->compress_profile = JCP_MAX_COMPRESSION; } diff --git a/third-party/mozjpeg/mozjpeg/jcarith.c b/third-party/mozjpeg/mozjpeg/jcarith.c index 23e2e09e25e..7a545d9ecc3 100644 --- a/third-party/mozjpeg/mozjpeg/jcarith.c +++ b/third-party/mozjpeg/mozjpeg/jcarith.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Developed 1997-2009 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2015, 2018, D. R. Commander. + * Copyright (C) 2015, 2018, 2021-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -342,14 +342,14 @@ emit_restart(j_compress_ptr cinfo, int restart_num) compptr = cinfo->cur_comp_info[ci]; /* DC needs no table for refinement scan */ if (cinfo->progressive_mode == 0 || (cinfo->Ss == 0 && cinfo->Ah == 0)) { - MEMZERO(entropy->dc_stats[compptr->dc_tbl_no], DC_STAT_BINS); + memset(entropy->dc_stats[compptr->dc_tbl_no], 0, DC_STAT_BINS); /* Reset DC predictions to 0 */ entropy->last_dc_val[ci] = 0; entropy->dc_context[ci] = 0; } /* AC needs no table when not present */ if (cinfo->progressive_mode == 0 || cinfo->Se) { - MEMZERO(entropy->ac_stats[compptr->ac_tbl_no], AC_STAT_BINS); + memset(entropy->ac_stats[compptr->ac_tbl_no], 0, AC_STAT_BINS); } } @@ -841,7 +841,7 @@ start_pass(j_compress_ptr cinfo, boolean gather_statistics) * We are fully adaptive here and need no extra * statistics gathering pass! */ - ERREXIT(cinfo, JERR_NOT_COMPILED); + ERREXIT(cinfo, JERR_NOTIMPL); /* We assume jcmaster.c already validated the progressive scan parameters. */ @@ -876,7 +876,7 @@ start_pass(j_compress_ptr cinfo, boolean gather_statistics) if (entropy->dc_stats[tbl] == NULL) entropy->dc_stats[tbl] = (unsigned char *)(*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, DC_STAT_BINS); - MEMZERO(entropy->dc_stats[tbl], DC_STAT_BINS); + memset(entropy->dc_stats[tbl], 0, DC_STAT_BINS); /* Initialize DC predictions to 0 */ entropy->last_dc_val[ci] = 0; entropy->dc_context[ci] = 0; @@ -889,7 +889,7 @@ start_pass(j_compress_ptr cinfo, boolean gather_statistics) if (entropy->ac_stats[tbl] == NULL) entropy->ac_stats[tbl] = (unsigned char *)(*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, AC_STAT_BINS); - MEMZERO(entropy->ac_stats[tbl], AC_STAT_BINS); + memset(entropy->ac_stats[tbl], 0, AC_STAT_BINS); #ifdef CALCULATE_SPECTRAL_CONDITIONING if (progressive_mode) /* Section G.1.3.2: Set appropriate arithmetic conditioning value Kx */ diff --git a/third-party/mozjpeg/mozjpeg/jccolext.c b/third-party/mozjpeg/mozjpeg/jccolext.c index 19c955c9d6a..20f891a6330 100644 --- a/third-party/mozjpeg/mozjpeg/jccolext.c +++ b/third-party/mozjpeg/mozjpeg/jccolext.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1996, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2009-2012, 2015, D. R. Commander. + * Copyright (C) 2009-2012, 2015, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -48,9 +48,9 @@ rgb_ycc_convert_internal(j_compress_ptr cinfo, JSAMPARRAY input_buf, outptr2 = output_buf[2][output_row]; output_row++; for (col = 0; col < num_cols; col++) { - r = GETJSAMPLE(inptr[RGB_RED]); - g = GETJSAMPLE(inptr[RGB_GREEN]); - b = GETJSAMPLE(inptr[RGB_BLUE]); + r = RANGE_LIMIT(inptr[RGB_RED]); + g = RANGE_LIMIT(inptr[RGB_GREEN]); + b = RANGE_LIMIT(inptr[RGB_BLUE]); inptr += RGB_PIXELSIZE; /* If the inputs are 0..MAXJSAMPLE, the outputs of these equations * must be too; we do not need an explicit range-limiting operation. @@ -100,9 +100,9 @@ rgb_gray_convert_internal(j_compress_ptr cinfo, JSAMPARRAY input_buf, outptr = output_buf[0][output_row]; output_row++; for (col = 0; col < num_cols; col++) { - r = GETJSAMPLE(inptr[RGB_RED]); - g = GETJSAMPLE(inptr[RGB_GREEN]); - b = GETJSAMPLE(inptr[RGB_BLUE]); + r = RANGE_LIMIT(inptr[RGB_RED]); + g = RANGE_LIMIT(inptr[RGB_GREEN]); + b = RANGE_LIMIT(inptr[RGB_BLUE]); inptr += RGB_PIXELSIZE; /* Y */ outptr[col] = (JSAMPLE)((ctab[r + R_Y_OFF] + ctab[g + G_Y_OFF] + @@ -135,9 +135,9 @@ rgb_rgb_convert_internal(j_compress_ptr cinfo, JSAMPARRAY input_buf, outptr2 = output_buf[2][output_row]; output_row++; for (col = 0; col < num_cols; col++) { - outptr0[col] = GETJSAMPLE(inptr[RGB_RED]); - outptr1[col] = GETJSAMPLE(inptr[RGB_GREEN]); - outptr2[col] = GETJSAMPLE(inptr[RGB_BLUE]); + outptr0[col] = inptr[RGB_RED]; + outptr1[col] = inptr[RGB_GREEN]; + outptr2[col] = inptr[RGB_BLUE]; inptr += RGB_PIXELSIZE; } } diff --git a/third-party/mozjpeg/mozjpeg/jccolor.c b/third-party/mozjpeg/mozjpeg/jccolor.c index 036f6016d18..fb9f1cc1927 100644 --- a/third-party/mozjpeg/mozjpeg/jccolor.c +++ b/third-party/mozjpeg/mozjpeg/jccolor.c @@ -5,7 +5,7 @@ * Copyright (C) 1991-1996, Thomas G. Lane. * libjpeg-turbo Modifications: * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009-2012, 2015, D. R. Commander. + * Copyright (C) 2009-2012, 2015, 2022, D. R. Commander. * Copyright (C) 2014, MIPS Technologies, Inc., California. * For conditions of distribution and use, see the accompanying README.ijg * file. @@ -17,7 +17,6 @@ #include "jinclude.h" #include "jpeglib.h" #include "jsimd.h" -#include "jconfigint.h" /* Private subobject */ @@ -84,6 +83,18 @@ typedef my_color_converter *my_cconvert_ptr; #define B_CR_OFF (7 * (MAXJSAMPLE + 1)) #define TABLE_SIZE (8 * (MAXJSAMPLE + 1)) +/* 12-bit samples use a 16-bit data type, so it is possible to pass + * out-of-range sample values (< 0 or > 4095) to jpeg_write_scanlines(). + * Thus, we mask the incoming 12-bit samples to guard against overrunning + * or underrunning the conversion tables. + */ + +#if BITS_IN_JSAMPLE == 12 +#define RANGE_LIMIT(value) ((value) & 0xFFF) +#else +#define RANGE_LIMIT(value) (value) +#endif + /* Include inline routines for colorspace extensions */ @@ -392,11 +403,11 @@ cmyk_ycck_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, outptr3 = output_buf[3][output_row]; output_row++; for (col = 0; col < num_cols; col++) { - r = MAXJSAMPLE - GETJSAMPLE(inptr[0]); - g = MAXJSAMPLE - GETJSAMPLE(inptr[1]); - b = MAXJSAMPLE - GETJSAMPLE(inptr[2]); + r = MAXJSAMPLE - RANGE_LIMIT(inptr[0]); + g = MAXJSAMPLE - RANGE_LIMIT(inptr[1]); + b = MAXJSAMPLE - RANGE_LIMIT(inptr[2]); /* K passes through as-is */ - outptr3[col] = inptr[3]; /* don't need GETJSAMPLE here */ + outptr3[col] = inptr[3]; inptr += 4; /* If the inputs are 0..MAXJSAMPLE, the outputs of these equations * must be too; we do not need an explicit range-limiting operation. @@ -438,7 +449,7 @@ grayscale_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, outptr = output_buf[0][output_row]; output_row++; for (col = 0; col < num_cols; col++) { - outptr[col] = inptr[0]; /* don't need GETJSAMPLE() here */ + outptr[col] = inptr[0]; inptr += instride; } } @@ -497,7 +508,7 @@ null_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, inptr = *input_buf; outptr = output_buf[ci][output_row]; for (col = 0; col < num_cols; col++) { - outptr[col] = inptr[ci]; /* don't need GETJSAMPLE() here */ + outptr[col] = inptr[ci]; inptr += nc; } } diff --git a/third-party/mozjpeg/mozjpeg/jcdctmgr.c b/third-party/mozjpeg/mozjpeg/jcdctmgr.c index 71594680ca2..f45d1088191 100644 --- a/third-party/mozjpeg/mozjpeg/jcdctmgr.c +++ b/third-party/mozjpeg/mozjpeg/jcdctmgr.c @@ -574,19 +574,19 @@ convsamp (JSAMPARRAY sample_data, JDIMENSION start_col, DCTELEM *workspace) elemptr = sample_data[elemr] + start_col; #if DCTSIZE == 8 /* unroll the inner loop */ - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; #else { register int elemc; for (elemc = DCTSIZE; elemc > 0; elemc--) - *workspaceptr++ = GETJSAMPLE(*elemptr++) - CENTERJSAMPLE; + *workspaceptr++ = (*elemptr++) - CENTERJSAMPLE; } #endif } @@ -774,20 +774,19 @@ convsamp_float(JSAMPARRAY sample_data, JDIMENSION start_col, for (elemr = 0; elemr < DCTSIZE; elemr++) { elemptr = sample_data[elemr] + start_col; #if DCTSIZE == 8 /* unroll the inner loop */ - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); - *workspaceptr++ = (FAST_FLOAT)(GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); #else { register int elemc; for (elemc = DCTSIZE; elemc > 0; elemc--) - *workspaceptr++ = (FAST_FLOAT) - (GETJSAMPLE(*elemptr++) - CENTERJSAMPLE); + *workspaceptr++ = (FAST_FLOAT)((*elemptr++) - CENTERJSAMPLE); } #endif } diff --git a/third-party/mozjpeg/mozjpeg/jchuff.c b/third-party/mozjpeg/mozjpeg/jchuff.c index cb05055d992..937166b5449 100644 --- a/third-party/mozjpeg/mozjpeg/jchuff.c +++ b/third-party/mozjpeg/mozjpeg/jchuff.c @@ -4,8 +4,10 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2009-2011, 2014-2016, 2018-2019, D. R. Commander. + * Copyright (C) 2009-2011, 2014-2016, 2018-2023, D. R. Commander. * Copyright (C) 2015, Matthieu Darbois. + * Copyright (C) 2018, Matthias Räncker. + * Copyright (C) 2020, Arm Limited. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -25,7 +27,6 @@ #include "jinclude.h" #include "jpeglib.h" #include "jsimd.h" -#include "jconfigint.h" #include /* @@ -34,23 +35,28 @@ * memory footprint by 64k, which is important for some mobile applications * that create many isolated instances of libjpeg-turbo (web browsers, for * instance.) This may improve performance on some mobile platforms as well. - * This feature is enabled by default only on ARM processors, because some x86 + * This feature is enabled by default only on Arm processors, because some x86 * chips have a slow implementation of bsr, and the use of clz/bsr cannot be * shown to have a significant performance impact even on the x86 chips that - * have a fast implementation of it. When building for ARMv6, you can + * have a fast implementation of it. When building for Armv6, you can * explicitly disable the use of clz/bsr by adding -mthumb to the compiler * flags (this defines __thumb__). */ /* NOTE: Both GCC and Clang define __GNUC__ */ -#if defined(__GNUC__) && (defined(__arm__) || defined(__aarch64__)) +#if (defined(__GNUC__) && (defined(__arm__) || defined(__aarch64__))) || \ + defined(_M_ARM) || defined(_M_ARM64) #if !defined(__thumb__) || defined(__thumb2__) #define USE_CLZ_INTRINSIC #endif #endif #ifdef USE_CLZ_INTRINSIC +#if defined(_MSC_VER) && !defined(__clang__) +#define JPEG_NBITS_NONZERO(x) (32 - _CountLeadingZeros(x)) +#else #define JPEG_NBITS_NONZERO(x) (32 - __builtin_clz(x)) +#endif #define JPEG_NBITS(x) (x ? JPEG_NBITS_NONZERO(x) : 0) #else #include "jpeg_nbits_table.h" @@ -65,31 +71,42 @@ * but must not be updated permanently until we complete the MCU. */ -typedef struct { - size_t put_buffer; /* current bit-accumulation buffer */ - int put_bits; /* # of bits now in it */ - int last_dc_val[MAX_COMPS_IN_SCAN]; /* last DC coef for each component */ -} savable_state; +#if defined(__x86_64__) && defined(__ILP32__) +typedef unsigned long long bit_buf_type; +#else +typedef size_t bit_buf_type; +#endif -/* This macro is to work around compilers with missing or broken - * structure assignment. You'll need to fix this code if you have - * such a compiler and you change MAX_COMPS_IN_SCAN. +/* NOTE: The more optimal Huffman encoding algorithm is only used by the + * intrinsics implementation of the Arm Neon SIMD extensions, which is why we + * retain the old Huffman encoder behavior when using the GAS implementation. */ - -#ifndef NO_STRUCT_ASSIGN -#define ASSIGN_STATE(dest, src) ((dest) = (src)) +#if defined(WITH_SIMD) && !(defined(__arm__) || defined(__aarch64__) || \ + defined(_M_ARM) || defined(_M_ARM64)) +typedef unsigned long long simd_bit_buf_type; #else -#if MAX_COMPS_IN_SCAN == 4 -#define ASSIGN_STATE(dest, src) \ - ((dest).put_buffer = (src).put_buffer, \ - (dest).put_bits = (src).put_bits, \ - (dest).last_dc_val[0] = (src).last_dc_val[0], \ - (dest).last_dc_val[1] = (src).last_dc_val[1], \ - (dest).last_dc_val[2] = (src).last_dc_val[2], \ - (dest).last_dc_val[3] = (src).last_dc_val[3]) +typedef bit_buf_type simd_bit_buf_type; #endif + +#if (defined(SIZEOF_SIZE_T) && SIZEOF_SIZE_T == 8) || defined(_WIN64) || \ + (defined(__x86_64__) && defined(__ILP32__)) +#define BIT_BUF_SIZE 64 +#elif (defined(SIZEOF_SIZE_T) && SIZEOF_SIZE_T == 4) || defined(_WIN32) +#define BIT_BUF_SIZE 32 +#else +#error Cannot determine word size #endif +#define SIMD_BIT_BUF_SIZE (sizeof(simd_bit_buf_type) * 8) +typedef struct { + union { + bit_buf_type c; + simd_bit_buf_type simd; + } put_buffer; /* current bit accumulation buffer */ + int free_bits; /* # of bits available in it */ + /* (Neon GAS: # of bits now in it) */ + int last_dc_val[MAX_COMPS_IN_SCAN]; /* last DC coef for each component */ +} savable_state; typedef struct { struct jpeg_entropy_encoder pub; /* public fields */ @@ -123,6 +140,7 @@ typedef struct { size_t free_in_buffer; /* # of byte spaces remaining in buffer */ savable_state cur; /* Current bit buffer & DC state */ j_compress_ptr cinfo; /* dump_buffer needs access to this */ + int simd; } working_state; @@ -181,12 +199,12 @@ start_pass_huff(j_compress_ptr cinfo, boolean gather_statistics) entropy->dc_count_ptrs[dctbl] = (long *) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, 257 * sizeof(long)); - MEMZERO(entropy->dc_count_ptrs[dctbl], 257 * sizeof(long)); + memset(entropy->dc_count_ptrs[dctbl], 0, 257 * sizeof(long)); if (entropy->ac_count_ptrs[actbl] == NULL) entropy->ac_count_ptrs[actbl] = (long *) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, 257 * sizeof(long)); - MEMZERO(entropy->ac_count_ptrs[actbl], 257 * sizeof(long)); + memset(entropy->ac_count_ptrs[actbl], 0, 257 * sizeof(long)); #endif } else { /* Compute derived values for Huffman tables */ @@ -201,8 +219,17 @@ start_pass_huff(j_compress_ptr cinfo, boolean gather_statistics) } /* Initialize bit buffer to empty */ - entropy->saved.put_buffer = 0; - entropy->saved.put_bits = 0; + if (entropy->simd) { + entropy->saved.put_buffer.simd = 0; +#if defined(__aarch64__) && !defined(NEON_INTRINSICS) + entropy->saved.free_bits = 0; +#else + entropy->saved.free_bits = SIMD_BIT_BUF_SIZE; +#endif + } else { + entropy->saved.put_buffer.c = 0; + entropy->saved.free_bits = BIT_BUF_SIZE; + } /* Initialize restart stuff */ entropy->restarts_to_go = cinfo->restart_interval; @@ -287,7 +314,8 @@ jpeg_make_c_derived_tbl(j_compress_ptr cinfo, boolean isDC, int tblno, * this lets us detect duplicate VAL entries here, and later * allows emit_bits to detect any attempt to emit such symbols. */ - MEMZERO(dtbl->ehufsi, sizeof(dtbl->ehufsi)); + memset(dtbl->ehufco, 0, sizeof(dtbl->ehufco)); + memset(dtbl->ehufsi, 0, sizeof(dtbl->ehufsi)); /* This is also a convenient place to check for out-of-range * and duplicated VAL entries. We allow 0..255 for AC symbols @@ -334,94 +362,94 @@ dump_buffer(working_state *state) /* Outputting bits to the file */ -/* These macros perform the same task as the emit_bits() function in the - * original libjpeg code. In addition to reducing overhead by explicitly - * inlining the code, additional performance is achieved by taking into - * account the size of the bit buffer and waiting until it is almost full - * before emptying it. This mostly benefits 64-bit platforms, since 6 - * bytes can be stored in a 64-bit bit buffer before it has to be emptied. +/* Output byte b and, speculatively, an additional 0 byte. 0xFF must be + * encoded as 0xFF 0x00, so the output buffer pointer is advanced by 2 if the + * byte is 0xFF. Otherwise, the output buffer pointer is advanced by 1, and + * the speculative 0 byte will be overwritten by the next byte. */ - -#define EMIT_BYTE() { \ - JOCTET c; \ - put_bits -= 8; \ - c = (JOCTET)GETJOCTET(put_buffer >> put_bits); \ - *buffer++ = c; \ - if (c == 0xFF) /* need to stuff a zero byte? */ \ - *buffer++ = 0; \ +#define EMIT_BYTE(b) { \ + buffer[0] = (JOCTET)(b); \ + buffer[1] = 0; \ + buffer -= -2 + ((JOCTET)(b) < 0xFF); \ } -#define PUT_BITS(code, size) { \ - put_bits += size; \ - put_buffer = (put_buffer << size) | code; \ -} - -#if SIZEOF_SIZE_T != 8 && !defined(_WIN64) - -#define CHECKBUF15() { \ - if (put_bits > 15) { \ - EMIT_BYTE() \ - EMIT_BYTE() \ +/* Output the entire bit buffer. If there are no 0xFF bytes in it, then write + * directly to the output buffer. Otherwise, use the EMIT_BYTE() macro to + * encode 0xFF as 0xFF 0x00. + */ +#if BIT_BUF_SIZE == 64 + +#define FLUSH() { \ + if (put_buffer & 0x8080808080808080 & ~(put_buffer + 0x0101010101010101)) { \ + EMIT_BYTE(put_buffer >> 56) \ + EMIT_BYTE(put_buffer >> 48) \ + EMIT_BYTE(put_buffer >> 40) \ + EMIT_BYTE(put_buffer >> 32) \ + EMIT_BYTE(put_buffer >> 24) \ + EMIT_BYTE(put_buffer >> 16) \ + EMIT_BYTE(put_buffer >> 8) \ + EMIT_BYTE(put_buffer ) \ + } else { \ + buffer[0] = (JOCTET)(put_buffer >> 56); \ + buffer[1] = (JOCTET)(put_buffer >> 48); \ + buffer[2] = (JOCTET)(put_buffer >> 40); \ + buffer[3] = (JOCTET)(put_buffer >> 32); \ + buffer[4] = (JOCTET)(put_buffer >> 24); \ + buffer[5] = (JOCTET)(put_buffer >> 16); \ + buffer[6] = (JOCTET)(put_buffer >> 8); \ + buffer[7] = (JOCTET)(put_buffer); \ + buffer += 8; \ } \ } -#endif - -#define CHECKBUF31() { \ - if (put_bits > 31) { \ - EMIT_BYTE() \ - EMIT_BYTE() \ - EMIT_BYTE() \ - EMIT_BYTE() \ - } \ -} +#else -#define CHECKBUF47() { \ - if (put_bits > 47) { \ - EMIT_BYTE() \ - EMIT_BYTE() \ - EMIT_BYTE() \ - EMIT_BYTE() \ - EMIT_BYTE() \ - EMIT_BYTE() \ +#define FLUSH() { \ + if (put_buffer & 0x80808080 & ~(put_buffer + 0x01010101)) { \ + EMIT_BYTE(put_buffer >> 24) \ + EMIT_BYTE(put_buffer >> 16) \ + EMIT_BYTE(put_buffer >> 8) \ + EMIT_BYTE(put_buffer ) \ + } else { \ + buffer[0] = (JOCTET)(put_buffer >> 24); \ + buffer[1] = (JOCTET)(put_buffer >> 16); \ + buffer[2] = (JOCTET)(put_buffer >> 8); \ + buffer[3] = (JOCTET)(put_buffer); \ + buffer += 4; \ } \ } -#if !defined(_WIN32) && !defined(SIZEOF_SIZE_T) -#error Cannot determine word size #endif -#if SIZEOF_SIZE_T == 8 || defined(_WIN64) - -#define EMIT_BITS(code, size) { \ - CHECKBUF47() \ - PUT_BITS(code, size) \ -} - -#define EMIT_CODE(code, size) { \ - temp2 &= (((JLONG)1) << nbits) - 1; \ - CHECKBUF31() \ - PUT_BITS(code, size) \ - PUT_BITS(temp2, nbits) \ +/* Fill the bit buffer to capacity with the leading bits from code, then output + * the bit buffer and put the remaining bits from code into the bit buffer. + */ +#define PUT_AND_FLUSH(code, size) { \ + put_buffer = (put_buffer << (size + free_bits)) | (code >> -free_bits); \ + FLUSH() \ + free_bits += BIT_BUF_SIZE; \ + put_buffer = code; \ } -#else - -#define EMIT_BITS(code, size) { \ - PUT_BITS(code, size) \ - CHECKBUF15() \ +/* Insert code into the bit buffer and output the bit buffer if needed. + * NOTE: We can't flush with free_bits == 0, since the left shift in + * PUT_AND_FLUSH() would have undefined behavior. + */ +#define PUT_BITS(code, size) { \ + free_bits -= size; \ + if (free_bits < 0) \ + PUT_AND_FLUSH(code, size) \ + else \ + put_buffer = (put_buffer << size) | code; \ } -#define EMIT_CODE(code, size) { \ - temp2 &= (((JLONG)1) << nbits) - 1; \ - PUT_BITS(code, size) \ - CHECKBUF15() \ - PUT_BITS(temp2, nbits) \ - CHECKBUF15() \ +#define PUT_CODE(code, size) { \ + temp &= (((JLONG)1) << nbits) - 1; \ + temp |= code << nbits; \ + nbits += size; \ + PUT_BITS(temp, nbits) \ } -#endif - /* Although it is exceedingly rare, it is possible for a Huffman-encoded * coefficient block to be larger than the 128-byte unencoded block. For each @@ -444,11 +472,12 @@ dump_buffer(working_state *state) #define STORE_BUFFER() { \ if (localbuf) { \ + size_t bytes, bytestocopy; \ bytes = buffer - _buffer; \ buffer = _buffer; \ while (bytes > 0) { \ bytestocopy = MIN(bytes, state->free_in_buffer); \ - MEMCOPY(state->next_output_byte, buffer, bytestocopy); \ + memcpy(state->next_output_byte, buffer, bytestocopy); \ state->next_output_byte += bytestocopy; \ buffer += bytestocopy; \ state->free_in_buffer -= bytestocopy; \ @@ -466,20 +495,48 @@ dump_buffer(working_state *state) LOCAL(boolean) flush_bits(working_state *state) { - JOCTET _buffer[BUFSIZE], *buffer; - size_t put_buffer; int put_bits; - size_t bytes, bytestocopy; int localbuf = 0; + JOCTET _buffer[BUFSIZE], *buffer, temp; + simd_bit_buf_type put_buffer; int put_bits; + int localbuf = 0; + + if (state->simd) { + if (state->cur.free_bits < 0) + ERREXIT(state->cinfo, JERR_BAD_DCT_COEF); +#if defined(__aarch64__) && !defined(NEON_INTRINSICS) + put_bits = state->cur.free_bits; +#else + put_bits = SIMD_BIT_BUF_SIZE - state->cur.free_bits; +#endif + put_buffer = state->cur.put_buffer.simd; + } else { + put_bits = BIT_BUF_SIZE - state->cur.free_bits; + put_buffer = state->cur.put_buffer.c; + } - put_buffer = state->cur.put_buffer; - put_bits = state->cur.put_bits; LOAD_BUFFER() - /* fill any partial byte with ones */ - PUT_BITS(0x7F, 7) - while (put_bits >= 8) EMIT_BYTE() + while (put_bits >= 8) { + put_bits -= 8; + temp = (JOCTET)(put_buffer >> put_bits); + EMIT_BYTE(temp) + } + if (put_bits > 0) { + /* fill partial byte with ones */ + temp = (JOCTET)((put_buffer << (8 - put_bits)) | (0xFF >> put_bits)); + EMIT_BYTE(temp) + } - state->cur.put_buffer = 0; /* and reset bit-buffer to empty */ - state->cur.put_bits = 0; + if (state->simd) { /* and reset bit buffer to empty */ + state->cur.put_buffer.simd = 0; +#if defined(__aarch64__) && !defined(NEON_INTRINSICS) + state->cur.free_bits = 0; +#else + state->cur.free_bits = SIMD_BIT_BUF_SIZE; +#endif + } else { + state->cur.put_buffer.c = 0; + state->cur.free_bits = BIT_BUF_SIZE; + } STORE_BUFFER() return TRUE; @@ -493,7 +550,7 @@ encode_one_block_simd(working_state *state, JCOEFPTR block, int last_dc_val, c_derived_tbl *dctbl, c_derived_tbl *actbl) { JOCTET _buffer[BUFSIZE], *buffer; - size_t bytes, bytestocopy; int localbuf = 0; + int localbuf = 0; LOAD_BUFFER() @@ -509,53 +566,46 @@ LOCAL(boolean) encode_one_block(working_state *state, JCOEFPTR block, int last_dc_val, c_derived_tbl *dctbl, c_derived_tbl *actbl) { - int temp, temp2, temp3; - int nbits; - int r, code, size; + int temp, nbits, free_bits; + bit_buf_type put_buffer; JOCTET _buffer[BUFSIZE], *buffer; - size_t put_buffer; int put_bits; - int code_0xf0 = actbl->ehufco[0xf0], size_0xf0 = actbl->ehufsi[0xf0]; - size_t bytes, bytestocopy; int localbuf = 0; + int localbuf = 0; - put_buffer = state->cur.put_buffer; - put_bits = state->cur.put_bits; + free_bits = state->cur.free_bits; + put_buffer = state->cur.put_buffer.c; LOAD_BUFFER() /* Encode the DC coefficient difference per section F.1.2.1 */ - temp = temp2 = block[0] - last_dc_val; + temp = block[0] - last_dc_val; /* This is a well-known technique for obtaining the absolute value without a * branch. It is derived from an assembly language technique presented in * "How to Optimize for the Pentium Processors", Copyright (c) 1996, 1997 by - * Agner Fog. + * Agner Fog. This code assumes we are on a two's complement machine. */ - temp3 = temp >> (CHAR_BIT * sizeof(int) - 1); - temp ^= temp3; - temp -= temp3; - - /* For a negative input, want temp2 = bitwise complement of abs(input) */ - /* This code assumes we are on a two's complement machine */ - temp2 += temp3; + nbits = temp >> (CHAR_BIT * sizeof(int) - 1); + temp += nbits; + nbits ^= temp; /* Find the number of bits needed for the magnitude of the coefficient */ - nbits = JPEG_NBITS(temp); - - /* Emit the Huffman-coded symbol for the number of bits */ - code = dctbl->ehufco[nbits]; - size = dctbl->ehufsi[nbits]; - EMIT_BITS(code, size) - - /* Mask off any extra bits in code */ - temp2 &= (((JLONG)1) << nbits) - 1; + nbits = JPEG_NBITS(nbits); + /* Check for out-of-range coefficient values. + * Since we're encoding a difference, the range limit is twice as much. + */ + if (nbits > MAX_COEF_BITS + 1) + ERREXIT(state->cinfo, JERR_BAD_DCT_COEF); - /* Emit that number of bits of the value, if positive, */ - /* or the complement of its magnitude, if negative. */ - EMIT_BITS(temp2, nbits) + /* Emit the Huffman-coded symbol for the number of bits. + * Emit that number of bits of the value, if positive, + * or the complement of its magnitude, if negative. + */ + PUT_CODE(dctbl->ehufco[nbits], dctbl->ehufsi[nbits]) /* Encode the AC coefficients per section F.1.2.2 */ - r = 0; /* r = run length of zeros */ + { + int r = 0; /* r = run length of zeros */ /* Manually unroll the k loop to eliminate the counter variable. This * improves performance greatly on systems with a limited number of @@ -563,51 +613,49 @@ encode_one_block(working_state *state, JCOEFPTR block, int last_dc_val, */ #define kloop(jpeg_natural_order_of_k) { \ if ((temp = block[jpeg_natural_order_of_k]) == 0) { \ - r++; \ + r += 16; \ } else { \ - temp2 = temp; \ /* Branch-less absolute value, bitwise complement, etc., same as above */ \ - temp3 = temp >> (CHAR_BIT * sizeof(int) - 1); \ - temp ^= temp3; \ - temp -= temp3; \ - temp2 += temp3; \ - nbits = JPEG_NBITS_NONZERO(temp); \ + nbits = temp >> (CHAR_BIT * sizeof(int) - 1); \ + temp += nbits; \ + nbits ^= temp; \ + nbits = JPEG_NBITS_NONZERO(nbits); \ + /* Check for out-of-range coefficient values */ \ + if (nbits > MAX_COEF_BITS) \ + ERREXIT(state->cinfo, JERR_BAD_DCT_COEF); \ /* if run length > 15, must emit special run-length-16 codes (0xF0) */ \ - while (r > 15) { \ - EMIT_BITS(code_0xf0, size_0xf0) \ - r -= 16; \ + while (r >= 16 * 16) { \ + r -= 16 * 16; \ + PUT_BITS(actbl->ehufco[0xf0], actbl->ehufsi[0xf0]) \ } \ /* Emit Huffman symbol for run length / number of bits */ \ - temp3 = (r << 4) + nbits; \ - code = actbl->ehufco[temp3]; \ - size = actbl->ehufsi[temp3]; \ - EMIT_CODE(code, size) \ + r += nbits; \ + PUT_CODE(actbl->ehufco[r], actbl->ehufsi[r]) \ r = 0; \ } \ } - /* One iteration for each value in jpeg_natural_order[] */ - kloop(1); kloop(8); kloop(16); kloop(9); kloop(2); kloop(3); - kloop(10); kloop(17); kloop(24); kloop(32); kloop(25); kloop(18); - kloop(11); kloop(4); kloop(5); kloop(12); kloop(19); kloop(26); - kloop(33); kloop(40); kloop(48); kloop(41); kloop(34); kloop(27); - kloop(20); kloop(13); kloop(6); kloop(7); kloop(14); kloop(21); - kloop(28); kloop(35); kloop(42); kloop(49); kloop(56); kloop(57); - kloop(50); kloop(43); kloop(36); kloop(29); kloop(22); kloop(15); - kloop(23); kloop(30); kloop(37); kloop(44); kloop(51); kloop(58); - kloop(59); kloop(52); kloop(45); kloop(38); kloop(31); kloop(39); - kloop(46); kloop(53); kloop(60); kloop(61); kloop(54); kloop(47); - kloop(55); kloop(62); kloop(63); - - /* If the last coef(s) were zero, emit an end-of-block code */ - if (r > 0) { - code = actbl->ehufco[0]; - size = actbl->ehufsi[0]; - EMIT_BITS(code, size) + /* One iteration for each value in jpeg_natural_order[] */ + kloop(1); kloop(8); kloop(16); kloop(9); kloop(2); kloop(3); + kloop(10); kloop(17); kloop(24); kloop(32); kloop(25); kloop(18); + kloop(11); kloop(4); kloop(5); kloop(12); kloop(19); kloop(26); + kloop(33); kloop(40); kloop(48); kloop(41); kloop(34); kloop(27); + kloop(20); kloop(13); kloop(6); kloop(7); kloop(14); kloop(21); + kloop(28); kloop(35); kloop(42); kloop(49); kloop(56); kloop(57); + kloop(50); kloop(43); kloop(36); kloop(29); kloop(22); kloop(15); + kloop(23); kloop(30); kloop(37); kloop(44); kloop(51); kloop(58); + kloop(59); kloop(52); kloop(45); kloop(38); kloop(31); kloop(39); + kloop(46); kloop(53); kloop(60); kloop(61); kloop(54); kloop(47); + kloop(55); kloop(62); kloop(63); + + /* If the last coef(s) were zero, emit an end-of-block code */ + if (r > 0) { + PUT_BITS(actbl->ehufco[0], actbl->ehufsi[0]) + } } - state->cur.put_buffer = put_buffer; - state->cur.put_bits = put_bits; + state->cur.put_buffer.c = put_buffer; + state->cur.free_bits = free_bits; STORE_BUFFER() return TRUE; @@ -654,8 +702,9 @@ encode_mcu_huff(j_compress_ptr cinfo, JBLOCKROW *MCU_data) /* Load up working state */ state.next_output_byte = cinfo->dest->next_output_byte; state.free_in_buffer = cinfo->dest->free_in_buffer; - ASSIGN_STATE(state.cur, entropy->saved); + state.cur = entropy->saved; state.cinfo = cinfo; + state.simd = entropy->simd; /* Emit restart marker if needed */ if (cinfo->restart_interval) { @@ -694,7 +743,7 @@ encode_mcu_huff(j_compress_ptr cinfo, JBLOCKROW *MCU_data) /* Completed MCU, so update state */ cinfo->dest->next_output_byte = state.next_output_byte; cinfo->dest->free_in_buffer = state.free_in_buffer; - ASSIGN_STATE(entropy->saved, state.cur); + entropy->saved = state.cur; /* Update restart-interval state too */ if (cinfo->restart_interval) { @@ -723,8 +772,9 @@ finish_pass_huff(j_compress_ptr cinfo) /* Load up working state ... flush_bits needs it */ state.next_output_byte = cinfo->dest->next_output_byte; state.free_in_buffer = cinfo->dest->free_in_buffer; - ASSIGN_STATE(state.cur, entropy->saved); + state.cur = entropy->saved; state.cinfo = cinfo; + state.simd = entropy->simd; /* Flush out the last data */ if (!flush_bits(&state)) @@ -733,7 +783,7 @@ finish_pass_huff(j_compress_ptr cinfo) /* Update state */ cinfo->dest->next_output_byte = state.next_output_byte; cinfo->dest->free_in_buffer = state.free_in_buffer; - ASSIGN_STATE(entropy->saved, state.cur); + entropy->saved = state.cur; } @@ -900,8 +950,8 @@ jpeg_gen_optimal_table(j_compress_ptr cinfo, JHUFF_TBL *htbl, long freq[]) /* This algorithm is explained in section K.2 of the JPEG standard */ - MEMZERO(bits, sizeof(bits)); - MEMZERO(codesize, sizeof(codesize)); + memset(bits, 0, sizeof(bits)); + memset(codesize, 0, sizeof(codesize)); for (i = 0; i < 257; i++) others[i] = -1; /* init links to empty */ @@ -1003,7 +1053,7 @@ jpeg_gen_optimal_table(j_compress_ptr cinfo, JHUFF_TBL *htbl, long freq[]) bits[i]--; /* Return final symbol counts (only for lengths 0..16) */ - MEMCOPY(htbl->bits, bits, sizeof(htbl->bits)); + memcpy(htbl->bits, bits, sizeof(htbl->bits)); /* Return a list of the symbols sorted by code length */ /* It's not real clear to me why we don't need to consider the codelength @@ -1042,8 +1092,8 @@ finish_pass_gather(j_compress_ptr cinfo) /* It's important not to apply jpeg_gen_optimal_table more than once * per table, because it clobbers the input frequency counts! */ - MEMZERO(did_dc, sizeof(did_dc)); - MEMZERO(did_ac, sizeof(did_ac)); + memset(did_dc, 0, sizeof(did_dc)); + memset(did_ac, 0, sizeof(did_ac)); for (ci = 0; ci < cinfo->comps_in_scan; ci++) { compptr = cinfo->cur_comp_info[ci]; diff --git a/third-party/mozjpeg/mozjpeg/jchuff.h b/third-party/mozjpeg/mozjpeg/jchuff.h index b38c1736386..3a588f88a40 100644 --- a/third-party/mozjpeg/mozjpeg/jchuff.h +++ b/third-party/mozjpeg/mozjpeg/jchuff.h @@ -3,11 +3,12 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2022, D. R. Commander. + * For conditions of distribution and use, see the accompanying README.ijg + * file. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. - * For conditions of distribution and use, see the accompanying README file. * * This file contains declarations for Huffman entropy encoding routines * that are shared between the sequential encoder (jchuff.c) and the @@ -26,6 +27,14 @@ #define MAX_COEF_BITS 14 #endif +/* The progressive Huffman encoder uses an unsigned 16-bit data type to store + * absolute values of coefficients, because it is possible to inject a + * coefficient value of -32768 into the encoder by attempting to transform a + * malformed 12-bit JPEG image, and the absolute value of -32768 would overflow + * a signed 16-bit integer. + */ +typedef unsigned short UJCOEF; + /* Derived data constructed for each Huffman table */ typedef struct { diff --git a/third-party/mozjpeg/mozjpeg/jcinit.c b/third-party/mozjpeg/mozjpeg/jcinit.c index e86be0c8876..4941c71c003 100644 --- a/third-party/mozjpeg/mozjpeg/jcinit.c +++ b/third-party/mozjpeg/mozjpeg/jcinit.c @@ -1,8 +1,10 @@ /* * jcinit.c * + * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. - * This file is part of the Independent JPEG Group's software. + * libjpeg-turbo Modifications: + * Copyright (C) 2020, D. R. Commander. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. * For conditions of distribution and use, see the accompanying README file. @@ -20,6 +22,7 @@ #define JPEG_INTERNALS #include "jinclude.h" #include "jpeglib.h" +#include "jpegcomp.h" /* diff --git a/third-party/mozjpeg/mozjpeg/jcmaster.c b/third-party/mozjpeg/mozjpeg/jcmaster.c index fd60507348f..b1ce15c2f8c 100644 --- a/third-party/mozjpeg/mozjpeg/jcmaster.c +++ b/third-party/mozjpeg/mozjpeg/jcmaster.c @@ -508,7 +508,7 @@ prepare_for_pass (j_compress_ptr cinfo) master->pass_type = output_pass; master->pass_number++; #endif - /*FALLTHROUGH*/ + FALLTHROUGH /*FALLTHROUGH*/ case output_pass: /* Do a data-output pass. */ /* We need not repeat per-scan setup if prior optimization pass did it. */ @@ -603,7 +603,7 @@ copy_buffer (j_compress_ptr cinfo, int scan_idx) while (size >= cinfo->dest->free_in_buffer) { - MEMCOPY(cinfo->dest->next_output_byte, src, cinfo->dest->free_in_buffer); + memcpy(cinfo->dest->next_output_byte, src, cinfo->dest->free_in_buffer); src += cinfo->dest->free_in_buffer; size -= cinfo->dest->free_in_buffer; cinfo->dest->next_output_byte += cinfo->dest->free_in_buffer; @@ -613,7 +613,7 @@ copy_buffer (j_compress_ptr cinfo, int scan_idx) ERREXIT(cinfo, JERR_UNSUPPORTED_SUSPEND); } - MEMCOPY(cinfo->dest->next_output_byte, src, size); + memcpy(cinfo->dest->next_output_byte, src, size); cinfo->dest->next_output_byte += size; cinfo->dest->free_in_buffer -= size; } diff --git a/third-party/mozjpeg/mozjpeg/jconfig.h.in b/third-party/mozjpeg/mozjpeg/jconfig.h.in index 18a69a48142..e0180122fed 100644 --- a/third-party/mozjpeg/mozjpeg/jconfig.h.in +++ b/third-party/mozjpeg/mozjpeg/jconfig.h.in @@ -32,42 +32,6 @@ #define BITS_IN_JSAMPLE @BITS_IN_JSAMPLE@ /* use 8 or 12 */ -/* Define to 1 if you have the header file. */ -#cmakedefine HAVE_LOCALE_H 1 - -/* Define to 1 if you have the header file. */ -#cmakedefine HAVE_STDDEF_H 1 - -/* Define to 1 if you have the header file. */ -#cmakedefine HAVE_STDLIB_H 1 - -/* Define if you need to include to get size_t. */ -#cmakedefine NEED_SYS_TYPES_H 1 - -/* Define if you have BSD-like bzero and bcopy in rather than - memset/memcpy in . */ -#cmakedefine NEED_BSD_STRINGS 1 - -/* Define to 1 if the system has the type `unsigned char'. */ -#cmakedefine HAVE_UNSIGNED_CHAR 1 - -/* Define to 1 if the system has the type `unsigned short'. */ -#cmakedefine HAVE_UNSIGNED_SHORT 1 - -/* Compiler does not support pointers to undefined structures. */ -#cmakedefine INCOMPLETE_TYPES_BROKEN 1 - /* Define if your (broken) compiler shifts signed values as if they were unsigned. */ #cmakedefine RIGHT_SHIFT_IS_UNSIGNED 1 - -/* Define to 1 if type `char' is unsigned and you are not using gcc. */ -#ifndef __CHAR_UNSIGNED__ - #cmakedefine __CHAR_UNSIGNED__ 1 -#endif - -/* Define to empty if `const' does not conform to ANSI C. */ -/* #undef const */ - -/* Define to `unsigned int' if does not define. */ -/* #undef size_t */ diff --git a/third-party/mozjpeg/mozjpeg/jconfig.txt b/third-party/mozjpeg/mozjpeg/jconfig.txt index 3f251adb267..e7a748965ac 100644 --- a/third-party/mozjpeg/mozjpeg/jconfig.txt +++ b/third-party/mozjpeg/mozjpeg/jconfig.txt @@ -26,56 +26,6 @@ * #define the symbol if yes, #undef it if no. */ -/* Does your compiler support the declaration "unsigned char" ? - * How about "unsigned short" ? - */ -#define HAVE_UNSIGNED_CHAR -#define HAVE_UNSIGNED_SHORT - -/* Define "void" as "char" if your compiler doesn't know about type void. - * NOTE: be sure to define void such that "void *" represents the most general - * pointer type, e.g., that returned by malloc(). - */ -/* #define void char */ - -/* Define "const" as empty if your compiler doesn't know the "const" keyword. - */ -/* #define const */ - -/* Define this if an ordinary "char" type is unsigned. - * If you're not sure, leaving it undefined will work at some cost in speed. - * If you defined HAVE_UNSIGNED_CHAR then the speed difference is minimal. - */ -#undef __CHAR_UNSIGNED__ - -/* Define this if your system has an ANSI-conforming file. - */ -#define HAVE_STDDEF_H - -/* Define this if your system has an ANSI-conforming file. - */ -#define HAVE_STDLIB_H - -/* Define this if your system does not have an ANSI/SysV , - * but does have a BSD-style . - */ -#undef NEED_BSD_STRINGS - -/* Define this if your system does not provide typedef size_t in any of the - * ANSI-standard places (stddef.h, stdlib.h, or stdio.h), but places it in - * instead. - */ -#undef NEED_SYS_TYPES_H - -/* Although a real ANSI C compiler can deal perfectly well with pointers to - * unspecified structures (see "incomplete types" in the spec), a few pre-ANSI - * and pseudo-ANSI compilers get confused. To keep one of these bozos happy, - * define INCOMPLETE_TYPES_BROKEN. This is not recommended unless you - * actually get "missing structure definition" warnings or errors while - * compiling the JPEG code. - */ -#undef INCOMPLETE_TYPES_BROKEN - /* Define "boolean" as unsigned char, not int, on Windows systems. */ #ifdef _WIN32 @@ -119,7 +69,6 @@ typedef unsigned char boolean; #define BMP_SUPPORTED /* BMP image file format */ #define GIF_SUPPORTED /* GIF image file format */ #define PPM_SUPPORTED /* PBMPLUS PPM/PGM image file format */ -#undef RLE_SUPPORTED /* Utah RLE image file format */ #define TARGA_SUPPORTED /* Targa image file format */ /* Define this if you want to name both input and output files on the command diff --git a/third-party/mozjpeg/mozjpeg/jconfigint.h.in b/third-party/mozjpeg/mozjpeg/jconfigint.h.in index 55df0536769..d087d7b5537 100644 --- a/third-party/mozjpeg/mozjpeg/jconfigint.h.in +++ b/third-party/mozjpeg/mozjpeg/jconfigint.h.in @@ -7,6 +7,9 @@ /* How to obtain function inlining. */ #define INLINE @INLINE@ +/* How to obtain thread-local storage */ +#define THREAD_LOCAL @THREAD_LOCAL@ + /* Define to the full name of this package. */ #define PACKAGE_NAME "@CMAKE_PROJECT_NAME@" @@ -29,3 +32,13 @@ #define HAVE_BITSCANFORWARD #endif #endif + +#if defined(__has_attribute) +#if __has_attribute(fallthrough) +#define FALLTHROUGH __attribute__((fallthrough)); +#else +#define FALLTHROUGH +#endif +#else +#define FALLTHROUGH +#endif diff --git a/third-party/mozjpeg/mozjpeg/jcparam.c b/third-party/mozjpeg/mozjpeg/jcparam.c index 06942337e43..a432a491f28 100644 --- a/third-party/mozjpeg/mozjpeg/jcparam.c +++ b/third-party/mozjpeg/mozjpeg/jcparam.c @@ -487,9 +487,9 @@ jpeg_set_defaults (j_compress_ptr cinfo) /* Choose JPEG colorspace based on input space, set defaults accordingly */ jpeg_default_colorspace(cinfo); - - cinfo->master->dc_scan_opt_mode = 1; - + + cinfo->master->dc_scan_opt_mode = 0; + #ifdef C_PROGRESSIVE_SUPPORTED if (cinfo->master->compress_profile == JCP_MAX_COMPRESSION) { cinfo->master->optimize_scans = TRUE; diff --git a/third-party/mozjpeg/mozjpeg/jcphuff.c b/third-party/mozjpeg/mozjpeg/jcphuff.c index 4912283dcfa..f80c7963a79 100644 --- a/third-party/mozjpeg/mozjpeg/jcphuff.c +++ b/third-party/mozjpeg/mozjpeg/jcphuff.c @@ -4,8 +4,10 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1995-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2011, 2015, 2018, D. R. Commander. - * Copyright (C) 2016, 2018, Matthieu Darbois. + * Copyright (C) 2011, 2015, 2018, 2021-2022, D. R. Commander. + * Copyright (C) 2016, 2018, 2022, Matthieu Darbois. + * Copyright (C) 2020, Arm Limited. + * Copyright (C) 2021, Alex Richardson. * Copyright (C) 2014, Mozilla Corporation. * For conditions of distribution and use, see the accompanying README.ijg * file. @@ -21,7 +23,6 @@ #include "jinclude.h" #include "jpeglib.h" #include "jsimd.h" -#include "jconfigint.h" #include #ifdef HAVE_INTRIN_H @@ -44,23 +45,28 @@ * memory footprint by 64k, which is important for some mobile applications * that create many isolated instances of libjpeg-turbo (web browsers, for * instance.) This may improve performance on some mobile platforms as well. - * This feature is enabled by default only on ARM processors, because some x86 + * This feature is enabled by default only on Arm processors, because some x86 * chips have a slow implementation of bsr, and the use of clz/bsr cannot be * shown to have a significant performance impact even on the x86 chips that - * have a fast implementation of it. When building for ARMv6, you can + * have a fast implementation of it. When building for Armv6, you can * explicitly disable the use of clz/bsr by adding -mthumb to the compiler * flags (this defines __thumb__). */ /* NOTE: Both GCC and Clang define __GNUC__ */ -#if defined(__GNUC__) && (defined(__arm__) || defined(__aarch64__)) +#if (defined(__GNUC__) && (defined(__arm__) || defined(__aarch64__))) || \ + defined(_M_ARM) || defined(_M_ARM64) #if !defined(__thumb__) || defined(__thumb2__) #define USE_CLZ_INTRINSIC #endif #endif #ifdef USE_CLZ_INTRINSIC +#if defined(_MSC_VER) && !defined(__clang__) +#define JPEG_NBITS_NONZERO(x) (32 - _CountLeadingZeros(x)) +#else #define JPEG_NBITS_NONZERO(x) (32 - __builtin_clz(x)) +#endif #define JPEG_NBITS(x) (x ? JPEG_NBITS_NONZERO(x) : 0) #else #include "jpeg_nbits_table.h" @@ -77,11 +83,11 @@ typedef struct { /* Pointer to routine to prepare data for encode_mcu_AC_first() */ void (*AC_first_prepare) (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits); + int Al, UJCOEF *values, size_t *zerobits); /* Pointer to routine to prepare data for encode_mcu_AC_refine() */ int (*AC_refine_prepare) (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits); + int Al, UJCOEF *absvalues, size_t *bits); /* Mode flag: TRUE for optimization, FALSE for actual data output */ boolean gather_statistics; @@ -136,9 +142,9 @@ typedef phuff_entropy_encoder *phuff_entropy_ptr; #ifdef RIGHT_SHIFT_IS_UNSIGNED #define ISHIFT_TEMPS int ishift_temp; #define IRIGHT_SHIFT(x,shft) \ - ((ishift_temp = (x)) < 0 ? \ + ((ishift_temp = (x)) < 0 ? \ (ishift_temp >> (shft)) | ((~0) << (16-(shft))) : \ - (ishift_temp >> (shft))) + (ishift_temp >> (shft))) #else #define ISHIFT_TEMPS #define IRIGHT_SHIFT(x,shft) ((x) >> (shft)) @@ -148,19 +154,19 @@ typedef phuff_entropy_encoder *phuff_entropy_ptr; /* Forward declarations */ METHODDEF(boolean) encode_mcu_DC_first (j_compress_ptr cinfo, - JBLOCKROW *MCU_data); + JBLOCKROW *MCU_data); METHODDEF(void) encode_mcu_AC_first_prepare (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, - JCOEF *values, size_t *zerobits); -METHODDEF(boolean) encode_mcu_AC_first (j_compress_ptr cinfo, - JBLOCKROW *MCU_data); + UJCOEF *values, size_t *zerobits); +METHODDEF(boolean) encode_mcu_AC_first(j_compress_ptr cinfo, + JBLOCKROW *MCU_data); METHODDEF(boolean) encode_mcu_DC_refine (j_compress_ptr cinfo, - JBLOCKROW *MCU_data); + JBLOCKROW *MCU_data); METHODDEF(int) encode_mcu_AC_refine_prepare (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, - JCOEF *absvalues, size_t *bits); -METHODDEF(boolean) encode_mcu_AC_refine (j_compress_ptr cinfo, - JBLOCKROW *MCU_data); + UJCOEF *absvalues, size_t *bits); +METHODDEF(boolean) encode_mcu_AC_refine(j_compress_ptr cinfo, + JBLOCKROW *MCU_data); METHODDEF(void) finish_pass_phuff (j_compress_ptr cinfo); METHODDEF(void) finish_pass_gather_phuff (j_compress_ptr cinfo); @@ -170,24 +176,26 @@ INLINE METHODDEF(int) count_zeroes(size_t *x) { - int result; #if defined(HAVE_BUILTIN_CTZL) + int result; result = __builtin_ctzl(*x); *x >>= result; #elif defined(HAVE_BITSCANFORWARD64) + unsigned long result; _BitScanForward64(&result, *x); *x >>= result; #elif defined(HAVE_BITSCANFORWARD) + unsigned long result; _BitScanForward(&result, *x); *x >>= result; #else - result = 0; + int result = 0; while ((*x & 1) == 0) { ++result; *x >>= 1; } #endif - return result; + return (int)result; } @@ -267,7 +275,8 @@ start_pass_phuff (j_compress_ptr cinfo, boolean gather_statistics) entropy->count_ptrs[tbl] = (long *) (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_IMAGE, 257 * sizeof(long)); - MEMZERO(entropy->count_ptrs[tbl], 257 * sizeof(long)); + memset(entropy->count_ptrs[tbl], 0, 257 * sizeof(long)); + if (cinfo->master->trellis_passes) { /* When generating tables for trellis passes, make sure that all */ /* codewords have an assigned length */ @@ -306,7 +315,7 @@ start_pass_phuff (j_compress_ptr cinfo, boolean gather_statistics) /* Emit a byte */ #define emit_byte(entropy, val) { \ *(entropy)->next_output_byte++ = (JOCTET)(val); \ - if (--(entropy)->free_in_buffer == 0) \ + if (--(entropy)->free_in_buffer == 0) \ dump_buffer(entropy); \ } @@ -403,7 +412,7 @@ emit_symbol (phuff_entropy_ptr entropy, int tbl_no, int symbol) LOCAL(void) emit_buffered_bits (phuff_entropy_ptr entropy, char *bufstart, - unsigned int nbits) + unsigned int nbits) { if (entropy->gather_statistics) return; /* no real work */ @@ -524,7 +533,7 @@ encode_mcu_DC_first (j_compress_ptr cinfo, JBLOCKROW *MCU_data) temp3 = temp >> (CHAR_BIT * sizeof(int) - 1); temp ^= temp3; temp -= temp3; /* temp is abs value of input */ - /* For a negative input, want temp2 = bitwise complement of abs(input) */ + /* For a negative input, want temp2 = bitwise complement of abs(input) */ temp2 = temp ^ temp3; /* Find the number of bits needed for the magnitude of the coefficient */ @@ -584,8 +593,8 @@ encode_mcu_DC_first (j_compress_ptr cinfo, JBLOCKROW *MCU_data) continue; \ /* For a negative coef, want temp2 = bitwise complement of abs(coef) */ \ temp2 ^= temp; \ - values[k] = temp; \ - values[k + DCTSIZE2] = temp2; \ + values[k] = (UJCOEF)temp; \ + values[k + DCTSIZE2] = (UJCOEF)temp2; \ zerobits |= ((size_t)1U) << k; \ } \ } @@ -593,7 +602,7 @@ encode_mcu_DC_first (j_compress_ptr cinfo, JBLOCKROW *MCU_data) METHODDEF(void) encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *bits) + int Al, UJCOEF *values, size_t *bits) { register int k, temp, temp2; size_t zerobits = 0U; @@ -666,9 +675,9 @@ encode_mcu_AC_first (j_compress_ptr cinfo, JBLOCKROW *MCU_data) register int nbits, r; int Sl = cinfo->Se - cinfo->Ss + 1; int Al = cinfo->Al; - JCOEF values_unaligned[2 * DCTSIZE2 + 15]; - JCOEF *values; - const JCOEF *cvalue; + UJCOEF values_unaligned[2 * DCTSIZE2 + 15]; + UJCOEF *values; + const UJCOEF *cvalue; size_t zerobits; size_t bits[8 / SIZEOF_SIZE_T]; @@ -681,7 +690,7 @@ encode_mcu_AC_first (j_compress_ptr cinfo, JBLOCKROW *MCU_data) emit_restart(entropy, entropy->next_restart_num); #ifdef WITH_SIMD - cvalue = values = (JCOEF *)PAD((size_t)values_unaligned, 16); + cvalue = values = (UJCOEF *)PAD((JUINTPTR)values_unaligned, 16); #else /* Not using SIMD, so alignment is not needed */ cvalue = values = values_unaligned; @@ -696,9 +705,9 @@ encode_mcu_AC_first (j_compress_ptr cinfo, JBLOCKROW *MCU_data) zerobits |= bits[1]; #endif - /* Emit any pending EOBRUN */ + /* Emit any pending EOBRUN */ if (zerobits && (entropy->EOBRUN > 0)) - emit_eobrun(entropy); + emit_eobrun(entropy); #if SIZEOF_SIZE_T == 4 zerobits = bits[0]; @@ -815,7 +824,7 @@ encode_mcu_DC_refine (j_compress_ptr cinfo, JBLOCKROW *MCU_data) zerobits |= ((size_t)1U) << k; \ signbits |= ((size_t)(temp2 + 1)) << k; \ } \ - absvalues[k] = (JCOEF)temp; /* save abs value for main pass */ \ + absvalues[k] = (UJCOEF)temp; /* save abs value for main pass */ \ if (temp == 1) \ EOB = k + koffset; /* EOB = index of last newly-nonzero coef */ \ } \ @@ -824,7 +833,7 @@ encode_mcu_DC_refine (j_compress_ptr cinfo, JBLOCKROW *MCU_data) METHODDEF(int) encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { register int k, temp, temp2; int EOB = 0; @@ -869,7 +878,7 @@ encode_mcu_AC_refine_prepare(const JCOEF *block, #define ENCODE_COEFS_AC_REFINE(label) { \ while (zerobits) { \ - int idx = count_zeroes(&zerobits); \ + idx = count_zeroes(&zerobits); \ r += idx; \ cabsvalue += idx; \ signbits >>= idx; \ @@ -926,14 +935,14 @@ METHODDEF(boolean) encode_mcu_AC_refine (j_compress_ptr cinfo, JBLOCKROW *MCU_data) { phuff_entropy_ptr entropy = (phuff_entropy_ptr) cinfo->entropy; - register int temp, r; + register int temp, r, idx; char *BR_buffer; unsigned int BR; int Sl = cinfo->Se - cinfo->Ss + 1; int Al = cinfo->Al; - JCOEF absvalues_unaligned[DCTSIZE2 + 15]; - JCOEF *absvalues; - const JCOEF *cabsvalue, *EOBPTR; + UJCOEF absvalues_unaligned[DCTSIZE2 + 15]; + UJCOEF *absvalues; + const UJCOEF *cabsvalue, *EOBPTR; size_t zerobits, signbits; size_t bits[16 / SIZEOF_SIZE_T]; @@ -946,7 +955,7 @@ encode_mcu_AC_refine (j_compress_ptr cinfo, JBLOCKROW *MCU_data) emit_restart(entropy, entropy->next_restart_num); #ifdef WITH_SIMD - cabsvalue = absvalues = (JCOEF *)PAD((size_t)absvalues_unaligned, 16); + cabsvalue = absvalues = (UJCOEF *)PAD((JUINTPTR)absvalues_unaligned, 16); #else /* Not using SIMD, so alignment is not needed */ cabsvalue = absvalues = absvalues_unaligned; @@ -977,13 +986,13 @@ encode_mcu_AC_refine (j_compress_ptr cinfo, JBLOCKROW *MCU_data) if (zerobits) { int diff = ((absvalues + DCTSIZE2 / 2) - cabsvalue); - int idx = count_zeroes(&zerobits); + idx = count_zeroes(&zerobits); signbits >>= idx; idx += diff; r += idx; cabsvalue += idx; goto first_iter_ac_refine; - } + } ENCODE_COEFS_AC_REFINE(first_iter_ac_refine:); #endif @@ -1062,7 +1071,7 @@ finish_pass_gather_phuff (j_compress_ptr cinfo) /* It's important not to apply jpeg_gen_optimal_table more than once * per table, because it clobbers the input frequency counts! */ - MEMZERO(did, sizeof(did)); + memset(did, 0, sizeof(did)); for (ci = 0; ci < cinfo->comps_in_scan; ci++) { compptr = cinfo->cur_comp_info[ci]; diff --git a/third-party/mozjpeg/mozjpeg/jcprepct.c b/third-party/mozjpeg/mozjpeg/jcprepct.c index d59713ae68e..f27cc345079 100644 --- a/third-party/mozjpeg/mozjpeg/jcprepct.c +++ b/third-party/mozjpeg/mozjpeg/jcprepct.c @@ -3,8 +3,8 @@ * * This file is part of the Independent JPEG Group's software: * Copyright (C) 1994-1996, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -289,8 +289,8 @@ create_context_buffer(j_compress_ptr cinfo) cinfo->max_h_samp_factor) / compptr->h_samp_factor), (JDIMENSION)(3 * rgroup_height)); /* Copy true buffer row pointers into the middle of the fake row array */ - MEMCOPY(fake_buffer + rgroup_height, true_buffer, - 3 * rgroup_height * sizeof(JSAMPROW)); + memcpy(fake_buffer + rgroup_height, true_buffer, + 3 * rgroup_height * sizeof(JSAMPROW)); /* Fill in the above and below wraparound pointers */ for (i = 0; i < rgroup_height; i++) { fake_buffer[i] = true_buffer[2 * rgroup_height + i]; diff --git a/third-party/mozjpeg/mozjpeg/jcsample.c b/third-party/mozjpeg/mozjpeg/jcsample.c index bd27b84e068..e8515ebf0fc 100644 --- a/third-party/mozjpeg/mozjpeg/jcsample.c +++ b/third-party/mozjpeg/mozjpeg/jcsample.c @@ -6,7 +6,7 @@ * libjpeg-turbo Modifications: * Copyright 2009 Pierre Ossman for Cendio AB * Copyright (C) 2014, MIPS Technologies, Inc., California. - * Copyright (C) 2015, D. R. Commander. + * Copyright (C) 2015, 2019, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -103,7 +103,7 @@ expand_right_edge(JSAMPARRAY image_data, int num_rows, JDIMENSION input_cols, if (numcols > 0) { for (row = 0; row < num_rows; row++) { ptr = image_data[row] + input_cols; - pixval = ptr[-1]; /* don't need GETJSAMPLE() here */ + pixval = ptr[-1]; for (count = numcols; count > 0; count--) *ptr++ = pixval; } @@ -174,7 +174,7 @@ int_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, for (v = 0; v < v_expand; v++) { inptr = input_data[inrow + v] + outcol_h; for (h = 0; h < h_expand; h++) { - outvalue += (JLONG)GETJSAMPLE(*inptr++); + outvalue += (JLONG)(*inptr++); } } *outptr++ = (JSAMPLE)((outvalue + numpix2) / numpix); @@ -237,8 +237,7 @@ h2v1_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, inptr = input_data[outrow]; bias = 0; /* bias = 0,1,0,1,... for successive samples */ for (outcol = 0; outcol < output_cols; outcol++) { - *outptr++ = - (JSAMPLE)((GETJSAMPLE(*inptr) + GETJSAMPLE(inptr[1]) + bias) >> 1); + *outptr++ = (JSAMPLE)((inptr[0] + inptr[1] + bias) >> 1); bias ^= 1; /* 0=>1, 1=>0 */ inptr += 2; } @@ -277,8 +276,7 @@ h2v2_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, bias = 1; /* bias = 1,2,1,2,... for successive samples */ for (outcol = 0; outcol < output_cols; outcol++) { *outptr++ = - (JSAMPLE)((GETJSAMPLE(*inptr0) + GETJSAMPLE(inptr0[1]) + - GETJSAMPLE(*inptr1) + GETJSAMPLE(inptr1[1]) + bias) >> 2); + (JSAMPLE)((inptr0[0] + inptr0[1] + inptr1[0] + inptr1[1] + bias) >> 2); bias ^= 3; /* 1=>2, 2=>1 */ inptr0 += 2; inptr1 += 2; } @@ -337,33 +335,25 @@ h2v2_smooth_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, below_ptr = input_data[inrow + 2]; /* Special case for first column: pretend column -1 is same as column 0 */ - membersum = GETJSAMPLE(*inptr0) + GETJSAMPLE(inptr0[1]) + - GETJSAMPLE(*inptr1) + GETJSAMPLE(inptr1[1]); - neighsum = GETJSAMPLE(*above_ptr) + GETJSAMPLE(above_ptr[1]) + - GETJSAMPLE(*below_ptr) + GETJSAMPLE(below_ptr[1]) + - GETJSAMPLE(*inptr0) + GETJSAMPLE(inptr0[2]) + - GETJSAMPLE(*inptr1) + GETJSAMPLE(inptr1[2]); + membersum = inptr0[0] + inptr0[1] + inptr1[0] + inptr1[1]; + neighsum = above_ptr[0] + above_ptr[1] + below_ptr[0] + below_ptr[1] + + inptr0[0] + inptr0[2] + inptr1[0] + inptr1[2]; neighsum += neighsum; - neighsum += GETJSAMPLE(*above_ptr) + GETJSAMPLE(above_ptr[2]) + - GETJSAMPLE(*below_ptr) + GETJSAMPLE(below_ptr[2]); + neighsum += above_ptr[0] + above_ptr[2] + below_ptr[0] + below_ptr[2]; membersum = membersum * memberscale + neighsum * neighscale; *outptr++ = (JSAMPLE)((membersum + 32768) >> 16); inptr0 += 2; inptr1 += 2; above_ptr += 2; below_ptr += 2; for (colctr = output_cols - 2; colctr > 0; colctr--) { /* sum of pixels directly mapped to this output element */ - membersum = GETJSAMPLE(*inptr0) + GETJSAMPLE(inptr0[1]) + - GETJSAMPLE(*inptr1) + GETJSAMPLE(inptr1[1]); + membersum = inptr0[0] + inptr0[1] + inptr1[0] + inptr1[1]; /* sum of edge-neighbor pixels */ - neighsum = GETJSAMPLE(*above_ptr) + GETJSAMPLE(above_ptr[1]) + - GETJSAMPLE(*below_ptr) + GETJSAMPLE(below_ptr[1]) + - GETJSAMPLE(inptr0[-1]) + GETJSAMPLE(inptr0[2]) + - GETJSAMPLE(inptr1[-1]) + GETJSAMPLE(inptr1[2]); + neighsum = above_ptr[0] + above_ptr[1] + below_ptr[0] + below_ptr[1] + + inptr0[-1] + inptr0[2] + inptr1[-1] + inptr1[2]; /* The edge-neighbors count twice as much as corner-neighbors */ neighsum += neighsum; /* Add in the corner-neighbors */ - neighsum += GETJSAMPLE(above_ptr[-1]) + GETJSAMPLE(above_ptr[2]) + - GETJSAMPLE(below_ptr[-1]) + GETJSAMPLE(below_ptr[2]); + neighsum += above_ptr[-1] + above_ptr[2] + below_ptr[-1] + below_ptr[2]; /* form final output scaled up by 2^16 */ membersum = membersum * memberscale + neighsum * neighscale; /* round, descale and output it */ @@ -372,15 +362,11 @@ h2v2_smooth_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, } /* Special case for last column */ - membersum = GETJSAMPLE(*inptr0) + GETJSAMPLE(inptr0[1]) + - GETJSAMPLE(*inptr1) + GETJSAMPLE(inptr1[1]); - neighsum = GETJSAMPLE(*above_ptr) + GETJSAMPLE(above_ptr[1]) + - GETJSAMPLE(*below_ptr) + GETJSAMPLE(below_ptr[1]) + - GETJSAMPLE(inptr0[-1]) + GETJSAMPLE(inptr0[1]) + - GETJSAMPLE(inptr1[-1]) + GETJSAMPLE(inptr1[1]); + membersum = inptr0[0] + inptr0[1] + inptr1[0] + inptr1[1]; + neighsum = above_ptr[0] + above_ptr[1] + below_ptr[0] + below_ptr[1] + + inptr0[-1] + inptr0[1] + inptr1[-1] + inptr1[1]; neighsum += neighsum; - neighsum += GETJSAMPLE(above_ptr[-1]) + GETJSAMPLE(above_ptr[1]) + - GETJSAMPLE(below_ptr[-1]) + GETJSAMPLE(below_ptr[1]); + neighsum += above_ptr[-1] + above_ptr[1] + below_ptr[-1] + below_ptr[1]; membersum = membersum * memberscale + neighsum * neighscale; *outptr = (JSAMPLE)((membersum + 32768) >> 16); @@ -429,21 +415,18 @@ fullsize_smooth_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, below_ptr = input_data[outrow + 1]; /* Special case for first column */ - colsum = GETJSAMPLE(*above_ptr++) + GETJSAMPLE(*below_ptr++) + - GETJSAMPLE(*inptr); - membersum = GETJSAMPLE(*inptr++); - nextcolsum = GETJSAMPLE(*above_ptr) + GETJSAMPLE(*below_ptr) + - GETJSAMPLE(*inptr); + colsum = (*above_ptr++) + (*below_ptr++) + inptr[0]; + membersum = *inptr++; + nextcolsum = above_ptr[0] + below_ptr[0] + inptr[0]; neighsum = colsum + (colsum - membersum) + nextcolsum; membersum = membersum * memberscale + neighsum * neighscale; *outptr++ = (JSAMPLE)((membersum + 32768) >> 16); lastcolsum = colsum; colsum = nextcolsum; for (colctr = output_cols - 2; colctr > 0; colctr--) { - membersum = GETJSAMPLE(*inptr++); + membersum = *inptr++; above_ptr++; below_ptr++; - nextcolsum = GETJSAMPLE(*above_ptr) + GETJSAMPLE(*below_ptr) + - GETJSAMPLE(*inptr); + nextcolsum = above_ptr[0] + below_ptr[0] + inptr[0]; neighsum = lastcolsum + (colsum - membersum) + nextcolsum; membersum = membersum * memberscale + neighsum * neighscale; *outptr++ = (JSAMPLE)((membersum + 32768) >> 16); @@ -451,7 +434,7 @@ fullsize_smooth_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, } /* Special case for last column */ - membersum = GETJSAMPLE(*inptr); + membersum = *inptr; neighsum = lastcolsum + (colsum - membersum) + colsum; membersum = membersum * memberscale + neighsum * neighscale; *outptr = (JSAMPLE)((membersum + 32768) >> 16); diff --git a/third-party/mozjpeg/mozjpeg/jctrans.c b/third-party/mozjpeg/mozjpeg/jctrans.c index bc0cd470ffa..a6bd1196048 100644 --- a/third-party/mozjpeg/mozjpeg/jctrans.c +++ b/third-party/mozjpeg/mozjpeg/jctrans.c @@ -4,8 +4,8 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1995-1998, Thomas G. Lane. * Modified 2000-2009 by Guido Vollbeding. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2020, 2022, D. R. Commander. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. * For conditions of distribution and use, see the accompanying README file. @@ -18,6 +18,7 @@ #define JPEG_INTERNALS #include "jinclude.h" #include "jpeglib.h" +#include "jpegcomp.h" /* Forward declarations */ @@ -106,7 +107,7 @@ jpeg_copy_critical_parameters (const j_decompress_ptr srcinfo, j_compress_ptr ds qtblptr = & dstinfo->quant_tbl_ptrs[tblno]; if (*qtblptr == NULL) *qtblptr = jpeg_alloc_quant_table((j_common_ptr) dstinfo); - MEMCOPY((*qtblptr)->quantval, srcinfo->quant_tbl_ptrs[tblno]->quantval, + memcpy((*qtblptr)->quantval, srcinfo->quant_tbl_ptrs[tblno]->quantval, sizeof((*qtblptr)->quantval)); (*qtblptr)->sent_table = FALSE; } @@ -171,7 +172,7 @@ jpeg_copy_critical_parameters (const j_decompress_ptr srcinfo, j_compress_ptr ds LOCAL(void) transencode_master_selection (j_compress_ptr cinfo, - jvirt_barray_ptr *coef_arrays) + jvirt_barray_ptr *coef_arrays) { /* Although we don't actually use input_components for transcoding, * jcmaster.c's initial_setup will complain if input_components is 0. @@ -380,7 +381,7 @@ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf) LOCAL(void) transencode_coef_controller (j_compress_ptr cinfo, - jvirt_barray_ptr *coef_arrays) + jvirt_barray_ptr *coef_arrays) { my_coef_ptr coef; JBLOCKROW buffer; diff --git a/third-party/mozjpeg/mozjpeg/jdapimin.c b/third-party/mozjpeg/mozjpeg/jdapimin.c index 805b1e2d6c3..b7f91f25157 100644 --- a/third-party/mozjpeg/mozjpeg/jdapimin.c +++ b/third-party/mozjpeg/mozjpeg/jdapimin.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1994-1998, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2016, D. R. Commander. + * Copyright (C) 2016, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -52,7 +52,7 @@ jpeg_CreateDecompress (j_decompress_ptr cinfo, int version, size_t structsize) { struct jpeg_error_mgr * err = cinfo->err; void * client_data = cinfo->client_data; /* ignore Purify complaint here */ - MEMZERO(cinfo, sizeof(struct jpeg_decompress_struct)); + memset(cinfo, 0, sizeof(struct jpeg_decompress_struct)); cinfo->err = err; cinfo->client_data = client_data; } @@ -90,8 +90,8 @@ jpeg_CreateDecompress (j_decompress_ptr cinfo, int version, size_t structsize) */ cinfo->master = (struct jpeg_decomp_master *) (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT, - sizeof(my_decomp_master)); - MEMZERO(cinfo->master, sizeof(my_decomp_master)); + sizeof(my_decomp_master)); + memset(cinfo->master, 0, sizeof(my_decomp_master)); } @@ -308,7 +308,7 @@ jpeg_consume_input (j_decompress_ptr cinfo) /* Initialize application's data source module */ (*cinfo->src->init_source) (cinfo); cinfo->global_state = DSTATE_INHEADER; - /*FALLTHROUGH*/ + FALLTHROUGH /*FALLTHROUGH*/ case DSTATE_INHEADER: retcode = (*cinfo->inputctl->consume_input) (cinfo); if (retcode == JPEG_REACHED_SOS) { /* Found SOS, prepare to decompress */ diff --git a/third-party/mozjpeg/mozjpeg/jdapistd.c b/third-party/mozjpeg/mozjpeg/jdapistd.c index 2c808fa5640..96cded11290 100644 --- a/third-party/mozjpeg/mozjpeg/jdapistd.c +++ b/third-party/mozjpeg/mozjpeg/jdapistd.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1994-1996, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2010, 2015-2018, D. R. Commander. + * Copyright (C) 2010, 2015-2020, 2022-2023, D. R. Commander. * Copyright (C) 2015, Google, Inc. * For conditions of distribution and use, see the accompanying README.ijg * file. @@ -21,6 +21,8 @@ #include "jinclude.h" #include "jdmainct.h" #include "jdcoefct.h" +#include "jdmaster.h" +#include "jdmerge.h" #include "jdsample.h" #include "jmemsys.h" @@ -157,8 +159,12 @@ jpeg_crop_scanline(j_decompress_ptr cinfo, JDIMENSION *xoffset, JDIMENSION input_xoffset; boolean reinit_upsampler = FALSE; jpeg_component_info *compptr; +#ifdef UPSAMPLE_MERGING_SUPPORTED + my_master_ptr master = (my_master_ptr)cinfo->master; +#endif - if (cinfo->global_state != DSTATE_SCANNING || cinfo->output_scanline != 0) + if ((cinfo->global_state != DSTATE_SCANNING && + cinfo->global_state != DSTATE_BUFIMAGE) || cinfo->output_scanline != 0) ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state); if (!xoffset || !width) @@ -206,6 +212,13 @@ jpeg_crop_scanline(j_decompress_ptr cinfo, JDIMENSION *xoffset, */ *width = *width + input_xoffset - *xoffset; cinfo->output_width = *width; +#ifdef UPSAMPLE_MERGING_SUPPORTED + if (master->using_merged_upsample && cinfo->max_v_samp_factor == 2) { + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; + upsample->out_row_width = + cinfo->output_width * cinfo->out_color_components; + } +#endif /* Set the first and last iMCU columns that we must decompress. These values * will be used in single-scan decompressions. @@ -223,9 +236,11 @@ jpeg_crop_scanline(j_decompress_ptr cinfo, JDIMENSION *xoffset, /* Set downsampled_width to the new output width. */ orig_downsampled_width = compptr->downsampled_width; compptr->downsampled_width = - (JDIMENSION)jdiv_round_up((long)(cinfo->output_width * - compptr->h_samp_factor), - (long)cinfo->max_h_samp_factor); + (JDIMENSION)jdiv_round_up((long)cinfo->output_width * + (long)(compptr->h_samp_factor * + compptr->_DCT_scaled_size), + (long)(cinfo->max_h_samp_factor * + cinfo->_min_DCT_scaled_size)); if (compptr->downsampled_width < 2 && orig_downsampled_width >= 2) reinit_upsampler = TRUE; @@ -316,6 +331,12 @@ LOCAL(void) read_and_discard_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) { JDIMENSION n; +#ifdef UPSAMPLE_MERGING_SUPPORTED + my_master_ptr master = (my_master_ptr)cinfo->master; +#endif + JSAMPLE dummy_sample[1] = { 0 }; + JSAMPROW dummy_row = dummy_sample; + JSAMPARRAY scanlines = NULL; void (*color_convert) (j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION input_row, JSAMPARRAY output_buf, int num_rows) = NULL; @@ -325,6 +346,10 @@ read_and_discard_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) if (cinfo->cconvert && cinfo->cconvert->color_convert) { color_convert = cinfo->cconvert->color_convert; cinfo->cconvert->color_convert = noop_convert; + /* This just prevents UBSan from complaining about adding 0 to a NULL + * pointer. The pointer isn't actually used. + */ + scanlines = &dummy_row; } if (cinfo->cquantize && cinfo->cquantize->color_quantize) { @@ -332,8 +357,15 @@ read_and_discard_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) cinfo->cquantize->color_quantize = noop_quantize; } +#ifdef UPSAMPLE_MERGING_SUPPORTED + if (master->using_merged_upsample && cinfo->max_v_samp_factor == 2) { + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; + scanlines = &upsample->spare_row; + } +#endif + for (n = 0; n < num_lines; n++) - jpeg_read_scanlines(cinfo, NULL, 1); + jpeg_read_scanlines(cinfo, scanlines, 1); if (color_convert) cinfo->cconvert->color_convert = color_convert; @@ -353,6 +385,12 @@ increment_simple_rowgroup_ctr(j_decompress_ptr cinfo, JDIMENSION rows) { JDIMENSION rows_left; my_main_ptr main_ptr = (my_main_ptr)cinfo->main; + my_master_ptr master = (my_master_ptr)cinfo->master; + + if (master->using_merged_upsample && cinfo->max_v_samp_factor == 2) { + read_and_discard_scanlines(cinfo, rows); + return; + } /* Increment the counter to the next row group after the skipped rows. */ main_ptr->rowgroup_ctr += rows / cinfo->max_v_samp_factor; @@ -382,21 +420,27 @@ jpeg_skip_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) { my_main_ptr main_ptr = (my_main_ptr)cinfo->main; my_coef_ptr coef = (my_coef_ptr)cinfo->coef; + my_master_ptr master = (my_master_ptr)cinfo->master; my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; JDIMENSION i, x; int y; JDIMENSION lines_per_iMCU_row, lines_left_in_iMCU_row, lines_after_iMCU_row; JDIMENSION lines_to_skip, lines_to_read; + /* Two-pass color quantization is not supported. */ + if (cinfo->quantize_colors && cinfo->two_pass_quantize) + ERREXIT(cinfo, JERR_NOTIMPL); + if (cinfo->global_state != DSTATE_SCANNING) ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state); /* Do not skip past the bottom of the image. */ if (cinfo->output_scanline + num_lines >= cinfo->output_height) { + num_lines = cinfo->output_height - cinfo->output_scanline; cinfo->output_scanline = cinfo->output_height; (*cinfo->inputctl->finish_input_pass) (cinfo); cinfo->inputctl->eoi_reached = TRUE; - return cinfo->output_height - cinfo->output_scanline; + return num_lines; } if (num_lines == 0) @@ -445,8 +489,10 @@ jpeg_skip_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) main_ptr->buffer_full = FALSE; main_ptr->rowgroup_ctr = 0; main_ptr->context_state = CTX_PREPARE_FOR_IMCU; - upsample->next_row_out = cinfo->max_v_samp_factor; - upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; + if (!master->using_merged_upsample) { + upsample->next_row_out = cinfo->max_v_samp_factor; + upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; + } } /* Skipping is much simpler when context rows are not required. */ @@ -458,8 +504,10 @@ jpeg_skip_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) cinfo->output_scanline += lines_left_in_iMCU_row; main_ptr->buffer_full = FALSE; main_ptr->rowgroup_ctr = 0; - upsample->next_row_out = cinfo->max_v_samp_factor; - upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; + if (!master->using_merged_upsample) { + upsample->next_row_out = cinfo->max_v_samp_factor; + upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; + } } } @@ -480,7 +528,7 @@ jpeg_skip_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) * all of the entropy decoding occurs in jpeg_start_decompress(), assuming * that the input data source is non-suspending. This makes skipping easy. */ - if (cinfo->inputctl->has_multiple_scans) { + if (cinfo->inputctl->has_multiple_scans || cinfo->buffered_image) { if (cinfo->upsample->need_context_rows) { cinfo->output_scanline += lines_to_skip; cinfo->output_iMCU_row += lines_to_skip / lines_per_iMCU_row; @@ -494,7 +542,8 @@ jpeg_skip_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) cinfo->output_iMCU_row += lines_to_skip / lines_per_iMCU_row; increment_simple_rowgroup_ctr(cinfo, lines_to_read); } - upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; + if (!master->using_merged_upsample) + upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; return num_lines; } @@ -506,6 +555,8 @@ jpeg_skip_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) * decoded coefficients. This is ~5% faster for large subsets, but * it's tough to tell a difference for smaller images. */ + if (!cinfo->entropy->insufficient_data) + cinfo->master->last_good_iMCU_row = cinfo->input_iMCU_row; (*cinfo->entropy->decode_mcu) (cinfo, NULL); } } @@ -535,7 +586,8 @@ jpeg_skip_scanlines(j_decompress_ptr cinfo, JDIMENSION num_lines) * bit odd, since "rows_to_go" seems to be redundantly keeping track of * output_scanline. */ - upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; + if (!master->using_merged_upsample) + upsample->rows_to_go = cinfo->output_height - cinfo->output_scanline; /* Always skip the requested number of lines. */ return num_lines; diff --git a/third-party/mozjpeg/mozjpeg/jdarith.c b/third-party/mozjpeg/mozjpeg/jdarith.c index 6002481e242..21575e80c72 100644 --- a/third-party/mozjpeg/mozjpeg/jdarith.c +++ b/third-party/mozjpeg/mozjpeg/jdarith.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Developed 1997-2015 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2015-2018, D. R. Commander. + * Copyright (C) 2015-2020, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -80,7 +80,7 @@ get_byte(j_decompress_ptr cinfo) if (!(*src->fill_input_buffer) (cinfo)) ERREXIT(cinfo, JERR_CANT_SUSPEND); src->bytes_in_buffer--; - return GETJOCTET(*src->next_input_byte++); + return *src->next_input_byte++; } @@ -210,13 +210,13 @@ process_restart(j_decompress_ptr cinfo) for (ci = 0; ci < cinfo->comps_in_scan; ci++) { compptr = cinfo->cur_comp_info[ci]; if (!cinfo->progressive_mode || (cinfo->Ss == 0 && cinfo->Ah == 0)) { - MEMZERO(entropy->dc_stats[compptr->dc_tbl_no], DC_STAT_BINS); + memset(entropy->dc_stats[compptr->dc_tbl_no], 0, DC_STAT_BINS); /* Reset DC predictions to 0 */ entropy->last_dc_val[ci] = 0; entropy->dc_context[ci] = 0; } if (!cinfo->progressive_mode || cinfo->Ss) { - MEMZERO(entropy->ac_stats[compptr->ac_tbl_no], AC_STAT_BINS); + memset(entropy->ac_stats[compptr->ac_tbl_no], 0, AC_STAT_BINS); } } @@ -471,17 +471,17 @@ decode_mcu_AC_refine(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) if (*thiscoef) { /* previously nonzero coef */ if (arith_decode(cinfo, st + 2)) { if (*thiscoef < 0) - *thiscoef += m1; + *thiscoef += (JCOEF)m1; else - *thiscoef += p1; + *thiscoef += (JCOEF)p1; } break; } if (arith_decode(cinfo, st + 1)) { /* newly nonzero coef */ if (arith_decode(cinfo, entropy->fixed_bin)) - *thiscoef = m1; + *thiscoef = (JCOEF)m1; else - *thiscoef = p1; + *thiscoef = (JCOEF)p1; break; } st += 3; k++; @@ -665,8 +665,16 @@ start_pass(j_decompress_ptr cinfo) for (ci = 0; ci < cinfo->comps_in_scan; ci++) { int coefi, cindex = cinfo->cur_comp_info[ci]->component_index; int *coef_bit_ptr = &cinfo->coef_bits[cindex][0]; + int *prev_coef_bit_ptr = + &cinfo->coef_bits[cindex + cinfo->num_components][0]; if (cinfo->Ss && coef_bit_ptr[0] < 0) /* AC without prior DC scan */ WARNMS2(cinfo, JWRN_BOGUS_PROGRESSION, cindex, 0); + for (coefi = MIN(cinfo->Ss, 1); coefi <= MAX(cinfo->Se, 9); coefi++) { + if (cinfo->input_scan_number > 1) + prev_coef_bit_ptr[coefi] = coef_bit_ptr[coefi]; + else + prev_coef_bit_ptr[coefi] = 0; + } for (coefi = cinfo->Ss; coefi <= cinfo->Se; coefi++) { int expected = (coef_bit_ptr[coefi] < 0) ? 0 : coef_bit_ptr[coefi]; if (cinfo->Ah != expected) @@ -690,8 +698,8 @@ start_pass(j_decompress_ptr cinfo) /* Check that the scan parameters Ss, Se, Ah/Al are OK for sequential JPEG. * This ought to be an error condition, but we make it a warning. */ - if (cinfo->Ss != 0 || cinfo->Ah != 0 || cinfo->Al != 0 || - (cinfo->Se < DCTSIZE2 && cinfo->Se != DCTSIZE2 - 1)) + if (cinfo->Ss != 0 || cinfo->Se != DCTSIZE2 - 1 || + cinfo->Ah != 0 || cinfo->Al != 0) WARNMS(cinfo, JWRN_NOT_SEQUENTIAL); /* Select MCU decoding routine */ entropy->pub.decode_mcu = decode_mcu; @@ -707,7 +715,7 @@ start_pass(j_decompress_ptr cinfo) if (entropy->dc_stats[tbl] == NULL) entropy->dc_stats[tbl] = (unsigned char *)(*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, DC_STAT_BINS); - MEMZERO(entropy->dc_stats[tbl], DC_STAT_BINS); + memset(entropy->dc_stats[tbl], 0, DC_STAT_BINS); /* Initialize DC predictions to 0 */ entropy->last_dc_val[ci] = 0; entropy->dc_context[ci] = 0; @@ -719,7 +727,7 @@ start_pass(j_decompress_ptr cinfo) if (entropy->ac_stats[tbl] == NULL) entropy->ac_stats[tbl] = (unsigned char *)(*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, AC_STAT_BINS); - MEMZERO(entropy->ac_stats[tbl], AC_STAT_BINS); + memset(entropy->ac_stats[tbl], 0, AC_STAT_BINS); } } @@ -727,6 +735,7 @@ start_pass(j_decompress_ptr cinfo) entropy->c = 0; entropy->a = 0; entropy->ct = -16; /* force reading 2 initial bytes to fill C */ + entropy->pub.insufficient_data = FALSE; /* Initialize restart counter */ entropy->restarts_to_go = cinfo->restart_interval; @@ -763,7 +772,7 @@ jinit_arith_decoder(j_decompress_ptr cinfo) int *coef_bit_ptr, ci; cinfo->coef_bits = (int (*)[DCTSIZE2]) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, - cinfo->num_components * DCTSIZE2 * + cinfo->num_components * 2 * DCTSIZE2 * sizeof(int)); coef_bit_ptr = &cinfo->coef_bits[0][0]; for (ci = 0; ci < cinfo->num_components; ci++) diff --git a/third-party/mozjpeg/mozjpeg/jdatadst-tj.c b/third-party/mozjpeg/mozjpeg/jdatadst-tj.c index fdaa2de1de3..e10d98127bf 100644 --- a/third-party/mozjpeg/mozjpeg/jdatadst-tj.c +++ b/third-party/mozjpeg/mozjpeg/jdatadst-tj.c @@ -5,7 +5,7 @@ * Copyright (C) 1994-1996, Thomas G. Lane. * Modified 2009-2012 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2011, 2014, 2016, 2019, D. R. Commander. + * Copyright (C) 2011, 2014, 2016, 2019, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -23,10 +23,6 @@ #include "jpeglib.h" #include "jerror.h" -#ifndef HAVE_STDLIB_H /* should declare malloc(),free() */ -extern void *malloc(size_t size); -extern void free(void *ptr); -#endif void jpeg_mem_dest_tj(j_compress_ptr cinfo, unsigned char **outbuffer, unsigned long *outsize, boolean alloc); @@ -101,7 +97,7 @@ empty_mem_output_buffer(j_compress_ptr cinfo) if (nextbuffer == NULL) ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10); - MEMCOPY(nextbuffer, dest->buffer, dest->bufsize); + memcpy(nextbuffer, dest->buffer, dest->bufsize); free(dest->newbuffer); diff --git a/third-party/mozjpeg/mozjpeg/jdatadst.c b/third-party/mozjpeg/mozjpeg/jdatadst.c index a1a31b65996..37b80c47076 100644 --- a/third-party/mozjpeg/mozjpeg/jdatadst.c +++ b/third-party/mozjpeg/mozjpeg/jdatadst.c @@ -5,7 +5,7 @@ * Copyright (C) 1994-1996, Thomas G. Lane. * Modified 2009-2012 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2013, 2016, D. R. Commander. + * Copyright (C) 2013, 2016, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -24,11 +24,6 @@ #include "jerror.h" #include "jpegint.h" -#ifndef HAVE_STDLIB_H /* should declare malloc(),free() */ -extern void *malloc (size_t size); -extern void free (void *ptr); -#endif - /* Expanded data destination object for stdio output */ @@ -74,7 +69,7 @@ init_destination (j_compress_ptr cinfo) /* Allocate the output buffer --- it will be released when done with image */ dest->buffer = (JOCTET *) (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_IMAGE, - OUTPUT_BUF_SIZE * sizeof(JOCTET)); + OUTPUT_BUF_SIZE * sizeof(JOCTET)); dest->pub.next_output_byte = dest->buffer; dest->pub.free_in_buffer = OUTPUT_BUF_SIZE; @@ -117,7 +112,7 @@ empty_output_buffer (j_compress_ptr cinfo) { my_dest_ptr dest = (my_dest_ptr) cinfo->dest; - if (JFWRITE(dest->outfile, dest->buffer, OUTPUT_BUF_SIZE) != + if (fwrite(dest->buffer, 1, OUTPUT_BUF_SIZE, dest->outfile) != (size_t) OUTPUT_BUF_SIZE) ERREXIT(cinfo, JERR_FILE_WRITE); @@ -142,7 +137,7 @@ empty_mem_output_buffer (j_compress_ptr cinfo) if (nextbuffer == NULL) ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10); - MEMCOPY(nextbuffer, dest->buffer, dest->bufsize); + memcpy(nextbuffer, dest->buffer, dest->bufsize); free(dest->newbuffer); @@ -176,7 +171,7 @@ term_destination (j_compress_ptr cinfo) /* Write any data remaining in the buffer */ if (datacount > 0) { - if (JFWRITE(dest->outfile, dest->buffer, datacount) != datacount) + if (fwrite(dest->buffer, 1, datacount, dest->outfile) != datacount) ERREXIT(cinfo, JERR_FILE_WRITE); } fflush(dest->outfile); diff --git a/third-party/mozjpeg/mozjpeg/jdatasrc.c b/third-party/mozjpeg/mozjpeg/jdatasrc.c index eadb4a2c90d..e36a30d8944 100644 --- a/third-party/mozjpeg/mozjpeg/jdatasrc.c +++ b/third-party/mozjpeg/mozjpeg/jdatasrc.c @@ -5,7 +5,7 @@ * Copyright (C) 1994-1996, Thomas G. Lane. * Modified 2009-2011 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2013, 2016, D. R. Commander. + * Copyright (C) 2013, 2016, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -104,7 +104,7 @@ fill_input_buffer(j_decompress_ptr cinfo) my_src_ptr src = (my_src_ptr)cinfo->src; size_t nbytes; - nbytes = JFREAD(src->infile, src->buffer, INPUT_BUF_SIZE); + nbytes = fread(src->buffer, 1, INPUT_BUF_SIZE, src->infile); if (nbytes <= 0) { if (src->start_of_file) /* Treat empty input file as fatal error */ diff --git a/third-party/mozjpeg/mozjpeg/jdcoefct.c b/third-party/mozjpeg/mozjpeg/jdcoefct.c index 723a9ac2be6..82d2310fb1d 100644 --- a/third-party/mozjpeg/mozjpeg/jdcoefct.c +++ b/third-party/mozjpeg/mozjpeg/jdcoefct.c @@ -5,8 +5,8 @@ * Copyright (C) 1994-1997, Thomas G. Lane. * libjpeg-turbo Modifications: * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2010, 2015-2016, D. R. Commander. - * Copyright (C) 2015, Google, Inc. + * Copyright (C) 2010, 2015-2016, 2019-2020, 2022, D. R. Commander. + * Copyright (C) 2015, 2020, Google, Inc. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -102,6 +102,8 @@ decompress_onepass(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) /* Try to fetch an MCU. Entropy decoder expects buffer to be zeroed. */ jzero_far((void *)coef->MCU_buffer[0], (size_t)(cinfo->blocks_in_MCU * sizeof(JBLOCK))); + if (!cinfo->entropy->insufficient_data) + cinfo->master->last_good_iMCU_row = cinfo->input_iMCU_row; if (!(*cinfo->entropy->decode_mcu) (cinfo, coef->MCU_buffer)) { /* Suspension forced; update state counters and exit */ coef->MCU_vert_offset = yoffset; @@ -227,6 +229,8 @@ consume_data(j_decompress_ptr cinfo) } } } + if (!cinfo->entropy->insufficient_data) + cinfo->master->last_good_iMCU_row = cinfo->input_iMCU_row; /* Try to fetch the MCU. */ if (!(*cinfo->entropy->decode_mcu) (cinfo, coef->MCU_buffer)) { /* Suspension forced; update state counters and exit */ @@ -326,19 +330,22 @@ decompress_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) #ifdef BLOCK_SMOOTHING_SUPPORTED /* - * This code applies interblock smoothing as described by section K.8 - * of the JPEG standard: the first 5 AC coefficients are estimated from - * the DC values of a DCT block and its 8 neighboring blocks. + * This code applies interblock smoothing; the first 9 AC coefficients are + * estimated from the DC values of a DCT block and its 24 neighboring blocks. * We apply smoothing only for progressive JPEG decoding, and only if * the coefficients it can estimate are not yet known to full precision. */ -/* Natural-order array positions of the first 5 zigzag-order coefficients */ +/* Natural-order array positions of the first 9 zigzag-order coefficients */ #define Q01_POS 1 #define Q10_POS 8 #define Q20_POS 16 #define Q11_POS 9 #define Q02_POS 2 +#define Q03_POS 3 +#define Q12_POS 10 +#define Q21_POS 17 +#define Q30_POS 24 /* * Determine whether block smoothing is applicable and safe. @@ -356,8 +363,8 @@ smoothing_ok(j_decompress_ptr cinfo) int ci, coefi; jpeg_component_info *compptr; JQUANT_TBL *qtable; - int *coef_bits; - int *coef_bits_latch; + int *coef_bits, *prev_coef_bits; + int *coef_bits_latch, *prev_coef_bits_latch; if (!cinfo->progressive_mode || cinfo->coef_bits == NULL) return FALSE; @@ -366,34 +373,47 @@ smoothing_ok(j_decompress_ptr cinfo) if (coef->coef_bits_latch == NULL) coef->coef_bits_latch = (int *) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, - cinfo->num_components * + cinfo->num_components * 2 * (SAVED_COEFS * sizeof(int))); coef_bits_latch = coef->coef_bits_latch; + prev_coef_bits_latch = + &coef->coef_bits_latch[cinfo->num_components * SAVED_COEFS]; for (ci = 0, compptr = cinfo->comp_info; ci < cinfo->num_components; ci++, compptr++) { /* All components' quantization values must already be latched. */ if ((qtable = compptr->quant_table) == NULL) return FALSE; - /* Verify DC & first 5 AC quantizers are nonzero to avoid zero-divide. */ + /* Verify DC & first 9 AC quantizers are nonzero to avoid zero-divide. */ if (qtable->quantval[0] == 0 || qtable->quantval[Q01_POS] == 0 || qtable->quantval[Q10_POS] == 0 || qtable->quantval[Q20_POS] == 0 || qtable->quantval[Q11_POS] == 0 || - qtable->quantval[Q02_POS] == 0) + qtable->quantval[Q02_POS] == 0 || + qtable->quantval[Q03_POS] == 0 || + qtable->quantval[Q12_POS] == 0 || + qtable->quantval[Q21_POS] == 0 || + qtable->quantval[Q30_POS] == 0) return FALSE; /* DC values must be at least partly known for all components. */ coef_bits = cinfo->coef_bits[ci]; + prev_coef_bits = cinfo->coef_bits[ci + cinfo->num_components]; if (coef_bits[0] < 0) return FALSE; + coef_bits_latch[0] = coef_bits[0]; /* Block smoothing is helpful if some AC coefficients remain inaccurate. */ - for (coefi = 1; coefi <= 5; coefi++) { + for (coefi = 1; coefi < SAVED_COEFS; coefi++) { + if (cinfo->input_scan_number > 1) + prev_coef_bits_latch[coefi] = prev_coef_bits[coefi]; + else + prev_coef_bits_latch[coefi] = -1; coef_bits_latch[coefi] = coef_bits[coefi]; if (coef_bits[coefi] != 0) smoothing_useful = TRUE; } coef_bits_latch += SAVED_COEFS; + prev_coef_bits_latch += SAVED_COEFS; } return smoothing_useful; @@ -412,17 +432,20 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) JDIMENSION block_num, last_block_column; int ci, block_row, block_rows, access_rows; JBLOCKARRAY buffer; - JBLOCKROW buffer_ptr, prev_block_row, next_block_row; + JBLOCKROW buffer_ptr, prev_prev_block_row, prev_block_row; + JBLOCKROW next_block_row, next_next_block_row; JSAMPARRAY output_ptr; JDIMENSION output_col; jpeg_component_info *compptr; inverse_DCT_method_ptr inverse_DCT; - boolean first_row, last_row; + boolean change_dc; JCOEF *workspace; int *coef_bits; JQUANT_TBL *quanttbl; - JLONG Q00, Q01, Q02, Q10, Q11, Q20, num; - int DC1, DC2, DC3, DC4, DC5, DC6, DC7, DC8, DC9; + JLONG Q00, Q01, Q02, Q03 = 0, Q10, Q11, Q12 = 0, Q20, Q21 = 0, Q30 = 0, num; + int DC01, DC02, DC03, DC04, DC05, DC06, DC07, DC08, DC09, DC10, DC11, DC12, + DC13, DC14, DC15, DC16, DC17, DC18, DC19, DC20, DC21, DC22, DC23, DC24, + DC25; int Al, pred; /* Keep a local variable to avoid looking it up more than once */ @@ -434,10 +457,10 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) if (cinfo->input_scan_number == cinfo->output_scan_number) { /* If input is working on current scan, we ordinarily want it to * have completed the current row. But if input scan is DC, - * we want it to keep one row ahead so that next block row's DC + * we want it to keep two rows ahead so that next two block rows' DC * values are up to date. */ - JDIMENSION delta = (cinfo->Ss == 0) ? 1 : 0; + JDIMENSION delta = (cinfo->Ss == 0) ? 2 : 0; if (cinfo->input_iMCU_row > cinfo->output_iMCU_row + delta) break; } @@ -452,34 +475,53 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) if (!compptr->component_needed) continue; /* Count non-dummy DCT block rows in this iMCU row. */ - if (cinfo->output_iMCU_row < last_iMCU_row) { + if (cinfo->output_iMCU_row + 1 < last_iMCU_row) { + block_rows = compptr->v_samp_factor; + access_rows = block_rows * 3; /* this and next two iMCU rows */ + } else if (cinfo->output_iMCU_row < last_iMCU_row) { block_rows = compptr->v_samp_factor; access_rows = block_rows * 2; /* this and next iMCU row */ - last_row = FALSE; } else { /* NB: can't use last_row_height here; it is input-side-dependent! */ block_rows = (int)(compptr->height_in_blocks % compptr->v_samp_factor); if (block_rows == 0) block_rows = compptr->v_samp_factor; access_rows = block_rows; /* this iMCU row only */ - last_row = TRUE; } /* Align the virtual buffer for this component. */ - if (cinfo->output_iMCU_row > 0) { - access_rows += compptr->v_samp_factor; /* prior iMCU row too */ + if (cinfo->output_iMCU_row > 1) { + access_rows += 2 * compptr->v_samp_factor; /* prior two iMCU rows too */ + buffer = (*cinfo->mem->access_virt_barray) + ((j_common_ptr)cinfo, coef->whole_image[ci], + (cinfo->output_iMCU_row - 2) * compptr->v_samp_factor, + (JDIMENSION)access_rows, FALSE); + buffer += 2 * compptr->v_samp_factor; /* point to current iMCU row */ + } else if (cinfo->output_iMCU_row > 0) { buffer = (*cinfo->mem->access_virt_barray) ((j_common_ptr)cinfo, coef->whole_image[ci], (cinfo->output_iMCU_row - 1) * compptr->v_samp_factor, (JDIMENSION)access_rows, FALSE); buffer += compptr->v_samp_factor; /* point to current iMCU row */ - first_row = FALSE; } else { buffer = (*cinfo->mem->access_virt_barray) ((j_common_ptr)cinfo, coef->whole_image[ci], (JDIMENSION)0, (JDIMENSION)access_rows, FALSE); - first_row = TRUE; } - /* Fetch component-dependent info */ - coef_bits = coef->coef_bits_latch + (ci * SAVED_COEFS); + /* Fetch component-dependent info. + * If the current scan is incomplete, then we use the component-dependent + * info from the previous scan. + */ + if (cinfo->output_iMCU_row > cinfo->master->last_good_iMCU_row) + coef_bits = + coef->coef_bits_latch + ((ci + cinfo->num_components) * SAVED_COEFS); + else + coef_bits = coef->coef_bits_latch + (ci * SAVED_COEFS); + + /* We only do DC interpolation if no AC coefficient data is available. */ + change_dc = + coef_bits[1] == -1 && coef_bits[2] == -1 && coef_bits[3] == -1 && + coef_bits[4] == -1 && coef_bits[5] == -1 && coef_bits[6] == -1 && + coef_bits[7] == -1 && coef_bits[8] == -1 && coef_bits[9] == -1; + quanttbl = compptr->quant_table; Q00 = quanttbl->quantval[0]; Q01 = quanttbl->quantval[Q01_POS]; @@ -487,25 +529,51 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) Q20 = quanttbl->quantval[Q20_POS]; Q11 = quanttbl->quantval[Q11_POS]; Q02 = quanttbl->quantval[Q02_POS]; + if (change_dc) { + Q03 = quanttbl->quantval[Q03_POS]; + Q12 = quanttbl->quantval[Q12_POS]; + Q21 = quanttbl->quantval[Q21_POS]; + Q30 = quanttbl->quantval[Q30_POS]; + } inverse_DCT = cinfo->idct->inverse_DCT[ci]; output_ptr = output_buf[ci]; /* Loop over all DCT blocks to be processed. */ for (block_row = 0; block_row < block_rows; block_row++) { buffer_ptr = buffer[block_row] + cinfo->master->first_MCU_col[ci]; - if (first_row && block_row == 0) + + if (block_row > 0 || cinfo->output_iMCU_row > 0) + prev_block_row = + buffer[block_row - 1] + cinfo->master->first_MCU_col[ci]; + else prev_block_row = buffer_ptr; + + if (block_row > 1 || cinfo->output_iMCU_row > 1) + prev_prev_block_row = + buffer[block_row - 2] + cinfo->master->first_MCU_col[ci]; + else + prev_prev_block_row = prev_block_row; + + if (block_row < block_rows - 1 || cinfo->output_iMCU_row < last_iMCU_row) + next_block_row = + buffer[block_row + 1] + cinfo->master->first_MCU_col[ci]; else - prev_block_row = buffer[block_row - 1]; - if (last_row && block_row == block_rows - 1) next_block_row = buffer_ptr; + + if (block_row < block_rows - 2 || + cinfo->output_iMCU_row + 1 < last_iMCU_row) + next_next_block_row = + buffer[block_row + 2] + cinfo->master->first_MCU_col[ci]; else - next_block_row = buffer[block_row + 1]; + next_next_block_row = next_block_row; + /* We fetch the surrounding DC values using a sliding-register approach. - * Initialize all nine here so as to do the right thing on narrow pics. + * Initialize all 25 here so as to do the right thing on narrow pics. */ - DC1 = DC2 = DC3 = (int)prev_block_row[0][0]; - DC4 = DC5 = DC6 = (int)buffer_ptr[0][0]; - DC7 = DC8 = DC9 = (int)next_block_row[0][0]; + DC01 = DC02 = DC03 = DC04 = DC05 = (int)prev_prev_block_row[0][0]; + DC06 = DC07 = DC08 = DC09 = DC10 = (int)prev_block_row[0][0]; + DC11 = DC12 = DC13 = DC14 = DC15 = (int)buffer_ptr[0][0]; + DC16 = DC17 = DC18 = DC19 = DC20 = (int)next_block_row[0][0]; + DC21 = DC22 = DC23 = DC24 = DC25 = (int)next_next_block_row[0][0]; output_col = 0; last_block_column = compptr->width_in_blocks - 1; for (block_num = cinfo->master->first_MCU_col[ci]; @@ -513,18 +581,39 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) /* Fetch current DCT block into workspace so we can modify it. */ jcopy_block_row(buffer_ptr, (JBLOCKROW)workspace, (JDIMENSION)1); /* Update DC values */ - if (block_num < last_block_column) { - DC3 = (int)prev_block_row[1][0]; - DC6 = (int)buffer_ptr[1][0]; - DC9 = (int)next_block_row[1][0]; + if (block_num == cinfo->master->first_MCU_col[ci] && + block_num < last_block_column) { + DC04 = DC05 = (int)prev_prev_block_row[1][0]; + DC09 = DC10 = (int)prev_block_row[1][0]; + DC14 = DC15 = (int)buffer_ptr[1][0]; + DC19 = DC20 = (int)next_block_row[1][0]; + DC24 = DC25 = (int)next_next_block_row[1][0]; } - /* Compute coefficient estimates per K.8. - * An estimate is applied only if coefficient is still zero, - * and is not known to be fully accurate. + if (block_num + 1 < last_block_column) { + DC05 = (int)prev_prev_block_row[2][0]; + DC10 = (int)prev_block_row[2][0]; + DC15 = (int)buffer_ptr[2][0]; + DC20 = (int)next_block_row[2][0]; + DC25 = (int)next_next_block_row[2][0]; + } + /* If DC interpolation is enabled, compute coefficient estimates using + * a Gaussian-like kernel, keeping the averages of the DC values. + * + * If DC interpolation is disabled, compute coefficient estimates using + * an algorithm similar to the one described in Section K.8 of the JPEG + * standard, except applied to a 5x5 window rather than a 3x3 window. + * + * An estimate is applied only if the coefficient is still zero and is + * not known to be fully accurate. */ /* AC01 */ if ((Al = coef_bits[1]) != 0 && workspace[1] == 0) { - num = 36 * Q00 * (DC4 - DC6); + num = Q00 * (change_dc ? + (-DC01 - DC02 + DC04 + DC05 - 3 * DC06 + 13 * DC07 - + 13 * DC09 + 3 * DC10 - 3 * DC11 + 38 * DC12 - 38 * DC14 + + 3 * DC15 - 3 * DC16 + 13 * DC17 - 13 * DC19 + 3 * DC20 - + DC21 - DC22 + DC24 + DC25) : + (-7 * DC11 + 50 * DC12 - 50 * DC14 + 7 * DC15)); if (num >= 0) { pred = (int)(((Q01 << 7) + num) / (Q01 << 8)); if (Al > 0 && pred >= (1 << Al)) @@ -539,7 +628,12 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) } /* AC10 */ if ((Al = coef_bits[2]) != 0 && workspace[8] == 0) { - num = 36 * Q00 * (DC2 - DC8); + num = Q00 * (change_dc ? + (-DC01 - 3 * DC02 - 3 * DC03 - 3 * DC04 - DC05 - DC06 + + 13 * DC07 + 38 * DC08 + 13 * DC09 - DC10 + DC16 - + 13 * DC17 - 38 * DC18 - 13 * DC19 + DC20 + DC21 + + 3 * DC22 + 3 * DC23 + 3 * DC24 + DC25) : + (-7 * DC03 + 50 * DC08 - 50 * DC18 + 7 * DC23)); if (num >= 0) { pred = (int)(((Q10 << 7) + num) / (Q10 << 8)); if (Al > 0 && pred >= (1 << Al)) @@ -554,7 +648,10 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) } /* AC20 */ if ((Al = coef_bits[3]) != 0 && workspace[16] == 0) { - num = 9 * Q00 * (DC2 + DC8 - 2 * DC5); + num = Q00 * (change_dc ? + (DC03 + 2 * DC07 + 7 * DC08 + 2 * DC09 - 5 * DC12 - 14 * DC13 - + 5 * DC14 + 2 * DC17 + 7 * DC18 + 2 * DC19 + DC23) : + (-DC03 + 13 * DC08 - 24 * DC13 + 13 * DC18 - DC23)); if (num >= 0) { pred = (int)(((Q20 << 7) + num) / (Q20 << 8)); if (Al > 0 && pred >= (1 << Al)) @@ -569,7 +666,11 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) } /* AC11 */ if ((Al = coef_bits[4]) != 0 && workspace[9] == 0) { - num = 5 * Q00 * (DC1 - DC3 - DC7 + DC9); + num = Q00 * (change_dc ? + (-DC01 + DC05 + 9 * DC07 - 9 * DC09 - 9 * DC17 + + 9 * DC19 + DC21 - DC25) : + (DC10 + DC16 - 10 * DC17 + 10 * DC19 - DC02 - DC20 + DC22 - + DC24 + DC04 - DC06 + 10 * DC07 - 10 * DC09)); if (num >= 0) { pred = (int)(((Q11 << 7) + num) / (Q11 << 8)); if (Al > 0 && pred >= (1 << Al)) @@ -584,7 +685,10 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) } /* AC02 */ if ((Al = coef_bits[5]) != 0 && workspace[2] == 0) { - num = 9 * Q00 * (DC4 + DC6 - 2 * DC5); + num = Q00 * (change_dc ? + (2 * DC07 - 5 * DC08 + 2 * DC09 + DC11 + 7 * DC12 - 14 * DC13 + + 7 * DC14 + DC15 + 2 * DC17 - 5 * DC18 + 2 * DC19) : + (-DC11 + 13 * DC12 - 24 * DC13 + 13 * DC14 - DC15)); if (num >= 0) { pred = (int)(((Q02 << 7) + num) / (Q02 << 8)); if (Al > 0 && pred >= (1 << Al)) @@ -597,14 +701,96 @@ decompress_smooth_data(j_decompress_ptr cinfo, JSAMPIMAGE output_buf) } workspace[2] = (JCOEF)pred; } + if (change_dc) { + /* AC03 */ + if ((Al = coef_bits[6]) != 0 && workspace[3] == 0) { + num = Q00 * (DC07 - DC09 + 2 * DC12 - 2 * DC14 + DC17 - DC19); + if (num >= 0) { + pred = (int)(((Q03 << 7) + num) / (Q03 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + } else { + pred = (int)(((Q03 << 7) - num) / (Q03 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + pred = -pred; + } + workspace[3] = (JCOEF)pred; + } + /* AC12 */ + if ((Al = coef_bits[7]) != 0 && workspace[10] == 0) { + num = Q00 * (DC07 - 3 * DC08 + DC09 - DC17 + 3 * DC18 - DC19); + if (num >= 0) { + pred = (int)(((Q12 << 7) + num) / (Q12 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + } else { + pred = (int)(((Q12 << 7) - num) / (Q12 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + pred = -pred; + } + workspace[10] = (JCOEF)pred; + } + /* AC21 */ + if ((Al = coef_bits[8]) != 0 && workspace[17] == 0) { + num = Q00 * (DC07 - DC09 - 3 * DC12 + 3 * DC14 + DC17 - DC19); + if (num >= 0) { + pred = (int)(((Q21 << 7) + num) / (Q21 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + } else { + pred = (int)(((Q21 << 7) - num) / (Q21 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + pred = -pred; + } + workspace[17] = (JCOEF)pred; + } + /* AC30 */ + if ((Al = coef_bits[9]) != 0 && workspace[24] == 0) { + num = Q00 * (DC07 + 2 * DC08 + DC09 - DC17 - 2 * DC18 - DC19); + if (num >= 0) { + pred = (int)(((Q30 << 7) + num) / (Q30 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + } else { + pred = (int)(((Q30 << 7) - num) / (Q30 << 8)); + if (Al > 0 && pred >= (1 << Al)) + pred = (1 << Al) - 1; + pred = -pred; + } + workspace[24] = (JCOEF)pred; + } + /* coef_bits[0] is non-negative. Otherwise this function would not + * be called. + */ + num = Q00 * + (-2 * DC01 - 6 * DC02 - 8 * DC03 - 6 * DC04 - 2 * DC05 - + 6 * DC06 + 6 * DC07 + 42 * DC08 + 6 * DC09 - 6 * DC10 - + 8 * DC11 + 42 * DC12 + 152 * DC13 + 42 * DC14 - 8 * DC15 - + 6 * DC16 + 6 * DC17 + 42 * DC18 + 6 * DC19 - 6 * DC20 - + 2 * DC21 - 6 * DC22 - 8 * DC23 - 6 * DC24 - 2 * DC25); + if (num >= 0) { + pred = (int)(((Q00 << 7) + num) / (Q00 << 8)); + } else { + pred = (int)(((Q00 << 7) - num) / (Q00 << 8)); + pred = -pred; + } + workspace[0] = (JCOEF)pred; + } /* change_dc */ + /* OK, do the IDCT */ (*inverse_DCT) (cinfo, compptr, (JCOEFPTR)workspace, output_ptr, output_col); /* Advance for next column */ - DC1 = DC2; DC2 = DC3; - DC4 = DC5; DC5 = DC6; - DC7 = DC8; DC8 = DC9; - buffer_ptr++, prev_block_row++, next_block_row++; + DC01 = DC02; DC02 = DC03; DC03 = DC04; DC04 = DC05; + DC06 = DC07; DC07 = DC08; DC08 = DC09; DC09 = DC10; + DC11 = DC12; DC12 = DC13; DC13 = DC14; DC14 = DC15; + DC16 = DC17; DC17 = DC18; DC18 = DC19; DC19 = DC20; + DC21 = DC22; DC22 = DC23; DC23 = DC24; DC24 = DC25; + buffer_ptr++, prev_block_row++, next_block_row++, + prev_prev_block_row++, next_next_block_row++; output_col += compptr->_DCT_scaled_size; } output_ptr += compptr->_DCT_scaled_size; @@ -653,7 +839,7 @@ jinit_d_coef_controller(j_decompress_ptr cinfo, boolean need_full_buffer) #ifdef BLOCK_SMOOTHING_SUPPORTED /* If block smoothing could be used, need a bigger window */ if (cinfo->progressive_mode) - access_rows *= 3; + access_rows *= 5; #endif coef->whole_image[ci] = (*cinfo->mem->request_virt_barray) ((j_common_ptr)cinfo, JPOOL_IMAGE, TRUE, diff --git a/third-party/mozjpeg/mozjpeg/jdcoefct.h b/third-party/mozjpeg/mozjpeg/jdcoefct.h index c4d1943dd4d..9a0e7806636 100644 --- a/third-party/mozjpeg/mozjpeg/jdcoefct.h +++ b/third-party/mozjpeg/mozjpeg/jdcoefct.h @@ -5,6 +5,7 @@ * Copyright (C) 1994-1997, Thomas G. Lane. * libjpeg-turbo Modifications: * Copyright 2009 Pierre Ossman for Cendio AB + * Copyright (C) 2020, Google, Inc. * For conditions of distribution and use, see the accompanying README.ijg * file. */ @@ -51,7 +52,7 @@ typedef struct { #ifdef BLOCK_SMOOTHING_SUPPORTED /* When doing block smoothing, we latch coefficient Al values here */ int *coef_bits_latch; -#define SAVED_COEFS 6 /* we save coef_bits[0..5] */ +#define SAVED_COEFS 10 /* we save coef_bits[0..9] */ #endif } my_coef_controller; diff --git a/third-party/mozjpeg/mozjpeg/jdcol565.c b/third-party/mozjpeg/mozjpeg/jdcol565.c index 40068ef84fd..53c7bd9187d 100644 --- a/third-party/mozjpeg/mozjpeg/jdcol565.c +++ b/third-party/mozjpeg/mozjpeg/jdcol565.c @@ -45,9 +45,9 @@ ycc_rgb565_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, outptr = *output_buf++; if (PACK_NEED_ALIGNMENT(outptr)) { - y = GETJSAMPLE(*inptr0++); - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + y = *inptr0++; + cb = *inptr1++; + cr = *inptr2++; r = range_limit[y + Crrtab[cr]]; g = range_limit[y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS))]; @@ -58,18 +58,18 @@ ycc_rgb565_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, num_cols--; } for (col = 0; col < (num_cols >> 1); col++) { - y = GETJSAMPLE(*inptr0++); - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + y = *inptr0++; + cb = *inptr1++; + cr = *inptr2++; r = range_limit[y + Crrtab[cr]]; g = range_limit[y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS))]; b = range_limit[y + Cbbtab[cb]]; rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr0++); - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + y = *inptr0++; + cb = *inptr1++; + cr = *inptr2++; r = range_limit[y + Crrtab[cr]]; g = range_limit[y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS))]; @@ -80,9 +80,9 @@ ycc_rgb565_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, outptr += 4; } if (num_cols & 1) { - y = GETJSAMPLE(*inptr0); - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + y = *inptr0; + cb = *inptr1; + cr = *inptr2; r = range_limit[y + Crrtab[cr]]; g = range_limit[y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS))]; @@ -125,9 +125,9 @@ ycc_rgb565D_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, input_row++; outptr = *output_buf++; if (PACK_NEED_ALIGNMENT(outptr)) { - y = GETJSAMPLE(*inptr0++); - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + y = *inptr0++; + cb = *inptr1++; + cr = *inptr2++; r = range_limit[DITHER_565_R(y + Crrtab[cr], d0)]; g = range_limit[DITHER_565_G(y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], @@ -139,9 +139,9 @@ ycc_rgb565D_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, num_cols--; } for (col = 0; col < (num_cols >> 1); col++) { - y = GETJSAMPLE(*inptr0++); - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + y = *inptr0++; + cb = *inptr1++; + cr = *inptr2++; r = range_limit[DITHER_565_R(y + Crrtab[cr], d0)]; g = range_limit[DITHER_565_G(y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], @@ -150,9 +150,9 @@ ycc_rgb565D_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, d0 = DITHER_ROTATE(d0); rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr0++); - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + y = *inptr0++; + cb = *inptr1++; + cr = *inptr2++; r = range_limit[DITHER_565_R(y + Crrtab[cr], d0)]; g = range_limit[DITHER_565_G(y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], @@ -165,9 +165,9 @@ ycc_rgb565D_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, outptr += 4; } if (num_cols & 1) { - y = GETJSAMPLE(*inptr0); - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + y = *inptr0; + cb = *inptr1; + cr = *inptr2; r = range_limit[DITHER_565_R(y + Crrtab[cr], d0)]; g = range_limit[DITHER_565_G(y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], @@ -202,32 +202,32 @@ rgb_rgb565_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, input_row++; outptr = *output_buf++; if (PACK_NEED_ALIGNMENT(outptr)) { - r = GETJSAMPLE(*inptr0++); - g = GETJSAMPLE(*inptr1++); - b = GETJSAMPLE(*inptr2++); + r = *inptr0++; + g = *inptr1++; + b = *inptr2++; rgb = PACK_SHORT_565(r, g, b); *(INT16 *)outptr = (INT16)rgb; outptr += 2; num_cols--; } for (col = 0; col < (num_cols >> 1); col++) { - r = GETJSAMPLE(*inptr0++); - g = GETJSAMPLE(*inptr1++); - b = GETJSAMPLE(*inptr2++); + r = *inptr0++; + g = *inptr1++; + b = *inptr2++; rgb = PACK_SHORT_565(r, g, b); - r = GETJSAMPLE(*inptr0++); - g = GETJSAMPLE(*inptr1++); - b = GETJSAMPLE(*inptr2++); + r = *inptr0++; + g = *inptr1++; + b = *inptr2++; rgb = PACK_TWO_PIXELS(rgb, PACK_SHORT_565(r, g, b)); WRITE_TWO_ALIGNED_PIXELS(outptr, rgb); outptr += 4; } if (num_cols & 1) { - r = GETJSAMPLE(*inptr0); - g = GETJSAMPLE(*inptr1); - b = GETJSAMPLE(*inptr2); + r = *inptr0; + g = *inptr1; + b = *inptr2; rgb = PACK_SHORT_565(r, g, b); *(INT16 *)outptr = (INT16)rgb; } @@ -259,24 +259,24 @@ rgb_rgb565D_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, input_row++; outptr = *output_buf++; if (PACK_NEED_ALIGNMENT(outptr)) { - r = range_limit[DITHER_565_R(GETJSAMPLE(*inptr0++), d0)]; - g = range_limit[DITHER_565_G(GETJSAMPLE(*inptr1++), d0)]; - b = range_limit[DITHER_565_B(GETJSAMPLE(*inptr2++), d0)]; + r = range_limit[DITHER_565_R(*inptr0++, d0)]; + g = range_limit[DITHER_565_G(*inptr1++, d0)]; + b = range_limit[DITHER_565_B(*inptr2++, d0)]; rgb = PACK_SHORT_565(r, g, b); *(INT16 *)outptr = (INT16)rgb; outptr += 2; num_cols--; } for (col = 0; col < (num_cols >> 1); col++) { - r = range_limit[DITHER_565_R(GETJSAMPLE(*inptr0++), d0)]; - g = range_limit[DITHER_565_G(GETJSAMPLE(*inptr1++), d0)]; - b = range_limit[DITHER_565_B(GETJSAMPLE(*inptr2++), d0)]; + r = range_limit[DITHER_565_R(*inptr0++, d0)]; + g = range_limit[DITHER_565_G(*inptr1++, d0)]; + b = range_limit[DITHER_565_B(*inptr2++, d0)]; d0 = DITHER_ROTATE(d0); rgb = PACK_SHORT_565(r, g, b); - r = range_limit[DITHER_565_R(GETJSAMPLE(*inptr0++), d0)]; - g = range_limit[DITHER_565_G(GETJSAMPLE(*inptr1++), d0)]; - b = range_limit[DITHER_565_B(GETJSAMPLE(*inptr2++), d0)]; + r = range_limit[DITHER_565_R(*inptr0++, d0)]; + g = range_limit[DITHER_565_G(*inptr1++, d0)]; + b = range_limit[DITHER_565_B(*inptr2++, d0)]; d0 = DITHER_ROTATE(d0); rgb = PACK_TWO_PIXELS(rgb, PACK_SHORT_565(r, g, b)); @@ -284,9 +284,9 @@ rgb_rgb565D_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, outptr += 4; } if (num_cols & 1) { - r = range_limit[DITHER_565_R(GETJSAMPLE(*inptr0), d0)]; - g = range_limit[DITHER_565_G(GETJSAMPLE(*inptr1), d0)]; - b = range_limit[DITHER_565_B(GETJSAMPLE(*inptr2), d0)]; + r = range_limit[DITHER_565_R(*inptr0, d0)]; + g = range_limit[DITHER_565_G(*inptr1, d0)]; + b = range_limit[DITHER_565_B(*inptr2, d0)]; rgb = PACK_SHORT_565(r, g, b); *(INT16 *)outptr = (INT16)rgb; } diff --git a/third-party/mozjpeg/mozjpeg/jdcolext.c b/third-party/mozjpeg/mozjpeg/jdcolext.c index 72a53010703..fc7e7b8f004 100644 --- a/third-party/mozjpeg/mozjpeg/jdcolext.c +++ b/third-party/mozjpeg/mozjpeg/jdcolext.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2009, 2011, 2015, D. R. Commander. + * Copyright (C) 2009, 2011, 2015, 2023, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -53,19 +53,19 @@ ycc_rgb_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, input_row++; outptr = *output_buf++; for (col = 0; col < num_cols; col++) { - y = GETJSAMPLE(inptr0[col]); - cb = GETJSAMPLE(inptr1[col]); - cr = GETJSAMPLE(inptr2[col]); + y = inptr0[col]; + cb = inptr1[col]; + cr = inptr2[col]; /* Range-limiting is essential due to noise introduced by DCT losses. */ outptr[RGB_RED] = range_limit[y + Crrtab[cr]]; outptr[RGB_GREEN] = range_limit[y + ((int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS))]; outptr[RGB_BLUE] = range_limit[y + Cbbtab[cb]]; - /* Set unused byte to 0xFF so it can be interpreted as an opaque */ + /* Set unused byte to MAXJSAMPLE so it can be interpreted as an opaque */ /* alpha channel value */ #ifdef RGB_ALPHA - outptr[RGB_ALPHA] = 0xFF; + outptr[RGB_ALPHA] = MAXJSAMPLE; #endif outptr += RGB_PIXELSIZE; } @@ -93,12 +93,11 @@ gray_rgb_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, inptr = input_buf[0][input_row++]; outptr = *output_buf++; for (col = 0; col < num_cols; col++) { - /* We can dispense with GETJSAMPLE() here */ outptr[RGB_RED] = outptr[RGB_GREEN] = outptr[RGB_BLUE] = inptr[col]; - /* Set unused byte to 0xFF so it can be interpreted as an opaque */ + /* Set unused byte to MAXJSAMPLE so it can be interpreted as an opaque */ /* alpha channel value */ #ifdef RGB_ALPHA - outptr[RGB_ALPHA] = 0xFF; + outptr[RGB_ALPHA] = MAXJSAMPLE; #endif outptr += RGB_PIXELSIZE; } @@ -128,14 +127,13 @@ rgb_rgb_convert_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, input_row++; outptr = *output_buf++; for (col = 0; col < num_cols; col++) { - /* We can dispense with GETJSAMPLE() here */ outptr[RGB_RED] = inptr0[col]; outptr[RGB_GREEN] = inptr1[col]; outptr[RGB_BLUE] = inptr2[col]; - /* Set unused byte to 0xFF so it can be interpreted as an opaque */ + /* Set unused byte to MAXJSAMPLE so it can be interpreted as an opaque */ /* alpha channel value */ #ifdef RGB_ALPHA - outptr[RGB_ALPHA] = 0xFF; + outptr[RGB_ALPHA] = MAXJSAMPLE; #endif outptr += RGB_PIXELSIZE; } diff --git a/third-party/mozjpeg/mozjpeg/jdcolor.c b/third-party/mozjpeg/mozjpeg/jdcolor.c index dc0e3b6c0e9..735190b7000 100644 --- a/third-party/mozjpeg/mozjpeg/jdcolor.c +++ b/third-party/mozjpeg/mozjpeg/jdcolor.c @@ -18,7 +18,6 @@ #include "jinclude.h" #include "jpeglib.h" #include "jsimd.h" -#include "jconfigint.h" /* Private subobject */ @@ -341,9 +340,9 @@ rgb_gray_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, input_row++; outptr = *output_buf++; for (col = 0; col < num_cols; col++) { - r = GETJSAMPLE(inptr0[col]); - g = GETJSAMPLE(inptr1[col]); - b = GETJSAMPLE(inptr2[col]); + r = inptr0[col]; + g = inptr1[col]; + b = inptr2[col]; /* Y */ outptr[col] = (JSAMPLE)((ctab[r + R_Y_OFF] + ctab[g + G_Y_OFF] + ctab[b + B_Y_OFF]) >> SCALEBITS); @@ -550,9 +549,9 @@ ycck_cmyk_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, input_row++; outptr = *output_buf++; for (col = 0; col < num_cols; col++) { - y = GETJSAMPLE(inptr0[col]); - cb = GETJSAMPLE(inptr1[col]); - cr = GETJSAMPLE(inptr2[col]); + y = inptr0[col]; + cb = inptr1[col]; + cr = inptr2[col]; /* Range-limiting is essential due to noise introduced by DCT losses. */ outptr[0] = range_limit[MAXJSAMPLE - (y + Crrtab[cr])]; /* red */ outptr[1] = range_limit[MAXJSAMPLE - (y + /* green */ @@ -560,7 +559,7 @@ ycck_cmyk_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, SCALEBITS)))]; outptr[2] = range_limit[MAXJSAMPLE - (y + Cbbtab[cb])]; /* blue */ /* K passes through unchanged */ - outptr[3] = inptr3[col]; /* don't need GETJSAMPLE here */ + outptr[3] = inptr3[col]; outptr += 4; } } @@ -571,11 +570,10 @@ ycck_cmyk_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, * RGB565 conversion */ -#define PACK_SHORT_565_LE(r, g, b) ((((r) << 8) & 0xF800) | \ - (((g) << 3) & 0x7E0) | ((b) >> 3)) -#define PACK_SHORT_565_BE(r, g, b) (((r) & 0xF8) | ((g) >> 5) | \ - (((g) << 11) & 0xE000) | \ - (((b) << 5) & 0x1F00)) +#define PACK_SHORT_565_LE(r, g, b) \ + ((((r) << 8) & 0xF800) | (((g) << 3) & 0x7E0) | ((b) >> 3)) +#define PACK_SHORT_565_BE(r, g, b) \ + (((r) & 0xF8) | ((g) >> 5) | (((g) << 11) & 0xE000) | (((b) << 5) & 0x1F00)) #define PACK_TWO_PIXELS_LE(l, r) ((r << 16) | l) #define PACK_TWO_PIXELS_BE(l, r) ((l << 16) | r) diff --git a/third-party/mozjpeg/mozjpeg/jddctmgr.c b/third-party/mozjpeg/mozjpeg/jddctmgr.c index 77291a6922b..1083203a244 100644 --- a/third-party/mozjpeg/mozjpeg/jddctmgr.c +++ b/third-party/mozjpeg/mozjpeg/jddctmgr.c @@ -6,7 +6,7 @@ * Modified 2002-2010 by Guido Vollbeding. * libjpeg-turbo Modifications: * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2010, 2015, D. R. Commander. + * Copyright (C) 2010, 2015, 2022, D. R. Commander. * Copyright (C) 2013, MIPS Technologies, Inc., California. * For conditions of distribution and use, see the accompanying README.ijg * file. @@ -360,7 +360,7 @@ jinit_inverse_dct(j_decompress_ptr cinfo) compptr->dct_table = (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, sizeof(multiplier_table)); - MEMZERO(compptr->dct_table, sizeof(multiplier_table)); + memset(compptr->dct_table, 0, sizeof(multiplier_table)); /* Mark multiplier table not yet set up for any method */ idct->cur_method[ci] = -1; } diff --git a/third-party/mozjpeg/mozjpeg/jdhuff.c b/third-party/mozjpeg/mozjpeg/jdhuff.c index a1128178b0a..679d2216859 100644 --- a/third-party/mozjpeg/mozjpeg/jdhuff.c +++ b/third-party/mozjpeg/mozjpeg/jdhuff.c @@ -5,6 +5,7 @@ * Copyright (C) 1991-1997, Thomas G. Lane. * libjpeg-turbo Modifications: * Copyright (C) 2009-2011, 2016, 2018-2019, D. R. Commander. + * Copyright (C) 2018, Matthias Räncker. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -39,24 +40,6 @@ typedef struct { int last_dc_val[MAX_COMPS_IN_SCAN]; /* last DC coef for each component */ } savable_state; -/* This macro is to work around compilers with missing or broken - * structure assignment. You'll need to fix this code if you have - * such a compiler and you change MAX_COMPS_IN_SCAN. - */ - -#ifndef NO_STRUCT_ASSIGN -#define ASSIGN_STATE(dest, src) ((dest) = (src)) -#else -#if MAX_COMPS_IN_SCAN == 4 -#define ASSIGN_STATE(dest, src) \ - ((dest).last_dc_val[0] = (src).last_dc_val[0], \ - (dest).last_dc_val[1] = (src).last_dc_val[1], \ - (dest).last_dc_val[2] = (src).last_dc_val[2], \ - (dest).last_dc_val[3] = (src).last_dc_val[3]) -#endif -#endif - - typedef struct { struct jpeg_entropy_decoder pub; /* public fields */ @@ -325,7 +308,7 @@ jpeg_fill_bit_buffer(bitread_working_state *state, bytes_in_buffer = cinfo->src->bytes_in_buffer; } bytes_in_buffer--; - c = GETJOCTET(*next_input_byte++); + c = *next_input_byte++; /* If it's 0xFF, check and discard stuffed zero byte */ if (c == 0xFF) { @@ -342,7 +325,7 @@ jpeg_fill_bit_buffer(bitread_working_state *state, bytes_in_buffer = cinfo->src->bytes_in_buffer; } bytes_in_buffer--; - c = GETJOCTET(*next_input_byte++); + c = *next_input_byte++; } while (c == 0xFF); if (c == 0) { @@ -405,8 +388,8 @@ jpeg_fill_bit_buffer(bitread_working_state *state, #define GET_BYTE { \ register int c0, c1; \ - c0 = GETJOCTET(*buffer++); \ - c1 = GETJOCTET(*buffer); \ + c0 = *buffer++; \ + c1 = *buffer; \ /* Pre-execute most common case */ \ get_buffer = (get_buffer << 8) | c0; \ bits_left += 8; \ @@ -423,7 +406,7 @@ jpeg_fill_bit_buffer(bitread_working_state *state, } \ } -#if SIZEOF_SIZE_T == 8 || defined(_WIN64) +#if SIZEOF_SIZE_T == 8 || defined(_WIN64) || (defined(__x86_64__) && defined(__ILP32__)) /* Pre-fetch 48 bytes, because the holding register is 64-bit */ #define FILL_BIT_BUFFER_FAST \ @@ -557,6 +540,12 @@ process_restart(j_decompress_ptr cinfo) } +#if defined(__has_feature) +#if __has_feature(undefined_behavior_sanitizer) +__attribute__((no_sanitize("signed-integer-overflow"), + no_sanitize("unsigned-integer-overflow"))) +#endif +#endif LOCAL(boolean) decode_mcu_slow(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) { @@ -568,7 +557,7 @@ decode_mcu_slow(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) /* Load up working state */ BITREAD_LOAD_STATE(cinfo, entropy->bitstate); - ASSIGN_STATE(state, entropy->saved); + state = entropy->saved; for (blkn = 0; blkn < cinfo->blocks_in_MCU; blkn++) { JBLOCKROW block = MCU_data ? MCU_data[blkn] : NULL; @@ -589,11 +578,15 @@ decode_mcu_slow(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) if (entropy->dc_needed[blkn]) { /* Convert DC difference to actual value, update last_dc_val */ int ci = cinfo->MCU_membership[blkn]; - /* This is really just - * s += state.last_dc_val[ci]; - * It is written this way in order to shut up UBSan. + /* Certain malformed JPEG images produce repeated DC coefficient + * differences of 2047 or -2047, which causes state.last_dc_val[ci] to + * grow until it overflows or underflows a 32-bit signed integer. This + * behavior is, to the best of our understanding, innocuous, and it is + * unclear how to work around it without potentially affecting + * performance. Thus, we (hopefully temporarily) suppress UBSan integer + * overflow errors for this function and decode_mcu_fast(). */ - s = (int)((unsigned int)s + (unsigned int)state.last_dc_val[ci]); + s += state.last_dc_val[ci]; state.last_dc_val[ci] = s; if (block) { /* Output the DC coefficient (assumes jpeg_natural_order[0] = 0) */ @@ -653,11 +646,17 @@ decode_mcu_slow(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) /* Completed MCU, so update state */ BITREAD_SAVE_STATE(cinfo, entropy->bitstate); - ASSIGN_STATE(entropy->saved, state); + entropy->saved = state; return TRUE; } +#if defined(__has_feature) +#if __has_feature(undefined_behavior_sanitizer) +__attribute__((no_sanitize("signed-integer-overflow"), + no_sanitize("unsigned-integer-overflow"))) +#endif +#endif LOCAL(boolean) decode_mcu_fast(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) { @@ -671,7 +670,7 @@ decode_mcu_fast(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) /* Load up working state */ BITREAD_LOAD_STATE(cinfo, entropy->bitstate); buffer = (JOCTET *)br_state.next_input_byte; - ASSIGN_STATE(state, entropy->saved); + state = entropy->saved; for (blkn = 0; blkn < cinfo->blocks_in_MCU; blkn++) { JBLOCKROW block = MCU_data ? MCU_data[blkn] : NULL; @@ -688,7 +687,10 @@ decode_mcu_fast(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) if (entropy->dc_needed[blkn]) { int ci = cinfo->MCU_membership[blkn]; - s = (int)((unsigned int)s + (unsigned int)state.last_dc_val[ci]); + /* Refer to the comment in decode_mcu_slow() regarding the supression of + * a UBSan integer overflow error in this line of code. + */ + s += state.last_dc_val[ci]; state.last_dc_val[ci] = s; if (block) (*block)[0] = (JCOEF)s; @@ -740,7 +742,7 @@ decode_mcu_fast(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) br_state.bytes_in_buffer -= (buffer - br_state.next_input_byte); br_state.next_input_byte = buffer; BITREAD_SAVE_STATE(cinfo, entropy->bitstate); - ASSIGN_STATE(entropy->saved, state); + entropy->saved = state; return TRUE; } @@ -795,7 +797,8 @@ decode_mcu(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) } /* Account for restart interval (no-op if not using restarts) */ - entropy->restarts_to_go--; + if (cinfo->restart_interval) + entropy->restarts_to_go--; return TRUE; } diff --git a/third-party/mozjpeg/mozjpeg/jdhuff.h b/third-party/mozjpeg/mozjpeg/jdhuff.h index 6a8d90f4027..cfa0b7f5588 100644 --- a/third-party/mozjpeg/mozjpeg/jdhuff.h +++ b/third-party/mozjpeg/mozjpeg/jdhuff.h @@ -4,7 +4,8 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2010-2011, 2015-2016, D. R. Commander. + * Copyright (C) 2010-2011, 2015-2016, 2021, D. R. Commander. + * Copyright (C) 2018, Matthias Räncker. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -78,6 +79,11 @@ EXTERN(void) jpeg_make_d_derived_tbl(j_decompress_ptr cinfo, boolean isDC, typedef size_t bit_buf_type; /* type of bit-extraction buffer */ #define BIT_BUF_SIZE 64 /* size of buffer in bits */ +#elif defined(__x86_64__) && defined(__ILP32__) + +typedef unsigned long long bit_buf_type; /* type of bit-extraction buffer */ +#define BIT_BUF_SIZE 64 /* size of buffer in bits */ + #else typedef unsigned long bit_buf_type; /* type of bit-extraction buffer */ @@ -228,7 +234,10 @@ slowlabel: \ s |= GET_BITS(1); \ nb++; \ } \ - s = htbl->pub->huffval[(int)(s + htbl->valoffset[nb]) & 0xFF]; \ + if (nb > 16) \ + s = 0; \ + else \ + s = htbl->pub->huffval[(int)(s + htbl->valoffset[nb]) & 0xFF]; \ } /* Out-of-line case for Huffman code fetching */ diff --git a/third-party/mozjpeg/mozjpeg/jdicc.c b/third-party/mozjpeg/mozjpeg/jdicc.c index 7224695816b..50aa9a96767 100644 --- a/third-party/mozjpeg/mozjpeg/jdicc.c +++ b/third-party/mozjpeg/mozjpeg/jdicc.c @@ -18,10 +18,6 @@ #include "jpeglib.h" #include "jerror.h" -#ifndef HAVE_STDLIB_H /* should declare malloc() */ -extern void *malloc(size_t size); -#endif - #define ICC_MARKER (JPEG_APP0 + 2) /* JPEG marker code for ICC */ #define ICC_OVERHEAD_LEN 14 /* size of non-profile data in APP2 */ @@ -38,18 +34,18 @@ marker_is_icc(jpeg_saved_marker_ptr marker) marker->marker == ICC_MARKER && marker->data_length >= ICC_OVERHEAD_LEN && /* verify the identifying string */ - GETJOCTET(marker->data[0]) == 0x49 && - GETJOCTET(marker->data[1]) == 0x43 && - GETJOCTET(marker->data[2]) == 0x43 && - GETJOCTET(marker->data[3]) == 0x5F && - GETJOCTET(marker->data[4]) == 0x50 && - GETJOCTET(marker->data[5]) == 0x52 && - GETJOCTET(marker->data[6]) == 0x4F && - GETJOCTET(marker->data[7]) == 0x46 && - GETJOCTET(marker->data[8]) == 0x49 && - GETJOCTET(marker->data[9]) == 0x4C && - GETJOCTET(marker->data[10]) == 0x45 && - GETJOCTET(marker->data[11]) == 0x0; + marker->data[0] == 0x49 && + marker->data[1] == 0x43 && + marker->data[2] == 0x43 && + marker->data[3] == 0x5F && + marker->data[4] == 0x50 && + marker->data[5] == 0x52 && + marker->data[6] == 0x4F && + marker->data[7] == 0x46 && + marker->data[8] == 0x49 && + marker->data[9] == 0x4C && + marker->data[10] == 0x45 && + marker->data[11] == 0x0; } @@ -102,12 +98,12 @@ jpeg_read_icc_profile(j_decompress_ptr cinfo, JOCTET **icc_data_ptr, for (marker = cinfo->marker_list; marker != NULL; marker = marker->next) { if (marker_is_icc(marker)) { if (num_markers == 0) - num_markers = GETJOCTET(marker->data[13]); - else if (num_markers != GETJOCTET(marker->data[13])) { + num_markers = marker->data[13]; + else if (num_markers != marker->data[13]) { WARNMS(cinfo, JWRN_BOGUS_ICC); /* inconsistent num_markers fields */ return FALSE; } - seq_no = GETJOCTET(marker->data[12]); + seq_no = marker->data[12]; if (seq_no <= 0 || seq_no > num_markers) { WARNMS(cinfo, JWRN_BOGUS_ICC); /* bogus sequence number */ return FALSE; @@ -154,7 +150,7 @@ jpeg_read_icc_profile(j_decompress_ptr cinfo, JOCTET **icc_data_ptr, JOCTET FAR *src_ptr; JOCTET *dst_ptr; unsigned int length; - seq_no = GETJOCTET(marker->data[12]); + seq_no = marker->data[12]; dst_ptr = icc_data + data_offset[seq_no]; src_ptr = marker->data + ICC_OVERHEAD_LEN; length = data_length[seq_no]; diff --git a/third-party/mozjpeg/mozjpeg/jdinput.c b/third-party/mozjpeg/mozjpeg/jdinput.c index deec618f268..1bc5aff1a70 100644 --- a/third-party/mozjpeg/mozjpeg/jdinput.c +++ b/third-party/mozjpeg/mozjpeg/jdinput.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2010, 2016, 2018, D. R. Commander. + * Copyright (C) 2010, 2016, 2018, 2022, D. R. Commander. * Copyright (C) 2015, Google, Inc. * For conditions of distribution and use, see the accompanying README.ijg * file. @@ -264,7 +264,7 @@ latch_quant_tables(j_decompress_ptr cinfo) qtbl = (JQUANT_TBL *) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, sizeof(JQUANT_TBL)); - MEMCOPY(qtbl, cinfo->quant_tbl_ptrs[qtblno], sizeof(JQUANT_TBL)); + memcpy(qtbl, cinfo->quant_tbl_ptrs[qtblno], sizeof(JQUANT_TBL)); compptr->quant_table = qtbl; } } diff --git a/third-party/mozjpeg/mozjpeg/jdmainct.c b/third-party/mozjpeg/mozjpeg/jdmainct.c index 50301d6b50d..d332e6b2fa1 100644 --- a/third-party/mozjpeg/mozjpeg/jdmainct.c +++ b/third-party/mozjpeg/mozjpeg/jdmainct.c @@ -360,7 +360,7 @@ process_data_context_main(j_decompress_ptr cinfo, JSAMPARRAY output_buf, main_ptr->context_state = CTX_PREPARE_FOR_IMCU; if (*out_row_ctr >= out_rows_avail) return; /* Postprocessor exactly filled output buf */ - /*FALLTHROUGH*/ + FALLTHROUGH /*FALLTHROUGH*/ case CTX_PREPARE_FOR_IMCU: /* Prepare to process first M-1 row groups of this iMCU row */ main_ptr->rowgroup_ctr = 0; @@ -371,7 +371,7 @@ process_data_context_main(j_decompress_ptr cinfo, JSAMPARRAY output_buf, if (main_ptr->iMCU_row_ctr == cinfo->total_iMCU_rows) set_bottom_pointers(cinfo); main_ptr->context_state = CTX_PROCESS_IMCU; - /*FALLTHROUGH*/ + FALLTHROUGH /*FALLTHROUGH*/ case CTX_PROCESS_IMCU: /* Call postprocessor using previously set pointers */ (*cinfo->post->post_process_data) (cinfo, diff --git a/third-party/mozjpeg/mozjpeg/jdmarker.c b/third-party/mozjpeg/mozjpeg/jdmarker.c index c9c7ef63994..f7eba615fd5 100644 --- a/third-party/mozjpeg/mozjpeg/jdmarker.c +++ b/third-party/mozjpeg/mozjpeg/jdmarker.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1998, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2012, 2015, D. R. Commander. + * Copyright (C) 2012, 2015, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -151,7 +151,7 @@ typedef my_marker_reader *my_marker_ptr; #define INPUT_BYTE(cinfo, V, action) \ MAKESTMT( MAKE_BYTE_AVAIL(cinfo, action); \ bytes_in_buffer--; \ - V = GETJOCTET(*next_input_byte++); ) + V = *next_input_byte++; ) /* As above, but read two bytes interpreted as an unsigned 16-bit integer. * V should be declared unsigned int or perhaps JLONG. @@ -159,10 +159,10 @@ typedef my_marker_reader *my_marker_ptr; #define INPUT_2BYTES(cinfo, V, action) \ MAKESTMT( MAKE_BYTE_AVAIL(cinfo, action); \ bytes_in_buffer--; \ - V = ((unsigned int)GETJOCTET(*next_input_byte++)) << 8; \ + V = ((unsigned int)(*next_input_byte++)) << 8; \ MAKE_BYTE_AVAIL(cinfo, action); \ bytes_in_buffer--; \ - V += GETJOCTET(*next_input_byte++); ) + V += *next_input_byte++; ) /* @@ -473,7 +473,7 @@ get_dht(j_decompress_ptr cinfo) for (i = 0; i < count; i++) INPUT_BYTE(cinfo, huffval[i], return FALSE); - MEMZERO(&huffval[count], (256 - count) * sizeof(UINT8)); + memset(&huffval[count], 0, (256 - count) * sizeof(UINT8)); length -= count; @@ -491,8 +491,8 @@ get_dht(j_decompress_ptr cinfo) if (*htblptr == NULL) *htblptr = jpeg_alloc_huff_table((j_common_ptr)cinfo); - MEMCOPY((*htblptr)->bits, bits, sizeof((*htblptr)->bits)); - MEMCOPY((*htblptr)->huffval, huffval, sizeof((*htblptr)->huffval)); + memcpy((*htblptr)->bits, bits, sizeof((*htblptr)->bits)); + memcpy((*htblptr)->huffval, huffval, sizeof((*htblptr)->huffval)); } if (length != 0) @@ -608,18 +608,18 @@ examine_app0(j_decompress_ptr cinfo, JOCTET *data, unsigned int datalen, JLONG totallen = (JLONG)datalen + remaining; if (datalen >= APP0_DATA_LEN && - GETJOCTET(data[0]) == 0x4A && - GETJOCTET(data[1]) == 0x46 && - GETJOCTET(data[2]) == 0x49 && - GETJOCTET(data[3]) == 0x46 && - GETJOCTET(data[4]) == 0) { + data[0] == 0x4A && + data[1] == 0x46 && + data[2] == 0x49 && + data[3] == 0x46 && + data[4] == 0) { /* Found JFIF APP0 marker: save info */ cinfo->saw_JFIF_marker = TRUE; - cinfo->JFIF_major_version = GETJOCTET(data[5]); - cinfo->JFIF_minor_version = GETJOCTET(data[6]); - cinfo->density_unit = GETJOCTET(data[7]); - cinfo->X_density = (GETJOCTET(data[8]) << 8) + GETJOCTET(data[9]); - cinfo->Y_density = (GETJOCTET(data[10]) << 8) + GETJOCTET(data[11]); + cinfo->JFIF_major_version = data[5]; + cinfo->JFIF_minor_version = data[6]; + cinfo->density_unit = data[7]; + cinfo->X_density = (data[8] << 8) + data[9]; + cinfo->Y_density = (data[10] << 8) + data[11]; /* Check version. * Major version must be 1, anything else signals an incompatible change. * (We used to treat this as an error, but now it's a nonfatal warning, @@ -634,24 +634,22 @@ examine_app0(j_decompress_ptr cinfo, JOCTET *data, unsigned int datalen, cinfo->JFIF_major_version, cinfo->JFIF_minor_version, cinfo->X_density, cinfo->Y_density, cinfo->density_unit); /* Validate thumbnail dimensions and issue appropriate messages */ - if (GETJOCTET(data[12]) | GETJOCTET(data[13])) - TRACEMS2(cinfo, 1, JTRC_JFIF_THUMBNAIL, - GETJOCTET(data[12]), GETJOCTET(data[13])); + if (data[12] | data[13]) + TRACEMS2(cinfo, 1, JTRC_JFIF_THUMBNAIL, data[12], data[13]); totallen -= APP0_DATA_LEN; - if (totallen != - ((JLONG)GETJOCTET(data[12]) * (JLONG)GETJOCTET(data[13]) * (JLONG)3)) + if (totallen != ((JLONG)data[12] * (JLONG)data[13] * (JLONG)3)) TRACEMS1(cinfo, 1, JTRC_JFIF_BADTHUMBNAILSIZE, (int)totallen); } else if (datalen >= 6 && - GETJOCTET(data[0]) == 0x4A && - GETJOCTET(data[1]) == 0x46 && - GETJOCTET(data[2]) == 0x58 && - GETJOCTET(data[3]) == 0x58 && - GETJOCTET(data[4]) == 0) { + data[0] == 0x4A && + data[1] == 0x46 && + data[2] == 0x58 && + data[3] == 0x58 && + data[4] == 0) { /* Found JFIF "JFXX" extension APP0 marker */ /* The library doesn't actually do anything with these, * but we try to produce a helpful trace message. */ - switch (GETJOCTET(data[5])) { + switch (data[5]) { case 0x10: TRACEMS1(cinfo, 1, JTRC_THUMB_JPEG, (int)totallen); break; @@ -662,8 +660,7 @@ examine_app0(j_decompress_ptr cinfo, JOCTET *data, unsigned int datalen, TRACEMS1(cinfo, 1, JTRC_THUMB_RGB, (int)totallen); break; default: - TRACEMS2(cinfo, 1, JTRC_JFIF_EXTENSION, - GETJOCTET(data[5]), (int)totallen); + TRACEMS2(cinfo, 1, JTRC_JFIF_EXTENSION, data[5], (int)totallen); break; } } else { @@ -684,16 +681,16 @@ examine_app14(j_decompress_ptr cinfo, JOCTET *data, unsigned int datalen, unsigned int version, flags0, flags1, transform; if (datalen >= APP14_DATA_LEN && - GETJOCTET(data[0]) == 0x41 && - GETJOCTET(data[1]) == 0x64 && - GETJOCTET(data[2]) == 0x6F && - GETJOCTET(data[3]) == 0x62 && - GETJOCTET(data[4]) == 0x65) { + data[0] == 0x41 && + data[1] == 0x64 && + data[2] == 0x6F && + data[3] == 0x62 && + data[4] == 0x65) { /* Found Adobe APP14 marker */ - version = (GETJOCTET(data[5]) << 8) + GETJOCTET(data[6]); - flags0 = (GETJOCTET(data[7]) << 8) + GETJOCTET(data[8]); - flags1 = (GETJOCTET(data[9]) << 8) + GETJOCTET(data[10]); - transform = GETJOCTET(data[11]); + version = (data[5] << 8) + data[6]; + flags0 = (data[7] << 8) + data[8]; + flags1 = (data[9] << 8) + data[10]; + transform = data[11]; TRACEMS4(cinfo, 1, JTRC_ADOBE, version, flags0, flags1, transform); cinfo->saw_Adobe_marker = TRUE; cinfo->Adobe_transform = (UINT8)transform; diff --git a/third-party/mozjpeg/mozjpeg/jdmaster.c b/third-party/mozjpeg/mozjpeg/jdmaster.c index b20906438e4..a9446adfdff 100644 --- a/third-party/mozjpeg/mozjpeg/jdmaster.c +++ b/third-party/mozjpeg/mozjpeg/jdmaster.c @@ -5,7 +5,7 @@ * Copyright (C) 1991-1997, Thomas G. Lane. * Modified 2002-2009 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2009-2011, 2016, D. R. Commander. + * Copyright (C) 2009-2011, 2016, 2019, 2022-2023, D. R. Commander. * Copyright (C) 2013, Linaro Limited. * Copyright (C) 2015, Google, Inc. * For conditions of distribution and use, see the accompanying README.ijg @@ -22,7 +22,6 @@ #include "jpeglib.h" #include "jpegcomp.h" #include "jdmaster.h" -#include "jsimd.h" /* @@ -70,17 +69,6 @@ use_merged_upsample(j_decompress_ptr cinfo) cinfo->comp_info[1]._DCT_scaled_size != cinfo->_min_DCT_scaled_size || cinfo->comp_info[2]._DCT_scaled_size != cinfo->_min_DCT_scaled_size) return FALSE; -#ifdef WITH_SIMD - /* If YCbCr-to-RGB color conversion is SIMD-accelerated but merged upsampling - isn't, then disabling merged upsampling is likely to be faster when - decompressing YCbCr JPEG images. */ - if (!jsimd_can_h2v2_merged_upsample() && !jsimd_can_h2v1_merged_upsample() && - jsimd_can_ycc_rgb() && cinfo->jpeg_color_space == JCS_YCbCr && - (cinfo->out_color_space == JCS_RGB || - (cinfo->out_color_space >= JCS_EXT_RGB && - cinfo->out_color_space <= JCS_EXT_ARGB))) - return FALSE; -#endif /* ??? also need to test for upsample-time rescaling, when & if supported */ return TRUE; /* by golly, it'll work... */ #else @@ -429,7 +417,7 @@ prepare_range_limit_table(j_decompress_ptr cinfo) table += (MAXJSAMPLE + 1); /* allow negative subscripts of simple table */ cinfo->sample_range_limit = table; /* First segment of "simple" table: limit[x] = 0 for x < 0 */ - MEMZERO(table - (MAXJSAMPLE + 1), (MAXJSAMPLE + 1) * sizeof(JSAMPLE)); + memset(table - (MAXJSAMPLE + 1), 0, (MAXJSAMPLE + 1) * sizeof(JSAMPLE)); /* Main part of "simple" table: limit[x] = x */ for (i = 0; i <= MAXJSAMPLE; i++) table[i] = (JSAMPLE)i; @@ -438,10 +426,10 @@ prepare_range_limit_table(j_decompress_ptr cinfo) for (i = CENTERJSAMPLE; i < 2 * (MAXJSAMPLE + 1); i++) table[i] = MAXJSAMPLE; /* Second half of post-IDCT table */ - MEMZERO(table + (2 * (MAXJSAMPLE + 1)), - (2 * (MAXJSAMPLE + 1) - CENTERJSAMPLE) * sizeof(JSAMPLE)); - MEMCOPY(table + (4 * (MAXJSAMPLE + 1) - CENTERJSAMPLE), - cinfo->sample_range_limit, CENTERJSAMPLE * sizeof(JSAMPLE)); + memset(table + (2 * (MAXJSAMPLE + 1)), 0, + (2 * (MAXJSAMPLE + 1) - CENTERJSAMPLE) * sizeof(JSAMPLE)); + memcpy(table + (4 * (MAXJSAMPLE + 1) - CENTERJSAMPLE), + cinfo->sample_range_limit, CENTERJSAMPLE * sizeof(JSAMPLE)); } @@ -492,7 +480,8 @@ master_selection(j_decompress_ptr cinfo) if (cinfo->raw_data_out) ERREXIT(cinfo, JERR_NOTIMPL); /* 2-pass quantizer only works in 3-component color space. */ - if (cinfo->out_color_components != 3) { + if (cinfo->out_color_components != 3 || + cinfo->out_color_space == JCS_RGB565) { cinfo->enable_1pass_quant = TRUE; cinfo->enable_external_quant = FALSE; cinfo->enable_2pass_quant = FALSE; @@ -580,6 +569,7 @@ master_selection(j_decompress_ptr cinfo) */ cinfo->master->first_iMCU_col = 0; cinfo->master->last_iMCU_col = cinfo->MCUs_per_row - 1; + cinfo->master->last_good_iMCU_row = 0; #ifdef D_MULTISCAN_FILES_SUPPORTED /* If jpeg_start_decompress will read the whole file, initialize diff --git a/third-party/mozjpeg/mozjpeg/jdmerge.c b/third-party/mozjpeg/mozjpeg/jdmerge.c index dff5a350870..38b002729c1 100644 --- a/third-party/mozjpeg/mozjpeg/jdmerge.c +++ b/third-party/mozjpeg/mozjpeg/jdmerge.c @@ -5,7 +5,7 @@ * Copyright (C) 1994-1996, Thomas G. Lane. * libjpeg-turbo Modifications: * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009, 2011, 2014-2015, D. R. Commander. + * Copyright (C) 2009, 2011, 2014-2015, 2020, D. R. Commander. * Copyright (C) 2013, Linaro Limited. * For conditions of distribution and use, see the accompanying README.ijg * file. @@ -40,41 +40,12 @@ #define JPEG_INTERNALS #include "jinclude.h" #include "jpeglib.h" +#include "jdmerge.h" #include "jsimd.h" -#include "jconfigint.h" #ifdef UPSAMPLE_MERGING_SUPPORTED -/* Private subobject */ - -typedef struct { - struct jpeg_upsampler pub; /* public fields */ - - /* Pointer to routine to do actual upsampling/conversion of one row group */ - void (*upmethod) (j_decompress_ptr cinfo, JSAMPIMAGE input_buf, - JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf); - - /* Private state for YCC->RGB conversion */ - int *Cr_r_tab; /* => table for Cr to R conversion */ - int *Cb_b_tab; /* => table for Cb to B conversion */ - JLONG *Cr_g_tab; /* => table for Cr to G conversion */ - JLONG *Cb_g_tab; /* => table for Cb to G conversion */ - - /* For 2:1 vertical sampling, we produce two output rows at a time. - * We need a "spare" row buffer to hold the second output row if the - * application provides just a one-row buffer; we also use the spare - * to discard the dummy last row if the image height is odd. - */ - JSAMPROW spare_row; - boolean spare_full; /* T if spare buffer is occupied */ - - JDIMENSION out_row_width; /* samples per output row */ - JDIMENSION rows_to_go; /* counts rows remaining in image */ -} my_upsampler; - -typedef my_upsampler *my_upsample_ptr; - #define SCALEBITS 16 /* speediest right-shift on some machines */ #define ONE_HALF ((JLONG)1 << (SCALEBITS - 1)) #define FIX(x) ((JLONG)((x) * (1L << SCALEBITS) + 0.5)) @@ -189,7 +160,7 @@ typedef my_upsampler *my_upsample_ptr; LOCAL(void) build_ycc_rgb_table(j_decompress_ptr cinfo) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; int i; JLONG x; SHIFT_TEMPS @@ -232,7 +203,7 @@ build_ycc_rgb_table(j_decompress_ptr cinfo) METHODDEF(void) start_pass_merged_upsample(j_decompress_ptr cinfo) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; /* Mark the spare buffer empty */ upsample->spare_full = FALSE; @@ -254,7 +225,7 @@ merged_2v_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION *out_row_ctr, JDIMENSION out_rows_avail) /* 2:1 vertical sampling case: may need a spare row. */ { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; JSAMPROW work_ptrs[2]; JDIMENSION num_rows; /* number of rows returned to caller */ @@ -305,7 +276,7 @@ merged_1v_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION *out_row_ctr, JDIMENSION out_rows_avail) /* 1:1 vertical sampling case: much easier, never need a spare row. */ { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; /* Just do the upsampling. */ (*upsample->upmethod) (cinfo, input_buf, *in_row_group_ctr, @@ -420,11 +391,10 @@ h2v2_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, * RGB565 conversion */ -#define PACK_SHORT_565_LE(r, g, b) ((((r) << 8) & 0xF800) | \ - (((g) << 3) & 0x7E0) | ((b) >> 3)) -#define PACK_SHORT_565_BE(r, g, b) (((r) & 0xF8) | ((g) >> 5) | \ - (((g) << 11) & 0xE000) | \ - (((b) << 5) & 0x1F00)) +#define PACK_SHORT_565_LE(r, g, b) \ + ((((r) << 8) & 0xF800) | (((g) << 3) & 0x7E0) | ((b) >> 3)) +#define PACK_SHORT_565_BE(r, g, b) \ + (((r) & 0xF8) | ((g) >> 5) | (((g) << 11) & 0xE000) | (((b) << 5) & 0x1F00)) #define PACK_TWO_PIXELS_LE(l, r) ((r << 16) | l) #define PACK_TWO_PIXELS_BE(l, r) ((l << 16) | r) @@ -566,11 +536,11 @@ h2v2_merged_upsample_565D(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, GLOBAL(void) jinit_merged_upsampler(j_decompress_ptr cinfo) { - my_upsample_ptr upsample; + my_merged_upsample_ptr upsample; - upsample = (my_upsample_ptr) + upsample = (my_merged_upsample_ptr) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, - sizeof(my_upsampler)); + sizeof(my_merged_upsampler)); cinfo->upsample = (struct jpeg_upsampler *)upsample; upsample->pub.start_pass = start_pass_merged_upsample; upsample->pub.need_context_rows = FALSE; diff --git a/third-party/mozjpeg/mozjpeg/jdmerge.h b/third-party/mozjpeg/mozjpeg/jdmerge.h new file mode 100644 index 00000000000..b583396b106 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/jdmerge.h @@ -0,0 +1,47 @@ +/* + * jdmerge.h + * + * This file was part of the Independent JPEG Group's software: + * Copyright (C) 1994-1996, Thomas G. Lane. + * libjpeg-turbo Modifications: + * Copyright (C) 2020, D. R. Commander. + * For conditions of distribution and use, see the accompanying README.ijg + * file. + */ + +#define JPEG_INTERNALS +#include "jpeglib.h" + +#ifdef UPSAMPLE_MERGING_SUPPORTED + + +/* Private subobject */ + +typedef struct { + struct jpeg_upsampler pub; /* public fields */ + + /* Pointer to routine to do actual upsampling/conversion of one row group */ + void (*upmethod) (j_decompress_ptr cinfo, JSAMPIMAGE input_buf, + JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf); + + /* Private state for YCC->RGB conversion */ + int *Cr_r_tab; /* => table for Cr to R conversion */ + int *Cb_b_tab; /* => table for Cb to B conversion */ + JLONG *Cr_g_tab; /* => table for Cr to G conversion */ + JLONG *Cb_g_tab; /* => table for Cb to G conversion */ + + /* For 2:1 vertical sampling, we produce two output rows at a time. + * We need a "spare" row buffer to hold the second output row if the + * application provides just a one-row buffer; we also use the spare + * to discard the dummy last row if the image height is odd. + */ + JSAMPROW spare_row; + boolean spare_full; /* T if spare buffer is occupied */ + + JDIMENSION out_row_width; /* samples per output row */ + JDIMENSION rows_to_go; /* counts rows remaining in image */ +} my_merged_upsampler; + +typedef my_merged_upsampler *my_merged_upsample_ptr; + +#endif /* UPSAMPLE_MERGING_SUPPORTED */ diff --git a/third-party/mozjpeg/mozjpeg/jdmrg565.c b/third-party/mozjpeg/mozjpeg/jdmrg565.c index 1b87e3718d3..980a4e216e4 100644 --- a/third-party/mozjpeg/mozjpeg/jdmrg565.c +++ b/third-party/mozjpeg/mozjpeg/jdmrg565.c @@ -5,7 +5,7 @@ * Copyright (C) 1994-1996, Thomas G. Lane. * libjpeg-turbo Modifications: * Copyright (C) 2013, Linaro Limited. - * Copyright (C) 2014-2015, 2018, D. R. Commander. + * Copyright (C) 2014-2015, 2018, 2020, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -19,7 +19,7 @@ h2v1_merged_upsample_565_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; register int y, cred, cgreen, cblue; int cb, cr; register JSAMPROW outptr; @@ -43,20 +43,20 @@ h2v1_merged_upsample_565_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, /* Loop for each pair of output pixels */ for (col = cinfo->output_width >> 1; col > 0; col--) { /* Do the chroma part of the calculation */ - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + cb = *inptr1++; + cr = *inptr2++; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; /* Fetch 2 Y values and emit 2 pixels */ - y = GETJSAMPLE(*inptr0++); + y = *inptr0++; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr0++); + y = *inptr0++; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; @@ -68,12 +68,12 @@ h2v1_merged_upsample_565_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, /* If image width is odd, do the last output column separately */ if (cinfo->output_width & 1) { - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + cb = *inptr1; + cr = *inptr2; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; - y = GETJSAMPLE(*inptr0); + y = *inptr0; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; @@ -90,7 +90,7 @@ h2v1_merged_upsample_565D_internal(j_decompress_ptr cinfo, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; register int y, cred, cgreen, cblue; int cb, cr; register JSAMPROW outptr; @@ -115,21 +115,21 @@ h2v1_merged_upsample_565D_internal(j_decompress_ptr cinfo, /* Loop for each pair of output pixels */ for (col = cinfo->output_width >> 1; col > 0; col--) { /* Do the chroma part of the calculation */ - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + cb = *inptr1++; + cr = *inptr2++; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; /* Fetch 2 Y values and emit 2 pixels */ - y = GETJSAMPLE(*inptr0++); + y = *inptr0++; r = range_limit[DITHER_565_R(y + cred, d0)]; g = range_limit[DITHER_565_G(y + cgreen, d0)]; b = range_limit[DITHER_565_B(y + cblue, d0)]; d0 = DITHER_ROTATE(d0); rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr0++); + y = *inptr0++; r = range_limit[DITHER_565_R(y + cred, d0)]; g = range_limit[DITHER_565_G(y + cgreen, d0)]; b = range_limit[DITHER_565_B(y + cblue, d0)]; @@ -142,12 +142,12 @@ h2v1_merged_upsample_565D_internal(j_decompress_ptr cinfo, /* If image width is odd, do the last output column separately */ if (cinfo->output_width & 1) { - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + cb = *inptr1; + cr = *inptr2; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; - y = GETJSAMPLE(*inptr0); + y = *inptr0; r = range_limit[DITHER_565_R(y + cred, d0)]; g = range_limit[DITHER_565_G(y + cgreen, d0)]; b = range_limit[DITHER_565_B(y + cblue, d0)]; @@ -163,7 +163,7 @@ h2v2_merged_upsample_565_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; register int y, cred, cgreen, cblue; int cb, cr; register JSAMPROW outptr0, outptr1; @@ -189,20 +189,20 @@ h2v2_merged_upsample_565_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, /* Loop for each group of output pixels */ for (col = cinfo->output_width >> 1; col > 0; col--) { /* Do the chroma part of the calculation */ - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + cb = *inptr1++; + cr = *inptr2++; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; /* Fetch 4 Y values and emit 4 pixels */ - y = GETJSAMPLE(*inptr00++); + y = *inptr00++; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr00++); + y = *inptr00++; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; @@ -211,13 +211,13 @@ h2v2_merged_upsample_565_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, WRITE_TWO_PIXELS(outptr0, rgb); outptr0 += 4; - y = GETJSAMPLE(*inptr01++); + y = *inptr01++; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr01++); + y = *inptr01++; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; @@ -229,20 +229,20 @@ h2v2_merged_upsample_565_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, /* If image width is odd, do the last output column separately */ if (cinfo->output_width & 1) { - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + cb = *inptr1; + cr = *inptr2; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; - y = GETJSAMPLE(*inptr00); + y = *inptr00; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; rgb = PACK_SHORT_565(r, g, b); *(INT16 *)outptr0 = (INT16)rgb; - y = GETJSAMPLE(*inptr01); + y = *inptr01; r = range_limit[y + cred]; g = range_limit[y + cgreen]; b = range_limit[y + cblue]; @@ -259,7 +259,7 @@ h2v2_merged_upsample_565D_internal(j_decompress_ptr cinfo, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; register int y, cred, cgreen, cblue; int cb, cr; register JSAMPROW outptr0, outptr1; @@ -287,21 +287,21 @@ h2v2_merged_upsample_565D_internal(j_decompress_ptr cinfo, /* Loop for each group of output pixels */ for (col = cinfo->output_width >> 1; col > 0; col--) { /* Do the chroma part of the calculation */ - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + cb = *inptr1++; + cr = *inptr2++; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; /* Fetch 4 Y values and emit 4 pixels */ - y = GETJSAMPLE(*inptr00++); + y = *inptr00++; r = range_limit[DITHER_565_R(y + cred, d0)]; g = range_limit[DITHER_565_G(y + cgreen, d0)]; b = range_limit[DITHER_565_B(y + cblue, d0)]; d0 = DITHER_ROTATE(d0); rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr00++); + y = *inptr00++; r = range_limit[DITHER_565_R(y + cred, d0)]; g = range_limit[DITHER_565_G(y + cgreen, d0)]; b = range_limit[DITHER_565_B(y + cblue, d0)]; @@ -311,14 +311,14 @@ h2v2_merged_upsample_565D_internal(j_decompress_ptr cinfo, WRITE_TWO_PIXELS(outptr0, rgb); outptr0 += 4; - y = GETJSAMPLE(*inptr01++); + y = *inptr01++; r = range_limit[DITHER_565_R(y + cred, d1)]; g = range_limit[DITHER_565_G(y + cgreen, d1)]; b = range_limit[DITHER_565_B(y + cblue, d1)]; d1 = DITHER_ROTATE(d1); rgb = PACK_SHORT_565(r, g, b); - y = GETJSAMPLE(*inptr01++); + y = *inptr01++; r = range_limit[DITHER_565_R(y + cred, d1)]; g = range_limit[DITHER_565_G(y + cgreen, d1)]; b = range_limit[DITHER_565_B(y + cblue, d1)]; @@ -331,20 +331,20 @@ h2v2_merged_upsample_565D_internal(j_decompress_ptr cinfo, /* If image width is odd, do the last output column separately */ if (cinfo->output_width & 1) { - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + cb = *inptr1; + cr = *inptr2; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; - y = GETJSAMPLE(*inptr00); + y = *inptr00; r = range_limit[DITHER_565_R(y + cred, d0)]; g = range_limit[DITHER_565_G(y + cgreen, d0)]; b = range_limit[DITHER_565_B(y + cblue, d0)]; rgb = PACK_SHORT_565(r, g, b); *(INT16 *)outptr0 = (INT16)rgb; - y = GETJSAMPLE(*inptr01); + y = *inptr01; r = range_limit[DITHER_565_R(y + cred, d1)]; g = range_limit[DITHER_565_G(y + cgreen, d1)]; b = range_limit[DITHER_565_B(y + cblue, d1)]; diff --git a/third-party/mozjpeg/mozjpeg/jdmrgext.c b/third-party/mozjpeg/mozjpeg/jdmrgext.c index b1c27df56a4..038abc75d7e 100644 --- a/third-party/mozjpeg/mozjpeg/jdmrgext.c +++ b/third-party/mozjpeg/mozjpeg/jdmrgext.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1994-1996, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2011, 2015, D. R. Commander. + * Copyright (C) 2011, 2015, 2020, 2023, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -25,7 +25,7 @@ h2v1_merged_upsample_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; register int y, cred, cgreen, cblue; int cb, cr; register JSAMPROW outptr; @@ -46,42 +46,42 @@ h2v1_merged_upsample_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, /* Loop for each pair of output pixels */ for (col = cinfo->output_width >> 1; col > 0; col--) { /* Do the chroma part of the calculation */ - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + cb = *inptr1++; + cr = *inptr2++; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; /* Fetch 2 Y values and emit 2 pixels */ - y = GETJSAMPLE(*inptr0++); + y = *inptr0++; outptr[RGB_RED] = range_limit[y + cred]; outptr[RGB_GREEN] = range_limit[y + cgreen]; outptr[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr[RGB_ALPHA] = 0xFF; + outptr[RGB_ALPHA] = MAXJSAMPLE; #endif outptr += RGB_PIXELSIZE; - y = GETJSAMPLE(*inptr0++); + y = *inptr0++; outptr[RGB_RED] = range_limit[y + cred]; outptr[RGB_GREEN] = range_limit[y + cgreen]; outptr[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr[RGB_ALPHA] = 0xFF; + outptr[RGB_ALPHA] = MAXJSAMPLE; #endif outptr += RGB_PIXELSIZE; } /* If image width is odd, do the last output column separately */ if (cinfo->output_width & 1) { - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + cb = *inptr1; + cr = *inptr2; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; - y = GETJSAMPLE(*inptr0); + y = *inptr0; outptr[RGB_RED] = range_limit[y + cred]; outptr[RGB_GREEN] = range_limit[y + cgreen]; outptr[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr[RGB_ALPHA] = 0xFF; + outptr[RGB_ALPHA] = MAXJSAMPLE; #endif } } @@ -97,7 +97,7 @@ h2v2_merged_upsample_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { - my_upsample_ptr upsample = (my_upsample_ptr)cinfo->upsample; + my_merged_upsample_ptr upsample = (my_merged_upsample_ptr)cinfo->upsample; register int y, cred, cgreen, cblue; int cb, cr; register JSAMPROW outptr0, outptr1; @@ -120,65 +120,65 @@ h2v2_merged_upsample_internal(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, /* Loop for each group of output pixels */ for (col = cinfo->output_width >> 1; col > 0; col--) { /* Do the chroma part of the calculation */ - cb = GETJSAMPLE(*inptr1++); - cr = GETJSAMPLE(*inptr2++); + cb = *inptr1++; + cr = *inptr2++; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; /* Fetch 4 Y values and emit 4 pixels */ - y = GETJSAMPLE(*inptr00++); + y = *inptr00++; outptr0[RGB_RED] = range_limit[y + cred]; outptr0[RGB_GREEN] = range_limit[y + cgreen]; outptr0[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr0[RGB_ALPHA] = 0xFF; + outptr0[RGB_ALPHA] = MAXJSAMPLE; #endif outptr0 += RGB_PIXELSIZE; - y = GETJSAMPLE(*inptr00++); + y = *inptr00++; outptr0[RGB_RED] = range_limit[y + cred]; outptr0[RGB_GREEN] = range_limit[y + cgreen]; outptr0[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr0[RGB_ALPHA] = 0xFF; + outptr0[RGB_ALPHA] = MAXJSAMPLE; #endif outptr0 += RGB_PIXELSIZE; - y = GETJSAMPLE(*inptr01++); + y = *inptr01++; outptr1[RGB_RED] = range_limit[y + cred]; outptr1[RGB_GREEN] = range_limit[y + cgreen]; outptr1[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr1[RGB_ALPHA] = 0xFF; + outptr1[RGB_ALPHA] = MAXJSAMPLE; #endif outptr1 += RGB_PIXELSIZE; - y = GETJSAMPLE(*inptr01++); + y = *inptr01++; outptr1[RGB_RED] = range_limit[y + cred]; outptr1[RGB_GREEN] = range_limit[y + cgreen]; outptr1[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr1[RGB_ALPHA] = 0xFF; + outptr1[RGB_ALPHA] = MAXJSAMPLE; #endif outptr1 += RGB_PIXELSIZE; } /* If image width is odd, do the last output column separately */ if (cinfo->output_width & 1) { - cb = GETJSAMPLE(*inptr1); - cr = GETJSAMPLE(*inptr2); + cb = *inptr1; + cr = *inptr2; cred = Crrtab[cr]; cgreen = (int)RIGHT_SHIFT(Cbgtab[cb] + Crgtab[cr], SCALEBITS); cblue = Cbbtab[cb]; - y = GETJSAMPLE(*inptr00); + y = *inptr00; outptr0[RGB_RED] = range_limit[y + cred]; outptr0[RGB_GREEN] = range_limit[y + cgreen]; outptr0[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr0[RGB_ALPHA] = 0xFF; + outptr0[RGB_ALPHA] = MAXJSAMPLE; #endif - y = GETJSAMPLE(*inptr01); + y = *inptr01; outptr1[RGB_RED] = range_limit[y + cred]; outptr1[RGB_GREEN] = range_limit[y + cgreen]; outptr1[RGB_BLUE] = range_limit[y + cblue]; #ifdef RGB_ALPHA - outptr1[RGB_ALPHA] = 0xFF; + outptr1[RGB_ALPHA] = MAXJSAMPLE; #endif } } diff --git a/third-party/mozjpeg/mozjpeg/jdphuff.c b/third-party/mozjpeg/mozjpeg/jdphuff.c index 9e82636bbd1..9680ebcbd06 100644 --- a/third-party/mozjpeg/mozjpeg/jdphuff.c +++ b/third-party/mozjpeg/mozjpeg/jdphuff.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1995-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2015-2016, 2018, D. R. Commander. + * Copyright (C) 2015-2016, 2018-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -41,25 +41,6 @@ typedef struct { int last_dc_val[MAX_COMPS_IN_SCAN]; /* last DC coef for each component */ } savable_state; -/* This macro is to work around compilers with missing or broken - * structure assignment. You'll need to fix this code if you have - * such a compiler and you change MAX_COMPS_IN_SCAN. - */ - -#ifndef NO_STRUCT_ASSIGN -#define ASSIGN_STATE(dest, src) ((dest) = (src)) -#else -#if MAX_COMPS_IN_SCAN == 4 -#define ASSIGN_STATE(dest, src) \ - ((dest).EOBRUN = (src).EOBRUN, \ - (dest).last_dc_val[0] = (src).last_dc_val[0], \ - (dest).last_dc_val[1] = (src).last_dc_val[1], \ - (dest).last_dc_val[2] = (src).last_dc_val[2], \ - (dest).last_dc_val[3] = (src).last_dc_val[3]) -#endif -#endif - - typedef struct { struct jpeg_entropy_decoder pub; /* public fields */ @@ -102,7 +83,7 @@ start_pass_phuff_decoder(j_decompress_ptr cinfo) boolean is_DC_band, bad; int ci, coefi, tbl; d_derived_tbl **pdtbl; - int *coef_bit_ptr; + int *coef_bit_ptr, *prev_coef_bit_ptr; jpeg_component_info *compptr; is_DC_band = (cinfo->Ss == 0); @@ -143,8 +124,15 @@ start_pass_phuff_decoder(j_decompress_ptr cinfo) for (ci = 0; ci < cinfo->comps_in_scan; ci++) { int cindex = cinfo->cur_comp_info[ci]->component_index; coef_bit_ptr = &cinfo->coef_bits[cindex][0]; + prev_coef_bit_ptr = &cinfo->coef_bits[cindex + cinfo->num_components][0]; if (!is_DC_band && coef_bit_ptr[0] < 0) /* AC without prior DC scan */ WARNMS2(cinfo, JWRN_BOGUS_PROGRESSION, cindex, 0); + for (coefi = MIN(cinfo->Ss, 1); coefi <= MAX(cinfo->Se, 9); coefi++) { + if (cinfo->input_scan_number > 1) + prev_coef_bit_ptr[coefi] = coef_bit_ptr[coefi]; + else + prev_coef_bit_ptr[coefi] = 0; + } for (coefi = cinfo->Ss; coefi <= cinfo->Se; coefi++) { int expected = (coef_bit_ptr[coefi] < 0) ? 0 : coef_bit_ptr[coefi]; if (cinfo->Ah != expected) @@ -323,7 +311,7 @@ decode_mcu_DC_first(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) /* Load up working state */ BITREAD_LOAD_STATE(cinfo, entropy->bitstate); - ASSIGN_STATE(state, entropy->saved); + state = entropy->saved; /* Outer loop handles each block in the MCU */ @@ -356,11 +344,12 @@ decode_mcu_DC_first(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) /* Completed MCU, so update state */ BITREAD_SAVE_STATE(cinfo, entropy->bitstate); - ASSIGN_STATE(entropy->saved, state); + entropy->saved = state; } /* Account for restart interval (no-op if not using restarts) */ - entropy->restarts_to_go--; + if (cinfo->restart_interval) + entropy->restarts_to_go--; return TRUE; } @@ -444,7 +433,8 @@ decode_mcu_AC_first(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) } /* Account for restart interval (no-op if not using restarts) */ - entropy->restarts_to_go--; + if (cinfo->restart_interval) + entropy->restarts_to_go--; return TRUE; } @@ -495,7 +485,8 @@ decode_mcu_DC_refine(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) BITREAD_SAVE_STATE(cinfo, entropy->bitstate); /* Account for restart interval (no-op if not using restarts) */ - entropy->restarts_to_go--; + if (cinfo->restart_interval) + entropy->restarts_to_go--; return TRUE; } @@ -587,9 +578,9 @@ decode_mcu_AC_refine(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) if (GET_BITS(1)) { if ((*thiscoef & p1) == 0) { /* do nothing if already set it */ if (*thiscoef >= 0) - *thiscoef += p1; + *thiscoef += (JCOEF)p1; else - *thiscoef += m1; + *thiscoef += (JCOEF)m1; } } } else { @@ -621,9 +612,9 @@ decode_mcu_AC_refine(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) if (GET_BITS(1)) { if ((*thiscoef & p1) == 0) { /* do nothing if already changed it */ if (*thiscoef >= 0) - *thiscoef += p1; + *thiscoef += (JCOEF)p1; else - *thiscoef += m1; + *thiscoef += (JCOEF)m1; } } } @@ -638,7 +629,8 @@ decode_mcu_AC_refine(j_decompress_ptr cinfo, JBLOCKROW *MCU_data) } /* Account for restart interval (no-op if not using restarts) */ - entropy->restarts_to_go--; + if (cinfo->restart_interval) + entropy->restarts_to_go--; return TRUE; @@ -676,7 +668,7 @@ jinit_phuff_decoder(j_decompress_ptr cinfo) /* Create progression status table */ cinfo->coef_bits = (int (*)[DCTSIZE2]) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, - cinfo->num_components * DCTSIZE2 * + cinfo->num_components * 2 * DCTSIZE2 * sizeof(int)); coef_bit_ptr = &cinfo->coef_bits[0][0]; for (ci = 0; ci < cinfo->num_components; ci++) diff --git a/third-party/mozjpeg/mozjpeg/jdsample.c b/third-party/mozjpeg/mozjpeg/jdsample.c index 50a68b30131..eaad72a0308 100644 --- a/third-party/mozjpeg/mozjpeg/jdsample.c +++ b/third-party/mozjpeg/mozjpeg/jdsample.c @@ -8,7 +8,7 @@ * Copyright (C) 2010, 2015-2016, D. R. Commander. * Copyright (C) 2014, MIPS Technologies, Inc., California. * Copyright (C) 2015, Google, Inc. - * Copyright (C) 2019, Arm Limited. + * Copyright (C) 2019-2020, Arm Limited. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -177,7 +177,7 @@ int_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, outptr = output_data[outrow]; outend = outptr + cinfo->output_width; while (outptr < outend) { - invalue = *inptr++; /* don't need GETJSAMPLE() here */ + invalue = *inptr++; for (h = h_expand; h > 0; h--) { *outptr++ = invalue; } @@ -213,7 +213,7 @@ h2v1_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, outptr = output_data[inrow]; outend = outptr + cinfo->output_width; while (outptr < outend) { - invalue = *inptr++; /* don't need GETJSAMPLE() here */ + invalue = *inptr++; *outptr++ = invalue; *outptr++ = invalue; } @@ -242,7 +242,7 @@ h2v2_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, outptr = output_data[outrow]; outend = outptr + cinfo->output_width; while (outptr < outend) { - invalue = *inptr++; /* don't need GETJSAMPLE() here */ + invalue = *inptr++; *outptr++ = invalue; *outptr++ = invalue; } @@ -283,20 +283,20 @@ h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, inptr = input_data[inrow]; outptr = output_data[inrow]; /* Special case for first column */ - invalue = GETJSAMPLE(*inptr++); + invalue = *inptr++; *outptr++ = (JSAMPLE)invalue; - *outptr++ = (JSAMPLE)((invalue * 3 + GETJSAMPLE(*inptr) + 2) >> 2); + *outptr++ = (JSAMPLE)((invalue * 3 + inptr[0] + 2) >> 2); for (colctr = compptr->downsampled_width - 2; colctr > 0; colctr--) { /* General case: 3/4 * nearer pixel + 1/4 * further pixel */ - invalue = GETJSAMPLE(*inptr++) * 3; - *outptr++ = (JSAMPLE)((invalue + GETJSAMPLE(inptr[-2]) + 1) >> 2); - *outptr++ = (JSAMPLE)((invalue + GETJSAMPLE(*inptr) + 2) >> 2); + invalue = (*inptr++) * 3; + *outptr++ = (JSAMPLE)((invalue + inptr[-2] + 1) >> 2); + *outptr++ = (JSAMPLE)((invalue + inptr[0] + 2) >> 2); } /* Special case for last column */ - invalue = GETJSAMPLE(*inptr); - *outptr++ = (JSAMPLE)((invalue * 3 + GETJSAMPLE(inptr[-1]) + 1) >> 2); + invalue = *inptr; + *outptr++ = (JSAMPLE)((invalue * 3 + inptr[-1] + 1) >> 2); *outptr++ = (JSAMPLE)invalue; } } @@ -338,7 +338,7 @@ h1v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, outptr = output_data[outrow++]; for (colctr = 0; colctr < compptr->downsampled_width; colctr++) { - thiscolsum = GETJSAMPLE(*inptr0++) * 3 + GETJSAMPLE(*inptr1++); + thiscolsum = (*inptr0++) * 3 + (*inptr1++); *outptr++ = (JSAMPLE)((thiscolsum + bias) >> 2); } } @@ -381,8 +381,8 @@ h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, outptr = output_data[outrow++]; /* Special case for first column */ - thiscolsum = GETJSAMPLE(*inptr0++) * 3 + GETJSAMPLE(*inptr1++); - nextcolsum = GETJSAMPLE(*inptr0++) * 3 + GETJSAMPLE(*inptr1++); + thiscolsum = (*inptr0++) * 3 + (*inptr1++); + nextcolsum = (*inptr0++) * 3 + (*inptr1++); *outptr++ = (JSAMPLE)((thiscolsum * 4 + 8) >> 4); *outptr++ = (JSAMPLE)((thiscolsum * 3 + nextcolsum + 7) >> 4); lastcolsum = thiscolsum; thiscolsum = nextcolsum; @@ -390,7 +390,7 @@ h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, for (colctr = compptr->downsampled_width - 2; colctr > 0; colctr--) { /* General case: 3/4 * nearer pixel + 1/4 * further pixel in each */ /* dimension, thus 9/16, 3/16, 3/16, 1/16 overall */ - nextcolsum = GETJSAMPLE(*inptr0++) * 3 + GETJSAMPLE(*inptr1++); + nextcolsum = (*inptr0++) * 3 + (*inptr1++); *outptr++ = (JSAMPLE)((thiscolsum * 3 + lastcolsum + 8) >> 4); *outptr++ = (JSAMPLE)((thiscolsum * 3 + nextcolsum + 7) >> 4); lastcolsum = thiscolsum; thiscolsum = nextcolsum; @@ -477,7 +477,13 @@ jinit_upsampler(j_decompress_ptr cinfo) } else if (h_in_group == h_out_group && v_in_group * 2 == v_out_group && do_fancy) { /* Non-fancy upsampling is handled by the generic method */ - upsample->methods[ci] = h1v2_fancy_upsample; +#if defined(__arm__) || defined(__aarch64__) || \ + defined(_M_ARM) || defined(_M_ARM64) + if (jsimd_can_h1v2_fancy_upsample()) + upsample->methods[ci] = jsimd_h1v2_fancy_upsample; + else +#endif + upsample->methods[ci] = h1v2_fancy_upsample; upsample->pub.need_context_rows = TRUE; } else if (h_in_group * 2 == h_out_group && v_in_group * 2 == v_out_group) { diff --git a/third-party/mozjpeg/mozjpeg/jdtrans.c b/third-party/mozjpeg/mozjpeg/jdtrans.c index 56713efe64d..d7ec4b83b3a 100644 --- a/third-party/mozjpeg/mozjpeg/jdtrans.c +++ b/third-party/mozjpeg/mozjpeg/jdtrans.c @@ -3,8 +3,8 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1995-1997, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2020, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -16,6 +16,7 @@ #define JPEG_INTERNALS #include "jinclude.h" #include "jpeglib.h" +#include "jpegcomp.h" /* Forward declarations */ diff --git a/third-party/mozjpeg/mozjpeg/jerror.c b/third-party/mozjpeg/mozjpeg/jerror.c index 936c4f5d80a..d0ab5b88b0c 100644 --- a/third-party/mozjpeg/mozjpeg/jerror.c +++ b/third-party/mozjpeg/mozjpeg/jerror.c @@ -3,8 +3,8 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1998, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -189,13 +189,13 @@ format_message(j_common_ptr cinfo, char *buffer) /* Format the message into the passed buffer */ if (isstring) - sprintf(buffer, msgtext, err->msg_parm.s); + SNPRINTF(buffer, JMSG_LENGTH_MAX, msgtext, err->msg_parm.s); else - sprintf(buffer, msgtext, - err->msg_parm.i[0], err->msg_parm.i[1], - err->msg_parm.i[2], err->msg_parm.i[3], - err->msg_parm.i[4], err->msg_parm.i[5], - err->msg_parm.i[6], err->msg_parm.i[7]); + SNPRINTF(buffer, JMSG_LENGTH_MAX, msgtext, + err->msg_parm.i[0], err->msg_parm.i[1], + err->msg_parm.i[2], err->msg_parm.i[3], + err->msg_parm.i[4], err->msg_parm.i[5], + err->msg_parm.i[6], err->msg_parm.i[7]); } diff --git a/third-party/mozjpeg/mozjpeg/jerror.h b/third-party/mozjpeg/mozjpeg/jerror.h index df5502cbee3..17bccf8dbf7 100644 --- a/third-party/mozjpeg/mozjpeg/jerror.h +++ b/third-party/mozjpeg/mozjpeg/jerror.h @@ -5,7 +5,7 @@ * Copyright (C) 1994-1997, Thomas G. Lane. * Modified 1997-2009 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2014, 2017, D. R. Commander. + * Copyright (C) 2014, 2017, 2021-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -103,7 +103,7 @@ JMESSAGE(JERR_MISMATCHED_QUANT_TABLE, "Cannot transcode due to multiple use of quantization table %d") JMESSAGE(JERR_MISSING_DATA, "Scan script does not transmit all data") JMESSAGE(JERR_MODE_CHANGE, "Invalid color quantization mode change") -JMESSAGE(JERR_NOTIMPL, "Not implemented yet") +JMESSAGE(JERR_NOTIMPL, "Requested features are incompatible") JMESSAGE(JERR_NOT_COMPILED, "Requested feature was omitted at compile time") #if JPEG_LIB_VERSION >= 70 JMESSAGE(JERR_NO_ARITH_TABLE, "Arithmetic table 0x%02x was not defined") @@ -211,6 +211,10 @@ JMESSAGE(JERR_BAD_PARAM_VALUE, "Bogus parameter value") JMESSAGE(JERR_UNSUPPORTED_SUSPEND, "I/O suspension not supported in scan optimization") JMESSAGE(JWRN_BOGUS_ICC, "Corrupt JPEG data: bad ICC marker") +#if JPEG_LIB_VERSION < 70 +JMESSAGE(JERR_BAD_DROP_SAMPLING, + "Component index %d: mismatching sampling ratio %d:%d, %d:%d, %c") +#endif #ifdef JMAKE_ENUM_LIST @@ -255,11 +259,21 @@ JMESSAGE(JWRN_BOGUS_ICC, "Corrupt JPEG data: bad ICC marker") (cinfo)->err->msg_parm.i[1] = (p2), \ (cinfo)->err->msg_parm.i[2] = (p3), \ (cinfo)->err->msg_parm.i[3] = (p4), \ - (*(cinfo)->err->error_exit) ((j_common_ptr) (cinfo))) -#define ERREXITS(cinfo,code,str) \ + (*(cinfo)->err->error_exit) ((j_common_ptr)(cinfo))) +#define ERREXIT6(cinfo, code, p1, p2, p3, p4, p5, p6) \ + ((cinfo)->err->msg_code = (code), \ + (cinfo)->err->msg_parm.i[0] = (p1), \ + (cinfo)->err->msg_parm.i[1] = (p2), \ + (cinfo)->err->msg_parm.i[2] = (p3), \ + (cinfo)->err->msg_parm.i[3] = (p4), \ + (cinfo)->err->msg_parm.i[4] = (p5), \ + (cinfo)->err->msg_parm.i[5] = (p6), \ + (*(cinfo)->err->error_exit) ((j_common_ptr)(cinfo))) +#define ERREXITS(cinfo, code, str) \ ((cinfo)->err->msg_code = (code), \ strncpy((cinfo)->err->msg_parm.s, (str), JMSG_STR_PARM_MAX), \ - (*(cinfo)->err->error_exit) ((j_common_ptr) (cinfo))) + (cinfo)->err->msg_parm.s[JMSG_STR_PARM_MAX - 1] = '\0', \ + (*(cinfo)->err->error_exit) ((j_common_ptr)(cinfo))) #define MAKESTMT(stuff) do { stuff } while (0) @@ -292,29 +306,30 @@ JMESSAGE(JWRN_BOGUS_ICC, "Corrupt JPEG data: bad ICC marker") (*(cinfo)->err->emit_message) ((j_common_ptr) (cinfo), (lvl))) #define TRACEMS3(cinfo,lvl,code,p1,p2,p3) \ MAKESTMT(int * _mp = (cinfo)->err->msg_parm.i; \ - _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); \ + _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); \ (cinfo)->err->msg_code = (code); \ (*(cinfo)->err->emit_message) ((j_common_ptr) (cinfo), (lvl)); ) #define TRACEMS4(cinfo,lvl,code,p1,p2,p3,p4) \ MAKESTMT(int * _mp = (cinfo)->err->msg_parm.i; \ - _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); _mp[3] = (p4); \ + _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); _mp[3] = (p4); \ (cinfo)->err->msg_code = (code); \ (*(cinfo)->err->emit_message) ((j_common_ptr) (cinfo), (lvl)); ) #define TRACEMS5(cinfo,lvl,code,p1,p2,p3,p4,p5) \ MAKESTMT(int * _mp = (cinfo)->err->msg_parm.i; \ - _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); _mp[3] = (p4); \ + _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); _mp[3] = (p4); \ _mp[4] = (p5); \ (cinfo)->err->msg_code = (code); \ (*(cinfo)->err->emit_message) ((j_common_ptr) (cinfo), (lvl)); ) #define TRACEMS8(cinfo,lvl,code,p1,p2,p3,p4,p5,p6,p7,p8) \ MAKESTMT(int * _mp = (cinfo)->err->msg_parm.i; \ - _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); _mp[3] = (p4); \ - _mp[4] = (p5); _mp[5] = (p6); _mp[6] = (p7); _mp[7] = (p8); \ + _mp[0] = (p1); _mp[1] = (p2); _mp[2] = (p3); _mp[3] = (p4); \ + _mp[4] = (p5); _mp[5] = (p6); _mp[6] = (p7); _mp[7] = (p8); \ (cinfo)->err->msg_code = (code); \ (*(cinfo)->err->emit_message) ((j_common_ptr) (cinfo), (lvl)); ) #define TRACEMSS(cinfo,lvl,code,str) \ ((cinfo)->err->msg_code = (code), \ strncpy((cinfo)->err->msg_parm.s, (str), JMSG_STR_PARM_MAX), \ - (*(cinfo)->err->emit_message) ((j_common_ptr) (cinfo), (lvl))) + (cinfo)->err->msg_parm.s[JMSG_STR_PARM_MAX - 1] = '\0', \ + (*(cinfo)->err->emit_message) ((j_common_ptr)(cinfo), (lvl))) #endif /* JERROR_H */ diff --git a/third-party/mozjpeg/mozjpeg/jfdctint.c b/third-party/mozjpeg/mozjpeg/jfdctint.c index b47c3061ac5..c95a3a7fb8a 100644 --- a/third-party/mozjpeg/mozjpeg/jfdctint.c +++ b/third-party/mozjpeg/mozjpeg/jfdctint.c @@ -4,11 +4,11 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1996, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2015, D. R. Commander. + * Copyright (C) 2015, 2020, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * - * This file contains a slow-but-accurate integer implementation of the + * This file contains a slower but more accurate integer implementation of the * forward DCT (Discrete Cosine Transform). * * A 2-D DCT can be done by 1-D DCT on each row followed by 1-D DCT diff --git a/third-party/mozjpeg/mozjpeg/jidctint.c b/third-party/mozjpeg/mozjpeg/jidctint.c index 98425d5fd08..bb087480192 100644 --- a/third-party/mozjpeg/mozjpeg/jidctint.c +++ b/third-party/mozjpeg/mozjpeg/jidctint.c @@ -3,13 +3,13 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1998, Thomas G. Lane. - * Modification developed 2002-2009 by Guido Vollbeding. + * Modification developed 2002-2018 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2015, D. R. Commander. + * Copyright (C) 2015, 2020, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * - * This file contains a slow-but-accurate integer implementation of the + * This file contains a slower but more accurate integer implementation of the * inverse DCT (Discrete Cosine Transform). In the IJG code, this routine * must also perform dequantization of the input coefficients. * @@ -417,7 +417,7 @@ jpeg_idct_islow(j_decompress_ptr cinfo, jpeg_component_info *compptr, /* * Perform dequantization and inverse DCT on one block of coefficients, - * producing a 7x7 output block. + * producing a reduced-size 7x7 output block. * * Optimized algorithm with 12 multiplications in the 1-D kernel. * cK represents sqrt(2) * cos(K*pi/14). @@ -1258,7 +1258,7 @@ jpeg_idct_10x10(j_decompress_ptr cinfo, jpeg_component_info *compptr, /* * Perform dequantization and inverse DCT on one block of coefficients, - * producing a 11x11 output block. + * producing an 11x11 output block. * * Optimized algorithm with 24 multiplications in the 1-D kernel. * cK represents sqrt(2) * cos(K*pi/22). @@ -2398,7 +2398,7 @@ jpeg_idct_16x16(j_decompress_ptr cinfo, jpeg_component_info *compptr, tmp0 = DEQUANTIZE(inptr[DCTSIZE * 0], quantptr[DCTSIZE * 0]); tmp0 = LEFT_SHIFT(tmp0, CONST_BITS); /* Add fudge factor here for final descale. */ - tmp0 += 1 << (CONST_BITS - PASS1_BITS - 1); + tmp0 += ONE << (CONST_BITS - PASS1_BITS - 1); z1 = DEQUANTIZE(inptr[DCTSIZE * 4], quantptr[DCTSIZE * 4]); tmp1 = MULTIPLY(z1, FIX(1.306562965)); /* c4[16] = c2[8] */ diff --git a/third-party/mozjpeg/mozjpeg/jinclude.h b/third-party/mozjpeg/mozjpeg/jinclude.h index c1bcf7d9da6..56e7a4b296d 100644 --- a/third-party/mozjpeg/mozjpeg/jinclude.h +++ b/third-party/mozjpeg/mozjpeg/jinclude.h @@ -3,8 +3,8 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1994, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2022-2023, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -17,72 +17,131 @@ * JPEG library. Most applications need only include jpeglib.h. */ +#ifndef __JINCLUDE_H__ +#define __JINCLUDE_H__ /* Include auto-config file to find out which system include files we need. */ #include "jconfig.h" /* auto configuration options */ +#include "jconfigint.h" #define JCONFIG_INCLUDED /* so that jpeglib.h doesn't do it again */ /* - * We need the NULL macro and size_t typedef. - * On an ANSI-conforming system it is sufficient to include . - * Otherwise, we get them from or ; we may have to - * pull in as well. * Note that the core JPEG library does not require ; * only the default error handler and data source/destination modules do. * But we must pull it in because of the references to FILE in jpeglib.h. * You can remove those references if you want to compile without . */ -#ifdef HAVE_STDDEF_H #include -#endif - -#ifdef HAVE_STDLIB_H #include -#endif - -#ifdef NEED_SYS_TYPES_H -#include -#endif - #include +#include /* - * We need memory copying and zeroing functions, plus strncpy(). - * ANSI and System V implementations declare these in . - * BSD doesn't have the mem() functions, but it does have bcopy()/bzero(). - * Some systems may declare memset and memcpy in . - * - * NOTE: we assume the size parameters to these functions are of type size_t. - * Change the casts in these macros if not! + * These macros/inline functions facilitate using Microsoft's "safe string" + * functions with Visual Studio builds without the need to scatter #ifdefs + * throughout the code base. */ -#ifdef NEED_BSD_STRINGS -#include -#define MEMZERO(target, size) \ - bzero((void *)(target), (size_t)(size)) -#define MEMCOPY(dest, src, size) \ - bcopy((const void *)(src), (void *)(dest), (size_t)(size)) +#ifdef _MSC_VER -#else /* not BSD, assume ANSI/SysV string lib */ +#define SNPRINTF(str, n, format, ...) \ + _snprintf_s(str, n, _TRUNCATE, format, ##__VA_ARGS__) -#include -#define MEMZERO(target, size) \ - memset((void *)(target), 0, (size_t)(size)) -#define MEMCOPY(dest, src, size) \ - memcpy((void *)(dest), (const void *)(src), (size_t)(size)) +#else + +#define SNPRINTF snprintf #endif -/* - * The modules that use fread() and fwrite() always invoke them through - * these macros. On some systems you may need to twiddle the argument casts. - * CAUTION: argument order is different from underlying functions! + +#ifndef NO_GETENV + +#ifdef _MSC_VER + +static INLINE int GETENV_S(char *buffer, size_t buffer_size, const char *name) +{ + size_t required_size; + + return (int)getenv_s(&required_size, buffer, buffer_size, name); +} + +#else /* _MSC_VER */ + +#include + +/* This provides a similar interface to the Microsoft/C11 getenv_s() function, + * but other than parameter validation, it has no advantages over getenv(). + */ + +static INLINE int GETENV_S(char *buffer, size_t buffer_size, const char *name) +{ + char *env; + + if (!buffer) { + if (buffer_size == 0) + return 0; + else + return (errno = EINVAL); + } + if (buffer_size == 0) + return (errno = EINVAL); + if (!name) { + *buffer = 0; + return 0; + } + + env = getenv(name); + if (!env) + { + *buffer = 0; + return 0; + } + + if (strlen(env) + 1 > buffer_size) { + *buffer = 0; + return ERANGE; + } + + strncpy(buffer, env, buffer_size); + + return 0; +} + +#endif /* _MSC_VER */ + +#endif /* NO_GETENV */ + + +#ifndef NO_PUTENV + +#ifdef _WIN32 + +#define PUTENV_S(name, value) _putenv_s(name, value) + +#else + +#include + +/* This provides a similar interface to the Microsoft _putenv_s() function, but + * other than parameter validation, it has no advantages over setenv(). */ -#define JFREAD(file, buf, sizeofbuf) \ - ((size_t)fread((void *)(buf), (size_t)1, (size_t)(sizeofbuf), (file))) -#define JFWRITE(file, buf, sizeofbuf) \ - ((size_t)fwrite((const void *)(buf), (size_t)1, (size_t)(sizeofbuf), (file))) +static INLINE int PUTENV_S(const char *name, const char *value) +{ + if (!name || !value) + return (errno = EINVAL); + + setenv(name, value, 1); + + return errno; +} + +#endif /* _WIN32 */ + +#endif /* NO_PUTENV */ + + +#endif /* JINCLUDE_H */ diff --git a/third-party/mozjpeg/mozjpeg/jmemmgr.c b/third-party/mozjpeg/mozjpeg/jmemmgr.c index 508ca7429c6..a40446f6ac3 100644 --- a/third-party/mozjpeg/mozjpeg/jmemmgr.c +++ b/third-party/mozjpeg/mozjpeg/jmemmgr.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2016, D. R. Commander. + * Copyright (C) 2016, 2021-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -37,12 +37,6 @@ #endif #include -#ifndef NO_GETENV -#ifndef HAVE_STDLIB_H /* should declare getenv() */ -extern char *getenv(const char *name); -#endif -#endif - LOCAL(size_t) round_up_pow2(size_t a, size_t b) @@ -74,10 +68,13 @@ round_up_pow2(size_t a, size_t b) * There isn't any really portable way to determine the worst-case alignment * requirement. This module assumes that the alignment requirement is * multiples of ALIGN_SIZE. - * By default, we define ALIGN_SIZE as sizeof(double). This is necessary on - * some workstations (where doubles really do need 8-byte alignment) and will - * work fine on nearly everything. If your machine has lesser alignment needs, - * you can save a few bytes by making ALIGN_SIZE smaller. + * By default, we define ALIGN_SIZE as the maximum of sizeof(double) and + * sizeof(void *). This is necessary on some workstations (where doubles + * really do need 8-byte alignment) and will work fine on nearly everything. + * We use the maximum of sizeof(double) and sizeof(void *) since sizeof(double) + * may be insufficient, for example, on CHERI-enabled platforms with 16-byte + * pointers and a 16-byte alignment requirement. If your machine has lesser + * alignment needs, you can save a few bytes by making ALIGN_SIZE smaller. * The only place I know of where this will NOT work is certain Macintosh * 680x0 compilers that define double as a 10-byte IEEE extended float. * Doing 10-byte alignment is counterproductive because longwords won't be @@ -87,7 +84,7 @@ round_up_pow2(size_t a, size_t b) #ifndef ALIGN_SIZE /* so can override from jconfig.h */ #ifndef WITH_SIMD -#define ALIGN_SIZE sizeof(double) +#define ALIGN_SIZE MAX(sizeof(void *), sizeof(double)) #else #define ALIGN_SIZE 32 /* Most of the SIMD instructions we support require 16-byte (128-bit) alignment, but AVX2 requires @@ -1032,7 +1029,7 @@ free_pool(j_common_ptr cinfo, int pool_id) large_pool_ptr next_lhdr_ptr = lhdr_ptr->next; space_freed = lhdr_ptr->bytes_used + lhdr_ptr->bytes_left + - sizeof(large_pool_hdr); + sizeof(large_pool_hdr) + ALIGN_SIZE - 1; jpeg_free_large(cinfo, (void *)lhdr_ptr, space_freed); mem->total_space_allocated -= space_freed; lhdr_ptr = next_lhdr_ptr; @@ -1045,7 +1042,7 @@ free_pool(j_common_ptr cinfo, int pool_id) while (shdr_ptr != NULL) { small_pool_ptr next_shdr_ptr = shdr_ptr->next; space_freed = shdr_ptr->bytes_used + shdr_ptr->bytes_left + - sizeof(small_pool_hdr); + sizeof(small_pool_hdr) + ALIGN_SIZE - 1; jpeg_free_small(cinfo, (void *)shdr_ptr, space_freed); mem->total_space_allocated -= space_freed; shdr_ptr = next_shdr_ptr; @@ -1162,12 +1159,16 @@ jinit_memory_mgr(j_common_ptr cinfo) */ #ifndef NO_GETENV { - char *memenv; + char memenv[30] = { 0 }; - if ((memenv = getenv("JPEGMEM")) != NULL) { + if (!GETENV_S(memenv, 30, "JPEGMEM") && strlen(memenv) > 0) { char ch = 'x'; +#ifdef _MSC_VER + if (sscanf_s(memenv, "%ld%c", &max_to_use, &ch, 1) > 0) { +#else if (sscanf(memenv, "%ld%c", &max_to_use, &ch) > 0) { +#endif if (ch == 'm' || ch == 'M') max_to_use *= 1000L; mem->pub.max_memory_to_use = max_to_use * 1000L; diff --git a/third-party/mozjpeg/mozjpeg/jmemnobs.c b/third-party/mozjpeg/mozjpeg/jmemnobs.c index 089be8f500d..cd6571ba1c4 100644 --- a/third-party/mozjpeg/mozjpeg/jmemnobs.c +++ b/third-party/mozjpeg/mozjpeg/jmemnobs.c @@ -22,11 +22,6 @@ #include "jpeglib.h" #include "jmemsys.h" /* import the system-dependent declarations */ -#ifndef HAVE_STDLIB_H /* should declare malloc(),free() */ -extern void *malloc(size_t size); -extern void free(void *ptr); -#endif - /* * Memory allocation and freeing are controlled by the regular library diff --git a/third-party/mozjpeg/mozjpeg/jmorecfg.h b/third-party/mozjpeg/mozjpeg/jmorecfg.h index d0b930079a1..b33a991914e 100644 --- a/third-party/mozjpeg/mozjpeg/jmorecfg.h +++ b/third-party/mozjpeg/mozjpeg/jmorecfg.h @@ -5,7 +5,7 @@ * Copyright (C) 1991-1997, Thomas G. Lane. * Modified 1997-2009 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2009, 2011, 2014-2015, 2018, D. R. Commander. + * Copyright (C) 2009, 2011, 2014-2015, 2018, 2020, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -43,25 +43,11 @@ #if BITS_IN_JSAMPLE == 8 /* JSAMPLE should be the smallest type that will hold the values 0..255. - * You can use a signed char by having GETJSAMPLE mask it with 0xFF. */ -#ifdef HAVE_UNSIGNED_CHAR - typedef unsigned char JSAMPLE; #define GETJSAMPLE(value) ((int)(value)) -#else /* not HAVE_UNSIGNED_CHAR */ - -typedef char JSAMPLE; -#ifdef __CHAR_UNSIGNED__ -#define GETJSAMPLE(value) ((int)(value)) -#else -#define GETJSAMPLE(value) ((int)(value) & 0xFF) -#endif /* __CHAR_UNSIGNED__ */ - -#endif /* HAVE_UNSIGNED_CHAR */ - #define MAXJSAMPLE 255 #define CENTERJSAMPLE 128 @@ -97,22 +83,9 @@ typedef short JCOEF; * managers, this is also the data type passed to fread/fwrite. */ -#ifdef HAVE_UNSIGNED_CHAR - typedef unsigned char JOCTET; #define GETJOCTET(value) (value) -#else /* not HAVE_UNSIGNED_CHAR */ - -typedef char JOCTET; -#ifdef __CHAR_UNSIGNED__ -#define GETJOCTET(value) (value) -#else -#define GETJOCTET(value) ((value) & 0xFF) -#endif /* __CHAR_UNSIGNED__ */ - -#endif /* HAVE_UNSIGNED_CHAR */ - /* These typedefs are used for various table entries and so forth. * They must be at least as wide as specified; but making them too big @@ -123,23 +96,11 @@ typedef char JOCTET; /* UINT8 must hold at least the values 0..255. */ -#ifdef HAVE_UNSIGNED_CHAR typedef unsigned char UINT8; -#else /* not HAVE_UNSIGNED_CHAR */ -#ifdef __CHAR_UNSIGNED__ -typedef char UINT8; -#else /* not __CHAR_UNSIGNED__ */ -typedef short UINT8; -#endif /* __CHAR_UNSIGNED__ */ -#endif /* HAVE_UNSIGNED_CHAR */ /* UINT16 must hold at least the values 0..65535. */ -#ifdef HAVE_UNSIGNED_SHORT typedef unsigned short UINT16; -#else /* not HAVE_UNSIGNED_SHORT */ -typedef unsigned int UINT16; -#endif /* HAVE_UNSIGNED_SHORT */ /* INT16 must hold at least the values -32768..32767. */ @@ -273,9 +234,9 @@ typedef int boolean; /* Capability options common to encoder and decoder: */ -#define DCT_ISLOW_SUPPORTED /* slow but accurate integer algorithm */ -#define DCT_IFAST_SUPPORTED /* faster, less accurate integer method */ -#define DCT_FLOAT_SUPPORTED /* floating-point: accurate, fast on fast HW */ +#define DCT_ISLOW_SUPPORTED /* accurate integer method */ +#define DCT_IFAST_SUPPORTED /* less accurate int method [legacy feature] */ +#define DCT_FLOAT_SUPPORTED /* floating-point method [legacy feature] */ /* Encoder capability options: */ diff --git a/third-party/mozjpeg/mozjpeg/jpegcomp.h b/third-party/mozjpeg/mozjpeg/jpegcomp.h index b32d544bf1e..c4834ac0df9 100644 --- a/third-party/mozjpeg/mozjpeg/jpegcomp.h +++ b/third-party/mozjpeg/mozjpeg/jpegcomp.h @@ -1,7 +1,7 @@ /* * jpegcomp.h * - * Copyright (C) 2010, D. R. Commander. + * Copyright (C) 2010, 2020, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -19,6 +19,7 @@ #define _min_DCT_v_scaled_size min_DCT_v_scaled_size #define _jpeg_width jpeg_width #define _jpeg_height jpeg_height +#define JERR_ARITH_NOTIMPL JERR_NOT_COMPILED #else #define _DCT_scaled_size DCT_scaled_size #define _DCT_h_scaled_size DCT_scaled_size diff --git a/third-party/mozjpeg/mozjpeg/jpegint.h b/third-party/mozjpeg/mozjpeg/jpegint.h index 9f48bbef165..092f98f96b7 100644 --- a/third-party/mozjpeg/mozjpeg/jpegint.h +++ b/third-party/mozjpeg/mozjpeg/jpegint.h @@ -5,8 +5,9 @@ * Copyright (C) 1991-1997, Thomas G. Lane. * Modified 1997-2009 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2015-2016, D. R. Commander. + * Copyright (C) 2015-2016, 2019, 2021, D. R. Commander. * Copyright (C) 2015, Google, Inc. + * Copyright (C) 2021, Alex Richardson. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. * For conditions of distribution and use, see the accompanying README.ijg @@ -30,33 +31,45 @@ typedef enum { /* Operating modes for buffer controllers */ } J_BUF_MODE; /* Values of global_state field (jdapi.c has some dependencies on ordering!) */ -#define CSTATE_START 100 /* after create_compress */ -#define CSTATE_SCANNING 101 /* start_compress done, write_scanlines OK */ -#define CSTATE_RAW_OK 102 /* start_compress done, write_raw_data OK */ -#define CSTATE_WRCOEFS 103 /* jpeg_write_coefficients done */ -#define DSTATE_START 200 /* after create_decompress */ -#define DSTATE_INHEADER 201 /* reading header markers, no SOS yet */ -#define DSTATE_READY 202 /* found SOS, ready for start_decompress */ -#define DSTATE_PRELOAD 203 /* reading multiscan file in start_decompress*/ -#define DSTATE_PRESCAN 204 /* performing dummy pass for 2-pass quant */ -#define DSTATE_SCANNING 205 /* start_decompress done, read_scanlines OK */ -#define DSTATE_RAW_OK 206 /* start_decompress done, read_raw_data OK */ -#define DSTATE_BUFIMAGE 207 /* expecting jpeg_start_output */ -#define DSTATE_BUFPOST 208 /* looking for SOS/EOI in jpeg_finish_output */ -#define DSTATE_RDCOEFS 209 /* reading file in jpeg_read_coefficients */ -#define DSTATE_STOPPING 210 /* looking for EOI in jpeg_finish_decompress */ +#define CSTATE_START 100 /* after create_compress */ +#define CSTATE_SCANNING 101 /* start_compress done, write_scanlines OK */ +#define CSTATE_RAW_OK 102 /* start_compress done, write_raw_data OK */ +#define CSTATE_WRCOEFS 103 /* jpeg_write_coefficients done */ +#define DSTATE_START 200 /* after create_decompress */ +#define DSTATE_INHEADER 201 /* reading header markers, no SOS yet */ +#define DSTATE_READY 202 /* found SOS, ready for start_decompress */ +#define DSTATE_PRELOAD 203 /* reading multiscan file in start_decompress*/ +#define DSTATE_PRESCAN 204 /* performing dummy pass for 2-pass quant */ +#define DSTATE_SCANNING 205 /* start_decompress done, read_scanlines OK */ +#define DSTATE_RAW_OK 206 /* start_decompress done, read_raw_data OK */ +#define DSTATE_BUFIMAGE 207 /* expecting jpeg_start_output */ +#define DSTATE_BUFPOST 208 /* looking for SOS/EOI in jpeg_finish_output */ +#define DSTATE_RDCOEFS 209 /* reading file in jpeg_read_coefficients */ +#define DSTATE_STOPPING 210 /* looking for EOI in jpeg_finish_decompress */ /* JLONG must hold at least signed 32-bit values. */ typedef long JLONG; +/* JUINTPTR must hold pointer values. */ +#ifdef __UINTPTR_TYPE__ +/* + * __UINTPTR_TYPE__ is GNU-specific and available in GCC 4.6+ and Clang 3.0+. + * Fortunately, that is sufficient to support the few architectures for which + * sizeof(void *) != sizeof(size_t). The only other options would require C99 + * or Clang-specific builtins. + */ +typedef __UINTPTR_TYPE__ JUINTPTR; +#else +typedef size_t JUINTPTR; +#endif /* * Left shift macro that handles a negative operand without causing any * sanitizer warnings */ -#define LEFT_SHIFT(a, b) ((JLONG)((unsigned long)(a) << (b))) +#define LEFT_SHIFT(a, b) ((JLONG)((unsigned long)(a) << (b))) /* Declarations for compression modules */ @@ -220,6 +233,9 @@ struct jpeg_decomp_master { JDIMENSION first_MCU_col[MAX_COMPONENTS]; JDIMENSION last_MCU_col[MAX_COMPONENTS]; boolean jinit_upsampler_no_alloc; + + /* Last iMCU row that was successfully decoded */ + JDIMENSION last_good_iMCU_row; }; /* Input control module */ @@ -354,9 +370,9 @@ struct jpeg_color_quantizer { #ifdef RIGHT_SHIFT_IS_UNSIGNED #define SHIFT_TEMPS JLONG shift_temp; #define RIGHT_SHIFT(x,shft) \ - ((shift_temp = (x)) < 0 ? \ + ((shift_temp = (x)) < 0 ? \ (shift_temp >> (shft)) | ((~((JLONG) 0)) << (32-(shft))) : \ - (shift_temp >> (shft))) + (shift_temp >> (shft))) #else #define SHIFT_TEMPS #define RIGHT_SHIFT(x,shft) ((x) >> (shft)) @@ -366,13 +382,13 @@ struct jpeg_color_quantizer { /* Compression module initialization routines */ EXTERN(void) jinit_compress_master (j_compress_ptr cinfo); EXTERN(void) jinit_c_master_control (j_compress_ptr cinfo, - boolean transcode_only); + boolean transcode_only); EXTERN(void) jinit_c_main_controller (j_compress_ptr cinfo, - boolean need_full_buffer); + boolean need_full_buffer); EXTERN(void) jinit_c_prep_controller (j_compress_ptr cinfo, - boolean need_full_buffer); + boolean need_full_buffer); EXTERN(void) jinit_c_coef_controller (j_compress_ptr cinfo, - boolean need_full_buffer); + boolean need_full_buffer); EXTERN(void) jinit_color_converter (j_compress_ptr cinfo); EXTERN(void) jinit_downsampler (j_compress_ptr cinfo); EXTERN(void) jinit_forward_dct (j_compress_ptr cinfo); @@ -383,11 +399,11 @@ EXTERN(void) jinit_marker_writer (j_compress_ptr cinfo); /* Decompression module initialization routines */ EXTERN(void) jinit_master_decompress (j_decompress_ptr cinfo); EXTERN(void) jinit_d_main_controller (j_decompress_ptr cinfo, - boolean need_full_buffer); + boolean need_full_buffer); EXTERN(void) jinit_d_coef_controller (j_decompress_ptr cinfo, - boolean need_full_buffer); + boolean need_full_buffer); EXTERN(void) jinit_d_post_controller (j_decompress_ptr cinfo, - boolean need_full_buffer); + boolean need_full_buffer); EXTERN(void) jinit_input_controller (j_decompress_ptr cinfo); EXTERN(void) jinit_marker_reader (j_decompress_ptr cinfo); EXTERN(void) jinit_huff_decoder (j_decompress_ptr cinfo); @@ -412,10 +428,10 @@ jpeg_mem_dest_internal (j_compress_ptr cinfo, EXTERN(long) jdiv_round_up (long a, long b); EXTERN(long) jround_up (long a, long b); EXTERN(void) jcopy_sample_rows (JSAMPARRAY input_array, int source_row, - JSAMPARRAY output_array, int dest_row, - int num_rows, JDIMENSION num_cols); + JSAMPARRAY output_array, int dest_row, + int num_rows, JDIMENSION num_cols); EXTERN(void) jcopy_block_row (JBLOCKROW input_row, JBLOCKROW output_row, - JDIMENSION num_blocks); + JDIMENSION num_blocks); EXTERN(void) jzero_far (void *target, size_t bytestozero); #ifdef C_ARITH_CODING_SUPPORTED @@ -435,12 +451,3 @@ extern const int jpeg_natural_order[]; /* zigzag coef order to natural order */ /* Arithmetic coding probability estimation tables in jaricom.c */ extern const JLONG jpeg_aritab[]; - -/* Suppress undefined-structure complaints if necessary. */ - -#ifdef INCOMPLETE_TYPES_BROKEN -#ifndef AM_MEMORY_MANAGER /* only jmemmgr.c defines these */ -struct jvirt_sarray_control { long dummy; }; -struct jvirt_barray_control { long dummy; }; -#endif -#endif /* INCOMPLETE_TYPES_BROKEN */ diff --git a/third-party/mozjpeg/mozjpeg/jpeglib.h b/third-party/mozjpeg/mozjpeg/jpeglib.h index 1f99e418255..16260d45bf7 100644 --- a/third-party/mozjpeg/mozjpeg/jpeglib.h +++ b/third-party/mozjpeg/mozjpeg/jpeglib.h @@ -5,7 +5,7 @@ * Copyright (C) 1991-1998, Thomas G. Lane. * Modified 2002-2009 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2009-2011, 2013-2014, 2016-2017, D. R. Commander. + * Copyright (C) 2009-2011, 2013-2014, 2016-2017, 2020, D. R. Commander. * Copyright (C) 2015, Google, Inc. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. @@ -246,9 +246,9 @@ typedef enum { /* DCT/IDCT algorithm options. */ typedef enum { - JDCT_ISLOW, /* slow but accurate integer algorithm */ - JDCT_IFAST, /* faster, less accurate integer method */ - JDCT_FLOAT /* floating-point: accurate, fast on fast HW */ + JDCT_ISLOW, /* accurate integer method */ + JDCT_IFAST, /* less accurate integer method [legacy feature] */ + JDCT_FLOAT /* floating-point method [legacy feature] */ } J_DCT_METHOD; #ifndef JDCT_DEFAULT /* may be overridden in jconfig.h */ diff --git a/third-party/mozjpeg/mozjpeg/jpegtran.1 b/third-party/mozjpeg/mozjpeg/jpegtran.1 index 2efb26472ac..5b1ded24bd1 100644 --- a/third-party/mozjpeg/mozjpeg/jpegtran.1 +++ b/third-party/mozjpeg/mozjpeg/jpegtran.1 @@ -1,4 +1,4 @@ -.TH JPEGTRAN 1 "18 March 2017" +.TH JPEGTRAN 1 "13 July 2021" .SH NAME jpegtran \- lossless transformation of JPEG files .SH SYNOPSIS @@ -161,13 +161,13 @@ to do a perfect rotation, if available, or an approximated one if not. .PP This version of \fBjpegtran\fR also offers a lossless crop option, which discards data outside of a given image region but losslessly preserves what is -inside. Like the rotate and flip transforms, lossless crop is restricted by the -current JPEG format; the upper left corner of the selected region must fall on -an iMCU boundary. If it doesn't, then it is silently moved up and/or left to -the nearest iMCU boundary (the lower right corner is unchanged.) Thus, the +inside. Like the rotate and flip transforms, lossless crop is restricted by +the current JPEG format; the upper left corner of the selected region must fall +on an iMCU boundary. If it doesn't, then it is silently moved up and/or left +to the nearest iMCU boundary (the lower right corner is unchanged.) Thus, the output image covers at least the requested region, but it may cover more. The -adjustment of the region dimensions may be optionally disabled by attaching -an 'f' character ("force") to the width or height number. +adjustment of the region dimensions may be optionally disabled by attaching an +'f' character ("force") to the width or height number. The image can be losslessly cropped by giving the switch: .TP @@ -180,6 +180,47 @@ left corner of the selected region must fall on an iMCU boundary. If it doesn't, then it is silently moved up and/or left to the nearest iMCU boundary (the lower right corner is unchanged.) .PP +If W or H is larger than the width/height of the input image, then the output +image is expanded in size, and the expanded region is filled in with zeros +(neutral gray). Attaching an 'f' character ("flatten") to the width number +will cause each block in the expanded region to be filled in with the DC +coefficient of the nearest block in the input image rather than grayed out. +Attaching an 'r' character ("reflect") to the width number will cause the +expanded region to be filled in with repeated reflections of the input image +rather than grayed out. +.PP +A complementary lossless wipe option is provided to discard (gray out) data +inside a given image region while losslessly preserving what is outside: +.TP +.B \-wipe WxH+X+Y +Wipe (gray out) a rectangular region of width W and height H from the input +image, starting at point X,Y. +.PP +Attaching an 'f' character ("flatten") to the width number will cause the +region to be filled with the average of adjacent blocks rather than grayed out. +If the wipe region and the region outside the wipe region, when adjusted to the +nearest iMCU boundary, form two horizontally adjacent rectangles, then +attaching an 'r' character ("reflect") to the width number will cause the wipe +region to be filled with repeated reflections of the outside region rather than +grayed out. +.PP +A lossless drop option is also provided, which allows another JPEG image to be +inserted ("dropped") into the input image data at a given position, replacing +the existing image data at that position: +.TP +.B \-drop +X+Y filename +Drop (insert) another image at point X,Y +.PP +Both the input image and the drop image must have the same subsampling level. +It is best if they also have the same quantization (quality.) Otherwise, the +quantization of the output image will be adapted to accommodate the higher of +the input image quality and the drop image quality. The trim option can be +used with the drop option to requantize the drop image to match the input +image. Note that a grayscale image can be dropped into a full-color image or +vice versa, as long as the full-color image has no vertical subsampling. If +the input image is grayscale and the drop image is full-color, then the +chrominance channels from the drop image will be discarded. +.PP Other not-strictly-lossless transformation switches are: .TP .B \-grayscale @@ -206,6 +247,10 @@ comments and other metadata in the source file. Copy only comment markers. This setting copies comments from the source file but discards any other metadata. .TP +.B \-copy icc +Copy only ICC profile markers. This setting copies the ICC profile from the +source file but discards any other metadata. +.TP .B \-copy all Copy all extra markers. This setting preserves miscellaneous markers found in the source file, such as JFIF thumbnails, Exif data, and Photoshop @@ -220,7 +265,7 @@ Additional switches recognized by jpegtran are: .BI \-icc " file" Embed ICC color management profile contained in the specified file. Note that this will cause \fBjpegtran\fR to ignore any APP2 markers in the input file, -even if \fB-copy all\fR is specified. +even if \fB-copy all\fR or \fB-copy icc\fR is specified. .TP .BI \-maxmemory " N" Set limit for amount of memory to use in processing large images. Value is @@ -229,9 +274,31 @@ number. For example, .B \-max 4m selects 4000000 bytes. If more space is needed, an error will occur. .TP +.BI \-maxscans " N" +Abort if the input image contains more than +.I N +scans. This feature demonstrates a method by which applications can guard +against denial-of-service attacks instigated by specially-crafted malformed +JPEG images containing numerous scans with missing image data or image data +consisting only of "EOB runs" (a feature of progressive JPEG images that allows +potentially hundreds of thousands of adjoining zero-value pixels to be +represented using only a few bytes.) Attempting to transform such malformed +JPEG images can cause excessive CPU activity, since the decompressor must fully +process each scan (even if the scan is corrupt) before it can proceed to the +next scan. +.TP .BI \-outfile " name" Send output image to the named file, not to standard output. .TP +.BI \-report +Report transformation progress. +.TP +.BI \-strict +Treat all warnings as fatal. This feature also demonstrates a method by which +applications can guard against attacks instigated by specially-crafted +malformed JPEG images. Enabling this option will cause the decompressor to +abort if the input image contains incomplete or corrupt image data. +.TP .B \-verbose Enable debug printout. More .BR \-v 's diff --git a/third-party/mozjpeg/mozjpeg/jpegtran.c b/third-party/mozjpeg/mozjpeg/jpegtran.c index 6ed998935ae..0ad080b61e7 100644 --- a/third-party/mozjpeg/mozjpeg/jpegtran.c +++ b/third-party/mozjpeg/mozjpeg/jpegtran.c @@ -2,9 +2,9 @@ * jpegtran.c * * This file was part of the Independent JPEG Group's software: - * Copyright (C) 1995-2010, Thomas G. Lane, Guido Vollbeding. + * Copyright (C) 1995-2019, Thomas G. Lane, Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2010, 2014, 2017, D. R. Commander. + * Copyright (C) 2010, 2014, 2017, 2019-2022, D. R. Commander. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. * For conditions of distribution and use, see the accompanying README file. @@ -15,21 +15,15 @@ * provides some lossless and sort-of-lossless transformations of JPEG data. */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ #include "transupp.h" /* Support routines for jpegtran */ #include "jversion.h" /* for version message */ #include "jconfigint.h" -#ifdef USE_CCOMMAND /* command-line reader for Macintosh */ -#ifdef __MWERKS__ -#include /* Metrowerks needs this */ -#include /* ... and this */ -#endif -#ifdef THINK_C -#include /* Think declares it here */ -#endif -#endif - /* * Argument-parsing code. @@ -42,7 +36,11 @@ static const char *progname; /* program name for error messages */ static char *icc_filename; /* for -icc switch */ +JDIMENSION max_scans; /* for -maxscans switch */ static char *outfilename; /* for -outfile switch */ +static char *dropfilename; /* for -drop switch */ +boolean report; /* for -report switch */ +boolean strict; /* for -strict switch */ static boolean prefer_smallest; /* use smallest of input or result file (if no image-changing options supplied) */ static JCOPY_OPTION copyoption; /* -copy switch */ static jpeg_transform_info transformoption; /* image transformation options */ @@ -51,7 +49,7 @@ boolean memsrc = FALSE; /* for -memsrc switch */ LOCAL(void) -usage (void) +usage(void) /* complain about bad command line */ { fprintf(stderr, "usage: %s [switches] ", progname); @@ -64,6 +62,7 @@ usage (void) fprintf(stderr, "Switches (names may be abbreviated):\n"); fprintf(stderr, " -copy none Copy no extra markers from source file\n"); fprintf(stderr, " -copy comments Copy only comment markers (default)\n"); + fprintf(stderr, " -copy icc Copy only ICC profile markers\n"); fprintf(stderr, " -copy all Copy all extra markers\n"); #ifdef ENTROPY_OPT_SUPPORTED fprintf(stderr, " -optimize Optimize Huffman table (smaller file, but slow compression, enabled by default)\n"); @@ -75,9 +74,10 @@ usage (void) fprintf(stderr, " -fastcrush Disable progressive scan optimization\n"); fprintf(stderr, "Switches for modifying the image:\n"); #if TRANSFORMS_SUPPORTED - fprintf(stderr, " -crop WxH+X+Y Crop to a rectangular subarea\n"); - fprintf(stderr, " -grayscale Reduce to grayscale (omit color data)\n"); + fprintf(stderr, " -crop WxH+X+Y Crop to a rectangular region\n"); + fprintf(stderr, " -drop +X+Y filename Drop (insert) another image\n"); fprintf(stderr, " -flip [horizontal|vertical] Mirror image (left-right or top-bottom)\n"); + fprintf(stderr, " -grayscale Reduce to grayscale (omit color data)\n"); fprintf(stderr, " -perfect Fail if there is non-transformable edge blocks\n"); fprintf(stderr, " -rotate [90|180|270] Rotate image (degrees clockwise)\n"); #endif @@ -85,6 +85,8 @@ usage (void) fprintf(stderr, " -transpose Transpose image\n"); fprintf(stderr, " -transverse Transverse transpose image\n"); fprintf(stderr, " -trim Drop non-transformable edge blocks\n"); + fprintf(stderr, " with -drop: Requantize drop file to match source file\n"); + fprintf(stderr, " -wipe WxH+X+Y Wipe (gray out) a rectangular region\n"); #endif fprintf(stderr, "Switches for advanced users:\n"); #ifdef C_ARITH_CODING_SUPPORTED @@ -93,7 +95,10 @@ usage (void) fprintf(stderr, " -icc FILE Embed ICC profile contained in FILE\n"); fprintf(stderr, " -restart N Set restart interval in rows, or in blocks with B\n"); fprintf(stderr, " -maxmemory N Maximum memory to use (in kbytes)\n"); + fprintf(stderr, " -maxscans N Maximum number of scans to allow in input file\n"); fprintf(stderr, " -outfile name Specify name for output file\n"); + fprintf(stderr, " -report Report transformation progress\n"); + fprintf(stderr, " -strict Treat all warnings as fatal\n"); fprintf(stderr, " -verbose or -debug Emit debug output\n"); fprintf(stderr, " -version Print version information and exit\n"); fprintf(stderr, "Switches for wizards:\n"); @@ -105,7 +110,7 @@ usage (void) LOCAL(void) -select_transform (JXFORM_CODE transform) +select_transform(JXFORM_CODE transform) /* Silly little routine to detect multiple transform options, * which we can't handle. */ @@ -128,7 +133,7 @@ select_transform (JXFORM_CODE transform) LOCAL(int) -parse_switches (j_compress_ptr cinfo, int argc, char **argv, +parse_switches(j_compress_ptr cinfo, int argc, char **argv, int last_file_arg_seen, boolean for_real) /* Parse optional switches. * Returns argv[] index of first file-name argument (== argc if none). @@ -151,7 +156,10 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, simple_progressive = FALSE; #endif icc_filename = NULL; + max_scans = 0; outfilename = NULL; + report = FALSE; + strict = FALSE; copyoption = JCOPYOPT_DEFAULT; transformoption.transform = JXFORM_NONE; transformoption.perfect = FALSE; @@ -198,6 +206,8 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, copyoption = JCOPYOPT_NONE; } else if (keymatch(argv[argn], "comments", 1)) { copyoption = JCOPYOPT_COMMENTS; + } else if (keymatch(argv[argn], "icc", 1)) { + copyoption = JCOPYOPT_ICC; } else if (keymatch(argv[argn], "all", 1)) { copyoption = JCOPYOPT_ALL; } else @@ -208,7 +218,8 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, #if TRANSFORMS_SUPPORTED if (++argn >= argc) /* advance to next argument */ usage(); - if (! jtransform_parse_crop_spec(&transformoption, argv[argn])) { + if (transformoption.crop /* reject multiple crop/drop/wipe requests */ || + !jtransform_parse_crop_spec(&transformoption, argv[argn])) { fprintf(stderr, "%s: bogus -crop argument '%s'\n", progname, argv[argn]); exit(EXIT_FAILURE); @@ -218,12 +229,32 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, select_transform(JXFORM_NONE); /* force an error */ #endif + } else if (keymatch(arg, "drop", 2)) { +#if TRANSFORMS_SUPPORTED + if (++argn >= argc) /* advance to next argument */ + usage(); + if (transformoption.crop /* reject multiple crop/drop/wipe requests */ || + !jtransform_parse_crop_spec(&transformoption, argv[argn]) || + transformoption.crop_width_set != JCROP_UNSET || + transformoption.crop_height_set != JCROP_UNSET) { + fprintf(stderr, "%s: bogus -drop argument '%s'\n", + progname, argv[argn]); + exit(EXIT_FAILURE); + } + if (++argn >= argc) /* advance to next argument */ + usage(); + dropfilename = argv[argn]; + select_transform(JXFORM_DROP); +#else + select_transform(JXFORM_NONE); /* force an error */ +#endif + } else if (keymatch(arg, "debug", 1) || keymatch(arg, "verbose", 1)) { /* Enable debug printouts. */ /* On first -d, print version identification */ static boolean printed_version = FALSE; - if (! printed_version) { + if (!printed_version) { fprintf(stderr, "%s version %s (build %s)\n", PACKAGE_NAME, VERSION, BUILD); fprintf(stderr, "%s\n\n", JCOPYRIGHT); @@ -282,6 +313,12 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, lval *= 1000L; cinfo->mem->max_memory_to_use = lval * 1000L; + } else if (keymatch(arg, "maxscans", 4)) { + if (++argn >= argc) /* advance to next argument */ + usage(); + if (sscanf(argv[argn], "%u", &max_scans) != 1) + usage(); + } else if (keymatch(arg, "optimize", 1) || keymatch(arg, "optimise", 1)) { /* Enable entropy parm optimization. */ #ifdef ENTROPY_OPT_SUPPORTED @@ -315,6 +352,9 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, exit(EXIT_FAILURE); #endif + } else if (keymatch(arg, "report", 3)) { + report = TRUE; + } else if (keymatch(arg, "restart", 1)) { /* Restart interval in MCU rows (or in MCUs with 'b'). */ long lval; @@ -327,10 +367,10 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, if (lval < 0 || lval > 65535L) usage(); if (ch == 'b' || ch == 'B') { - cinfo->restart_interval = (unsigned int) lval; + cinfo->restart_interval = (unsigned int)lval; cinfo->restart_in_rows = 0; /* else prior '-restart n' overrides me */ } else { - cinfo->restart_in_rows = (int) lval; + cinfo->restart_in_rows = (int)lval; /* restart_interval will be computed during startup */ } @@ -368,6 +408,9 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, exit(EXIT_FAILURE); #endif + } else if (keymatch(arg, "strict", 2)) { + strict = TRUE; + } else if (keymatch(arg, "transpose", 1)) { /* Transpose (across UL-to-LR axis). */ select_transform(JXFORM_TRANSPOSE); @@ -383,6 +426,21 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, transformoption.trim = TRUE; prefer_smallest = FALSE; + } else if (keymatch(arg, "wipe", 1)) { +#if TRANSFORMS_SUPPORTED + if (++argn >= argc) /* advance to next argument */ + usage(); + if (transformoption.crop /* reject multiple crop/drop/wipe requests */ || + !jtransform_parse_crop_spec(&transformoption, argv[argn])) { + fprintf(stderr, "%s: bogus -wipe argument '%s'\n", + progname, argv[argn]); + exit(EXIT_FAILURE); + } + select_transform(JXFORM_WIPE); +#else + select_transform(JXFORM_NONE); /* force an error */ +#endif + } else { usage(); /* bogus switch */ } @@ -399,29 +457,44 @@ parse_switches (j_compress_ptr cinfo, int argc, char **argv, #ifdef C_MULTISCAN_FILES_SUPPORTED if (scansarg != NULL) /* process -scans if it was present */ - if (! read_scan_script(cinfo, scansarg)) + if (!read_scan_script(cinfo, scansarg)) usage(); #endif - } return argn; /* return index of next arg (file name) */ } +METHODDEF(void) +my_emit_message(j_common_ptr cinfo, int msg_level) +{ + if (msg_level < 0) { + /* Treat warning as fatal */ + cinfo->err->error_exit(cinfo); + } else { + if (cinfo->err->trace_level >= msg_level) + cinfo->err->output_message(cinfo); + } +} + + /* * The main program. */ int -main (int argc, char **argv) +main(int argc, char **argv) { struct jpeg_decompress_struct srcinfo; +#if TRANSFORMS_SUPPORTED + struct jpeg_decompress_struct dropinfo; + struct jpeg_error_mgr jdroperr; + FILE *drop_file; +#endif struct jpeg_compress_struct dstinfo; struct jpeg_error_mgr jsrcerr, jdsterr; -#ifdef PROGRESS_REPORT - struct cdjpeg_progress_mgr progress; -#endif + struct cdjpeg_progress_mgr src_progress, dst_progress; jvirt_barray_ptr *src_coef_arrays; jvirt_barray_ptr *dst_coef_arrays; int file_index; @@ -437,11 +510,6 @@ main (int argc, char **argv) JOCTET *icc_profile = NULL; long icc_len = 0; - /* On Mac, fetch a command line. */ -#ifdef USE_CCOMMAND - argc = ccommand(&argv); -#endif - progname = argv[0]; if (progname == NULL || progname[0] == 0) progname = "jpegtran"; /* in case C library doesn't provide it */ @@ -458,24 +526,27 @@ main (int argc, char **argv) * values read here are mostly ignored; we will rescan the switches after * opening the input file. Also note that most of the switches affect the * destination JPEG object, so we parse into that and then copy over what - * needs to affects the source too. + * needs to affect the source too. */ file_index = parse_switches(&dstinfo, argc, argv, 0, FALSE); jsrcerr.trace_level = jdsterr.trace_level; srcinfo.mem->max_memory_to_use = dstinfo.mem->max_memory_to_use; + if (strict) + jsrcerr.emit_message = my_emit_message; + #ifdef TWO_FILE_COMMANDLINE /* Must have either -outfile switch or explicit output file name */ if (outfilename == NULL) { - if (file_index != argc-2) { + if (file_index != argc - 2) { fprintf(stderr, "%s: must name one input and one output file\n", progname); usage(); } - outfilename = argv[file_index+1]; + outfilename = argv[file_index + 1]; } else { - if (file_index != argc-1) { + if (file_index != argc - 1) { fprintf(stderr, "%s: must name one input and one output file\n", progname); usage(); @@ -483,7 +554,7 @@ main (int argc, char **argv) } #else /* Unix style: expect zero or one file name */ - if (file_index < argc-1) { + if (file_index < argc - 1) { fprintf(stderr, "%s: only one input file\n", progname); usage(); } @@ -528,10 +599,33 @@ main (int argc, char **argv) fclose(icc_file); if (copyoption == JCOPYOPT_ALL) copyoption = JCOPYOPT_ALL_EXCEPT_ICC; + if (copyoption == JCOPYOPT_ICC) + copyoption = JCOPYOPT_NONE; } -#ifdef PROGRESS_REPORT - start_progress_monitor((j_common_ptr) &dstinfo, &progress); + if (report) { + start_progress_monitor((j_common_ptr)&dstinfo, &dst_progress); + dst_progress.report = report; + } + if (report || max_scans != 0) { + start_progress_monitor((j_common_ptr)&srcinfo, &src_progress); + src_progress.report = report; + src_progress.max_scans = max_scans; + } +#if TRANSFORMS_SUPPORTED + /* Open the drop file. */ + if (dropfilename != NULL) { + if ((drop_file = fopen(dropfilename, READ_BINARY)) == NULL) { + fprintf(stderr, "%s: can't open %s for reading\n", progname, + dropfilename); + exit(EXIT_FAILURE); + } + dropinfo.err = jpeg_std_error(&jdroperr); + jpeg_create_decompress(&dropinfo); + jpeg_stdio_src(&dropinfo, drop_file); + } else { + drop_file = NULL; + } #endif /* Specify data source for decompression */ @@ -548,7 +642,7 @@ main (int argc, char **argv) fprintf(stderr, "%s: memory allocation failure\n", progname); exit(EXIT_FAILURE); } - nbytes = JFREAD(fp, &inbuffer[insize], INPUT_BUF_SIZE); + nbytes = fread(&inbuffer[insize], 1, INPUT_BUF_SIZE, fp); if (nbytes < INPUT_BUF_SIZE && ferror(fp)) { if (file_index < argc) fprintf(stderr, "%s: can't read from %s\n", progname, @@ -567,7 +661,18 @@ main (int argc, char **argv) jcopy_markers_setup(&srcinfo, copyoption); /* Read file header */ - (void) jpeg_read_header(&srcinfo, TRUE); + (void)jpeg_read_header(&srcinfo, TRUE); + +#if TRANSFORMS_SUPPORTED + if (dropfilename != NULL) { + (void)jpeg_read_header(&dropinfo, TRUE); + transformoption.crop_width = dropinfo.image_width; + transformoption.crop_width_set = JCROP_POS; + transformoption.crop_height = dropinfo.image_height; + transformoption.crop_height_set = JCROP_POS; + transformoption.drop_ptr = &dropinfo; + } +#endif /* Any space needed by a transform option must be requested before * jpeg_read_coefficients so that memory allocation will be done right. @@ -584,6 +689,12 @@ main (int argc, char **argv) /* Read source file as DCT coefficients */ src_coef_arrays = jpeg_read_coefficients(&srcinfo); +#if TRANSFORMS_SUPPORTED + if (dropfilename != NULL) { + transformoption.drop_coef_arrays = jpeg_read_coefficients(&dropinfo); + } +#endif + /* Initialize destination compression parameters from source values */ jpeg_copy_critical_parameters(&srcinfo, &dstinfo); @@ -601,7 +712,7 @@ main (int argc, char **argv) /* Close input file, if we opened it. * Note: we assume that jpeg_read_coefficients consumed all input * until JPEG_REACHED_EOI, and that jpeg_finish_decompress will - * only consume more while (! cinfo->inputctl->eoi_reached). + * only consume more while (!cinfo->inputctl->eoi_reached). * We cannot call jpeg_finish_decompress here since we still need the * virtual arrays allocated from the source object for processing. */ @@ -664,7 +775,7 @@ main (int argc, char **argv) buffer = inbuffer; } - nbytes = JFWRITE(fp, buffer, size); + nbytes = fwrite(buffer, 1, size, fp); if (nbytes < size && ferror(fp)) { if (file_index < argc) fprintf(stderr, "%s: can't write to %s\n", progname, @@ -674,26 +785,42 @@ main (int argc, char **argv) } } #endif - + jpeg_destroy_compress(&dstinfo); - (void) jpeg_finish_decompress(&srcinfo); + (void)jpeg_finish_decompress(&srcinfo); jpeg_destroy_decompress(&srcinfo); +#if TRANSFORMS_SUPPORTED + if (dropfilename != NULL) { + (void)jpeg_finish_decompress(&dropinfo); + jpeg_destroy_decompress(&dropinfo); + } +#endif + /* Close output file, if we opened it */ if (fp != stdout) fclose(fp); - -#ifdef PROGRESS_REPORT - end_progress_monitor((j_common_ptr) &dstinfo); +#if TRANSFORMS_SUPPORTED + if (drop_file != NULL) + fclose(drop_file); #endif + if (report) + end_progress_monitor((j_common_ptr)&dstinfo); + if (report || max_scans != 0) + end_progress_monitor((j_common_ptr)&srcinfo); + free(inbuffer); free(outbuffer); - if (icc_profile != NULL) - free(icc_profile); + free(icc_profile); /* All done. */ +#if TRANSFORMS_SUPPORTED + if (dropfilename != NULL) + exit(jsrcerr.num_warnings + jdroperr.num_warnings + + jdsterr.num_warnings ? EXIT_WARNING : EXIT_SUCCESS); +#endif exit(jsrcerr.num_warnings + jdsterr.num_warnings ? EXIT_WARNING : EXIT_SUCCESS); return 0; /* suppress no-return-value warnings */ diff --git a/third-party/mozjpeg/mozjpeg/jquant1.c b/third-party/mozjpeg/mozjpeg/jquant1.c index 40bbb28cc7f..73b83e16e5c 100644 --- a/third-party/mozjpeg/mozjpeg/jquant1.c +++ b/third-party/mozjpeg/mozjpeg/jquant1.c @@ -479,7 +479,7 @@ color_quantize(j_decompress_ptr cinfo, JSAMPARRAY input_buf, for (col = width; col > 0; col--) { pixcode = 0; for (ci = 0; ci < nc; ci++) { - pixcode += GETJSAMPLE(colorindex[ci][GETJSAMPLE(*ptrin++)]); + pixcode += colorindex[ci][*ptrin++]; } *ptrout++ = (JSAMPLE)pixcode; } @@ -506,9 +506,9 @@ color_quantize3(j_decompress_ptr cinfo, JSAMPARRAY input_buf, ptrin = input_buf[row]; ptrout = output_buf[row]; for (col = width; col > 0; col--) { - pixcode = GETJSAMPLE(colorindex0[GETJSAMPLE(*ptrin++)]); - pixcode += GETJSAMPLE(colorindex1[GETJSAMPLE(*ptrin++)]); - pixcode += GETJSAMPLE(colorindex2[GETJSAMPLE(*ptrin++)]); + pixcode = colorindex0[*ptrin++]; + pixcode += colorindex1[*ptrin++]; + pixcode += colorindex2[*ptrin++]; *ptrout++ = (JSAMPLE)pixcode; } } @@ -552,7 +552,7 @@ quantize_ord_dither(j_decompress_ptr cinfo, JSAMPARRAY input_buf, * required amount of padding. */ *output_ptr += - colorindex_ci[GETJSAMPLE(*input_ptr) + dither[col_index]]; + colorindex_ci[*input_ptr + dither[col_index]]; input_ptr += nc; output_ptr++; col_index = (col_index + 1) & ODITHER_MASK; @@ -595,12 +595,9 @@ quantize3_ord_dither(j_decompress_ptr cinfo, JSAMPARRAY input_buf, col_index = 0; for (col = width; col > 0; col--) { - pixcode = - GETJSAMPLE(colorindex0[GETJSAMPLE(*input_ptr++) + dither0[col_index]]); - pixcode += - GETJSAMPLE(colorindex1[GETJSAMPLE(*input_ptr++) + dither1[col_index]]); - pixcode += - GETJSAMPLE(colorindex2[GETJSAMPLE(*input_ptr++) + dither2[col_index]]); + pixcode = colorindex0[(*input_ptr++) + dither0[col_index]]; + pixcode += colorindex1[(*input_ptr++) + dither1[col_index]]; + pixcode += colorindex2[(*input_ptr++) + dither2[col_index]]; *output_ptr++ = (JSAMPLE)pixcode; col_index = (col_index + 1) & ODITHER_MASK; } @@ -677,15 +674,15 @@ quantize_fs_dither(j_decompress_ptr cinfo, JSAMPARRAY input_buf, * The maximum error is +- MAXJSAMPLE; this sets the required size * of the range_limit array. */ - cur += GETJSAMPLE(*input_ptr); - cur = GETJSAMPLE(range_limit[cur]); + cur += *input_ptr; + cur = range_limit[cur]; /* Select output value, accumulate into output code for this pixel */ - pixcode = GETJSAMPLE(colorindex_ci[cur]); + pixcode = colorindex_ci[cur]; *output_ptr += (JSAMPLE)pixcode; /* Compute actual representation error at this pixel */ /* Note: we can do this even though we don't have the final */ /* pixel code, because the colormap is orthogonal. */ - cur -= GETJSAMPLE(colormap_ci[pixcode]); + cur -= colormap_ci[pixcode]; /* Compute error fractions to be propagated to adjacent pixels. * Add these into the running sums, and simultaneously shift the * next-line error sums left by 1 column. diff --git a/third-party/mozjpeg/mozjpeg/jquant2.c b/third-party/mozjpeg/mozjpeg/jquant2.c index 0ce0ca54721..1c14ef76380 100644 --- a/third-party/mozjpeg/mozjpeg/jquant2.c +++ b/third-party/mozjpeg/mozjpeg/jquant2.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1996, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2009, 2014-2015, D. R. Commander. + * Copyright (C) 2009, 2014-2015, 2020, 2023, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -215,9 +215,9 @@ prescan_quantize(j_decompress_ptr cinfo, JSAMPARRAY input_buf, ptr = input_buf[row]; for (col = width; col > 0; col--) { /* get pixel value and index into the histogram */ - histp = &histogram[GETJSAMPLE(ptr[0]) >> C0_SHIFT] - [GETJSAMPLE(ptr[1]) >> C1_SHIFT] - [GETJSAMPLE(ptr[2]) >> C2_SHIFT]; + histp = &histogram[ptr[0] >> C0_SHIFT] + [ptr[1] >> C1_SHIFT] + [ptr[2] >> C2_SHIFT]; /* increment, check for overflow and undo increment if so. */ if (++(*histp) <= 0) (*histp)--; @@ -665,7 +665,7 @@ find_nearby_colors(j_decompress_ptr cinfo, int minc0, int minc1, int minc2, for (i = 0; i < numcolors; i++) { /* We compute the squared-c0-distance term, then add in the other two. */ - x = GETJSAMPLE(cinfo->colormap[0][i]); + x = cinfo->colormap[0][i]; if (x < minc0) { tdist = (x - minc0) * C0_SCALE; min_dist = tdist * tdist; @@ -688,7 +688,7 @@ find_nearby_colors(j_decompress_ptr cinfo, int minc0, int minc1, int minc2, } } - x = GETJSAMPLE(cinfo->colormap[1][i]); + x = cinfo->colormap[1][i]; if (x < minc1) { tdist = (x - minc1) * C1_SCALE; min_dist += tdist * tdist; @@ -710,7 +710,7 @@ find_nearby_colors(j_decompress_ptr cinfo, int minc0, int minc1, int minc2, } } - x = GETJSAMPLE(cinfo->colormap[2][i]); + x = cinfo->colormap[2][i]; if (x < minc2) { tdist = (x - minc2) * C2_SCALE; min_dist += tdist * tdist; @@ -788,13 +788,13 @@ find_best_colors(j_decompress_ptr cinfo, int minc0, int minc1, int minc2, #define STEP_C2 ((1 << C2_SHIFT) * C2_SCALE) for (i = 0; i < numcolors; i++) { - icolor = GETJSAMPLE(colorlist[i]); + icolor = colorlist[i]; /* Compute (square of) distance from minc0/c1/c2 to this color */ - inc0 = (minc0 - GETJSAMPLE(cinfo->colormap[0][icolor])) * C0_SCALE; + inc0 = (minc0 - cinfo->colormap[0][icolor]) * C0_SCALE; dist0 = inc0 * inc0; - inc1 = (minc1 - GETJSAMPLE(cinfo->colormap[1][icolor])) * C1_SCALE; + inc1 = (minc1 - cinfo->colormap[1][icolor]) * C1_SCALE; dist0 += inc1 * inc1; - inc2 = (minc2 - GETJSAMPLE(cinfo->colormap[2][icolor])) * C2_SCALE; + inc2 = (minc2 - cinfo->colormap[2][icolor]) * C2_SCALE; dist0 += inc2 * inc2; /* Form the initial difference increments */ inc0 = inc0 * (2 * STEP_C0) + STEP_C0 * STEP_C0; @@ -879,7 +879,7 @@ fill_inverse_cmap(j_decompress_ptr cinfo, int c0, int c1, int c2) for (ic1 = 0; ic1 < BOX_C1_ELEMS; ic1++) { cachep = &histogram[c0 + ic0][c1 + ic1][c2]; for (ic2 = 0; ic2 < BOX_C2_ELEMS; ic2++) { - *cachep++ = (histcell)(GETJSAMPLE(*cptr++) + 1); + *cachep++ = (histcell)((*cptr++) + 1); } } } @@ -909,9 +909,9 @@ pass2_no_dither(j_decompress_ptr cinfo, JSAMPARRAY input_buf, outptr = output_buf[row]; for (col = width; col > 0; col--) { /* get pixel value and index into the cache */ - c0 = GETJSAMPLE(*inptr++) >> C0_SHIFT; - c1 = GETJSAMPLE(*inptr++) >> C1_SHIFT; - c2 = GETJSAMPLE(*inptr++) >> C2_SHIFT; + c0 = (*inptr++) >> C0_SHIFT; + c1 = (*inptr++) >> C1_SHIFT; + c2 = (*inptr++) >> C2_SHIFT; cachep = &histogram[c0][c1][c2]; /* If we have not seen this color before, find nearest colormap entry */ /* and update the cache */ @@ -996,12 +996,12 @@ pass2_fs_dither(j_decompress_ptr cinfo, JSAMPARRAY input_buf, * The maximum error is +- MAXJSAMPLE (or less with error limiting); * this sets the required size of the range_limit array. */ - cur0 += GETJSAMPLE(inptr[0]); - cur1 += GETJSAMPLE(inptr[1]); - cur2 += GETJSAMPLE(inptr[2]); - cur0 = GETJSAMPLE(range_limit[cur0]); - cur1 = GETJSAMPLE(range_limit[cur1]); - cur2 = GETJSAMPLE(range_limit[cur2]); + cur0 += inptr[0]; + cur1 += inptr[1]; + cur2 += inptr[2]; + cur0 = range_limit[cur0]; + cur1 = range_limit[cur1]; + cur2 = range_limit[cur2]; /* Index into the cache with adjusted pixel value */ cachep = &histogram[cur0 >> C0_SHIFT][cur1 >> C1_SHIFT][cur2 >> C2_SHIFT]; @@ -1015,9 +1015,9 @@ pass2_fs_dither(j_decompress_ptr cinfo, JSAMPARRAY input_buf, register int pixcode = *cachep - 1; *outptr = (JSAMPLE)pixcode; /* Compute representation error for this pixel */ - cur0 -= GETJSAMPLE(colormap0[pixcode]); - cur1 -= GETJSAMPLE(colormap1[pixcode]); - cur2 -= GETJSAMPLE(colormap2[pixcode]); + cur0 -= colormap0[pixcode]; + cur1 -= colormap1[pixcode]; + cur2 -= colormap2[pixcode]; } /* Compute error fractions to be propagated to adjacent pixels. * Add these into the running sums, and simultaneously shift the @@ -1145,7 +1145,7 @@ start_pass_2_quant(j_decompress_ptr cinfo, boolean is_pre_scan) int i; /* Only F-S dithering or no dithering is supported. */ - /* If user asks for ordered dither, give him F-S. */ + /* If user asks for ordered dither, give them F-S. */ if (cinfo->dither_mode != JDITHER_NONE) cinfo->dither_mode = JDITHER_FS; @@ -1230,7 +1230,8 @@ jinit_2pass_quantizer(j_decompress_ptr cinfo) cquantize->error_limiter = NULL; /* Make sure jdmaster didn't give me a case I can't handle */ - if (cinfo->out_color_components != 3) + if (cinfo->out_color_components != 3 || + cinfo->out_color_space == JCS_RGB565) ERREXIT(cinfo, JERR_NOTIMPL); /* Allocate the histogram/inverse colormap storage */ @@ -1263,7 +1264,7 @@ jinit_2pass_quantizer(j_decompress_ptr cinfo) cquantize->sv_colormap = NULL; /* Only F-S dithering or no dithering is supported. */ - /* If user asks for ordered dither, give him F-S. */ + /* If user asks for ordered dither, give them F-S. */ if (cinfo->dither_mode != JDITHER_NONE) cinfo->dither_mode = JDITHER_FS; diff --git a/third-party/mozjpeg/mozjpeg/jsimd.h b/third-party/mozjpeg/mozjpeg/jsimd.h index 51e2b8c89de..74d480aa2c0 100644 --- a/third-party/mozjpeg/mozjpeg/jsimd.h +++ b/third-party/mozjpeg/mozjpeg/jsimd.h @@ -2,8 +2,9 @@ * jsimd.h * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2011, 2014, D. R. Commander. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2011, 2014, 2022, D. R. Commander. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. + * Copyright (C) 2020, Arm Limited. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -75,6 +76,7 @@ EXTERN(void) jsimd_int_upsample(j_decompress_ptr cinfo, EXTERN(int) jsimd_can_h2v2_fancy_upsample(void); EXTERN(int) jsimd_can_h2v1_fancy_upsample(void); +EXTERN(int) jsimd_can_h1v2_fancy_upsample(void); EXTERN(void) jsimd_h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, @@ -84,6 +86,10 @@ EXTERN(void) jsimd_h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr); +EXTERN(void) jsimd_h1v2_fancy_upsample(j_decompress_ptr cinfo, + jpeg_component_info *compptr, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr); EXTERN(int) jsimd_can_h2v2_merged_upsample(void); EXTERN(int) jsimd_can_h2v1_merged_upsample(void); @@ -108,10 +114,10 @@ EXTERN(int) jsimd_can_encode_mcu_AC_first_prepare(void); EXTERN(void) jsimd_encode_mcu_AC_first_prepare (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, - JCOEF *values, size_t *zerobits); + UJCOEF *values, size_t *zerobits); EXTERN(int) jsimd_can_encode_mcu_AC_refine_prepare(void); EXTERN(int) jsimd_encode_mcu_AC_refine_prepare (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, - JCOEF *absvalues, size_t *bits); + UJCOEF *absvalues, size_t *bits); diff --git a/third-party/mozjpeg/mozjpeg/jsimd_none.c b/third-party/mozjpeg/mozjpeg/jsimd_none.c index 3cb6c80f8aa..a25db73899c 100644 --- a/third-party/mozjpeg/mozjpeg/jsimd_none.c +++ b/third-party/mozjpeg/mozjpeg/jsimd_none.c @@ -2,8 +2,9 @@ * jsimd_none.c * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009-2011, 2014, D. R. Commander. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2009-2011, 2014, 2022, D. R. Commander. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. + * Copyright (C) 2020, Arm Limited. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -169,6 +170,12 @@ jsimd_can_h2v1_fancy_upsample(void) return 0; } +GLOBAL(int) +jsimd_can_h1v2_fancy_upsample(void) +{ + return 0; +} + GLOBAL(void) jsimd_h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) @@ -181,6 +188,12 @@ jsimd_h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, { } +GLOBAL(void) +jsimd_h1v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, + JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) +{ +} + GLOBAL(int) jsimd_can_h2v2_merged_upsample(void) { @@ -399,7 +412,7 @@ jsimd_can_encode_mcu_AC_first_prepare(void) GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { } @@ -412,7 +425,7 @@ jsimd_can_encode_mcu_AC_refine_prepare(void) GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { return 0; } diff --git a/third-party/mozjpeg/mozjpeg/jstdhuff.c b/third-party/mozjpeg/mozjpeg/jstdhuff.c index 036d6495a56..345b513d4dc 100644 --- a/third-party/mozjpeg/mozjpeg/jstdhuff.c +++ b/third-party/mozjpeg/mozjpeg/jstdhuff.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1998, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2013, D. R. Commander. + * Copyright (C) 2013, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -29,7 +29,7 @@ add_huff_table(j_common_ptr cinfo, JHUFF_TBL **htblptr, const UINT8 *bits, return; /* Copy the number-of-symbols-of-each-code-length counts */ - MEMCOPY((*htblptr)->bits, bits, sizeof((*htblptr)->bits)); + memcpy((*htblptr)->bits, bits, sizeof((*htblptr)->bits)); /* Validate the counts. We do this here mainly so we can copy the right * number of symbols from the val[] array, without risking marching off @@ -41,8 +41,9 @@ add_huff_table(j_common_ptr cinfo, JHUFF_TBL **htblptr, const UINT8 *bits, if (nsymbols < 1 || nsymbols > 256) ERREXIT(cinfo, JERR_BAD_HUFF_TABLE); - MEMCOPY((*htblptr)->huffval, val, nsymbols * sizeof(UINT8)); - MEMZERO(&((*htblptr)->huffval[nsymbols]), (256 - nsymbols) * sizeof(UINT8)); + memcpy((*htblptr)->huffval, val, nsymbols * sizeof(UINT8)); + memset(&((*htblptr)->huffval[nsymbols]), 0, + (256 - nsymbols) * sizeof(UINT8)); /* Initialize sent_table FALSE so table will be written to JPEG file. */ (*htblptr)->sent_table = FALSE; diff --git a/third-party/mozjpeg/mozjpeg/jutils.c b/third-party/mozjpeg/mozjpeg/jutils.c index 5c5bb17dc52..d86271624a6 100644 --- a/third-party/mozjpeg/mozjpeg/jutils.c +++ b/third-party/mozjpeg/mozjpeg/jutils.c @@ -3,8 +3,8 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1996, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code - * relevant to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -110,7 +110,7 @@ jcopy_sample_rows(JSAMPARRAY input_array, int source_row, for (row = num_rows; row > 0; row--) { inptr = *input_array++; outptr = *output_array++; - MEMCOPY(outptr, inptr, count); + memcpy(outptr, inptr, count); } } @@ -120,7 +120,7 @@ jcopy_block_row(JBLOCKROW input_row, JBLOCKROW output_row, JDIMENSION num_blocks) /* Copy a row of coefficient blocks from one place to another. */ { - MEMCOPY(output_row, input_row, num_blocks * (DCTSIZE2 * sizeof(JCOEF))); + memcpy(output_row, input_row, num_blocks * (DCTSIZE2 * sizeof(JCOEF))); } @@ -129,5 +129,5 @@ jzero_far(void *target, size_t bytestozero) /* Zero out a chunk of memory. */ /* This might be sample-array data, block-array data, or alloc_large data. */ { - MEMZERO(target, bytestozero); + memset(target, 0, bytestozero); } diff --git a/third-party/mozjpeg/mozjpeg/jversion.h b/third-party/mozjpeg/mozjpeg/jversion.h.in similarity index 67% rename from third-party/mozjpeg/mozjpeg/jversion.h rename to third-party/mozjpeg/mozjpeg/jversion.h.in index 22cf316e65d..6e4b01f4481 100644 --- a/third-party/mozjpeg/mozjpeg/jversion.h +++ b/third-party/mozjpeg/mozjpeg/jversion.h.in @@ -2,12 +2,13 @@ * jversion.h * * This file was part of the Independent JPEG Group's software: - * Copyright (C) 1991-2012, Thomas G. Lane, Guido Vollbeding. + * Copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2010, 2012-2020, D. R. Commander. + * Copyright (C) 2010, 2012-2023, D. R. Commander. + * For conditions of distribution and use, see the accompanying README.ijg + * file. * mozjpeg Modifications: * Copyright (C) 2014, Mozilla Corporation. - * For conditions of distribution and use, see the accompanying README file. * * This file contains software version identification. */ @@ -31,24 +32,25 @@ * NOTE: It is our convention to place the authors in the following order: * - libjpeg-turbo authors (2009-) in descending order of the date of their * most recent contribution to the project, then in ascending order of the - * date of their first contribution to the project + * date of their first contribution to the project, then in alphabetical + * order * - Upstream authors in descending order of the date of the first inclusion of * their code */ #define JCOPYRIGHT \ - "Copyright (C) 2009-2020 D. R. Commander\n" \ - "Copyright (C) 2011-2016 Siarhei Siamashka\n" \ + "Copyright (C) 2009-2023 D. R. Commander\n" \ + "Copyright (C) 2015, 2020 Google, Inc.\n" \ + "Copyright (C) 2019-2020 Arm Limited\n" \ "Copyright (C) 2015-2016, 2018 Matthieu Darbois\n" \ + "Copyright (C) 2011-2016 Siarhei Siamashka\n" \ "Copyright (C) 2015 Intel Corporation\n" \ - "Copyright (C) 2015 Google, Inc.\n" \ - "Copyright (C) 2014 Mozilla Corporation\n" \ + "Copyright (C) 2013-2014 Linaro Limited\n" \ "Copyright (C) 2013-2014 MIPS Technologies, Inc.\n" \ - "Copyright (C) 2013 Linaro Limited\n" \ + "Copyright (C) 2009, 2012 Pierre Ossman for Cendio AB\n" \ "Copyright (C) 2009-2011 Nokia Corporation and/or its subsidiary(-ies)\n" \ - "Copyright (C) 2009 Pierre Ossman for Cendio AB\n" \ "Copyright (C) 1999-2006 MIYASAKA Masaru\n" \ - "Copyright (C) 1991-2016 Thomas G. Lane, Guido Vollbeding" + "Copyright (C) 1991-2020 Thomas G. Lane, Guido Vollbeding" #define JCOPYRIGHT_SHORT \ - "Copyright (C) 1991-2020 The libjpeg-turbo Project and many others" + "Copyright (C) @COPYRIGHT_YEAR@ The libjpeg-turbo Project and many others" diff --git a/third-party/mozjpeg/mozjpeg/libjpeg.txt b/third-party/mozjpeg/mozjpeg/libjpeg.txt index c50cf906b00..309f9d3b463 100644 --- a/third-party/mozjpeg/mozjpeg/libjpeg.txt +++ b/third-party/mozjpeg/mozjpeg/libjpeg.txt @@ -3,7 +3,7 @@ USING THE IJG JPEG LIBRARY This file was part of the Independent JPEG Group's software: Copyright (C) 1994-2013, Thomas G. Lane, Guido Vollbeding. libjpeg-turbo Modifications: -Copyright (C) 2010, 2014-2018, D. R. Commander. +Copyright (C) 2010, 2014-2018, 2020, 2022, D. R. Commander. Copyright (C) 2015, Google, Inc. For conditions of distribution and use, see the accompanying README.ijg file. @@ -750,7 +750,9 @@ multiple rows in the JPEG image. Suspending data sources are not supported by this function. Calling jpeg_skip_scanlines() with a suspending data source will result in undefined -behavior. +behavior. Two-pass color quantization is also not supported by this function. +Calling jpeg_skip_scanlines() with two-pass color quantization enabled will +result in an error. jpeg_skip_scanlines() will not allow skipping past the bottom of the image. If the value of num_lines is large enough to skip past the bottom of the image, @@ -838,18 +840,7 @@ is to prepare a library file ("libjpeg.a", or a corresponding name on non-Unix machines) and reference it at your link step. If you use only half of the library (only compression or only decompression), only that much code will be included from the library, unless your linker is hopelessly brain-damaged. -The supplied makefiles build libjpeg.a automatically (see install.txt). - -While you can build the JPEG library as a shared library if the whim strikes -you, we don't really recommend it. The trouble with shared libraries is that -at some point you'll probably try to substitute a new version of the library -without recompiling the calling applications. That generally doesn't work -because the parameter struct declarations usually change with each new -version. In other words, the library's API is *not* guaranteed binary -compatible across versions; we only try to ensure source-code compatibility. -(In hindsight, it might have been smarter to hide the parameter structs from -applications and introduce a ton of access functions instead. Too late now, -however.) +The supplied build system builds libjpeg.a automatically. It may be worth pointing out that the core JPEG library does not actually require the stdio library: only the default source/destination managers and @@ -967,30 +958,38 @@ boolean arith_code J_DCT_METHOD dct_method Selects the algorithm used for the DCT step. Choices are: - JDCT_ISLOW: slow but accurate integer algorithm - JDCT_IFAST: faster, less accurate integer method - JDCT_FLOAT: floating-point method + JDCT_ISLOW: accurate integer method + JDCT_IFAST: less accurate integer method [legacy feature] + JDCT_FLOAT: floating-point method [legacy feature] JDCT_DEFAULT: default method (normally JDCT_ISLOW) JDCT_FASTEST: fastest method (normally JDCT_IFAST) - In libjpeg-turbo, JDCT_IFAST is generally about 5-15% faster than - JDCT_ISLOW when using the x86/x86-64 SIMD extensions (results may vary - with other SIMD implementations, or when using libjpeg-turbo without - SIMD extensions.) For quality levels of 90 and below, there should be - little or no perceptible difference between the two algorithms. For - quality levels above 90, however, the difference between JDCT_IFAST and + When the Independent JPEG Group's software was first released in 1991, + the compression time for a 1-megapixel JPEG image on a mainstream PC + was measured in minutes. Thus, JDCT_IFAST provided noticeable + performance benefits. On modern CPUs running libjpeg-turbo, however, + the compression time for a 1-megapixel JPEG image is measured in + milliseconds, and thus the performance benefits of JDCT_IFAST are much + less noticeable. On modern x86/x86-64 CPUs that support AVX2 + instructions, JDCT_IFAST and JDCT_ISLOW have similar performance. On + other types of CPUs, JDCT_IFAST is generally about 5-15% faster than + JDCT_ISLOW. + + For quality levels of 90 and below, there should be little or no + perceptible quality difference between the two algorithms. For quality + levels above 90, however, the difference between JDCT_IFAST and JDCT_ISLOW becomes more pronounced. With quality=97, for instance, - JDCT_IFAST incurs generally about a 1-3 dB loss (in PSNR) relative to + JDCT_IFAST incurs generally about a 1-3 dB loss in PSNR relative to JDCT_ISLOW, but this can be larger for some images. Do not use JDCT_IFAST with quality levels above 97. The algorithm often degenerates at quality=98 and above and can actually produce a more lossy image than if lower quality levels had been used. Also, in libjpeg-turbo, JDCT_IFAST is not fully accelerated for quality levels - above 97, so it will be slower than JDCT_ISLOW. JDCT_FLOAT is mainly a - legacy feature. It does not produce significantly more accurate - results than the ISLOW method, and it is much slower. The FLOAT method - may also give different results on different machines due to varying - roundoff behavior, whereas the integer methods should give the same - results on all machines. + above 97, so it will be slower than JDCT_ISLOW. + + JDCT_FLOAT does not produce significantly more accurate results than + JDCT_ISLOW, and it is much slower. JDCT_FLOAT may also give different + results on different machines due to varying roundoff behavior, whereas + the integer methods should give the same results on all machines. J_COLOR_SPACE jpeg_color_space int num_components @@ -1268,31 +1267,39 @@ Additional decompression parameters that the application may set include: J_DCT_METHOD dct_method Selects the algorithm used for the DCT step. Choices are: - JDCT_ISLOW: slow but accurate integer algorithm - JDCT_IFAST: faster, less accurate integer method - JDCT_FLOAT: floating-point method + JDCT_ISLOW: accurate integer method + JDCT_IFAST: less accurate integer method [legacy feature] + JDCT_FLOAT: floating-point method [legacy feature] JDCT_DEFAULT: default method (normally JDCT_ISLOW) JDCT_FASTEST: fastest method (normally JDCT_IFAST) - In libjpeg-turbo, JDCT_IFAST is generally about 5-15% faster than - JDCT_ISLOW when using the x86/x86-64 SIMD extensions (results may vary - with other SIMD implementations, or when using libjpeg-turbo without - SIMD extensions.) If the JPEG image was compressed using a quality - level of 85 or below, then there should be little or no perceptible - difference between the two algorithms. When decompressing images that - were compressed using quality levels above 85, however, the difference + When the Independent JPEG Group's software was first released in 1991, + the decompression time for a 1-megapixel JPEG image on a mainstream PC + was measured in minutes. Thus, JDCT_IFAST provided noticeable + performance benefits. On modern CPUs running libjpeg-turbo, however, + the decompression time for a 1-megapixel JPEG image is measured in + milliseconds, and thus the performance benefits of JDCT_IFAST are much + less noticeable. On modern x86/x86-64 CPUs that support AVX2 + instructions, JDCT_IFAST and JDCT_ISLOW have similar performance. On + other types of CPUs, JDCT_IFAST is generally about 5-15% faster than + JDCT_ISLOW. + + If the JPEG image was compressed using a quality level of 85 or below, + then there should be little or no perceptible quality difference + between the two algorithms. When decompressing images that were + compressed using quality levels above 85, however, the difference between JDCT_IFAST and JDCT_ISLOW becomes more pronounced. With images compressed using quality=97, for instance, JDCT_IFAST incurs generally - about a 4-6 dB loss (in PSNR) relative to JDCT_ISLOW, but this can be + about a 4-6 dB loss in PSNR relative to JDCT_ISLOW, but this can be larger for some images. If you can avoid it, do not use JDCT_IFAST when decompressing images that were compressed using quality levels above 97. The algorithm often degenerates for such images and can actually produce a more lossy output image than if the JPEG image had - been compressed using lower quality levels. JDCT_FLOAT is mainly a - legacy feature. It does not produce significantly more accurate - results than the ISLOW method, and it is much slower. The FLOAT method - may also give different results on different machines due to varying - roundoff behavior, whereas the integer methods should give the same - results on all machines. + been compressed using lower quality levels. + + JDCT_FLOAT does not produce significantly more accurate results than + JDCT_ISLOW, and it is much slower. JDCT_FLOAT may also give different + results on different machines due to varying roundoff behavior, whereas + the integer methods should give the same results on all machines. boolean do_fancy_upsampling If TRUE, do careful upsampling of chroma components. If FALSE, @@ -3057,9 +3064,8 @@ BITS_IN_JSAMPLE as 12 rather than 8. Note that this causes JSAMPLE to be larger than a char, so it affects the surrounding application's image data. The sample applications cjpeg and djpeg can support 12-bit mode only for PPM and GIF file formats; you must disable the other file formats to compile a -12-bit cjpeg or djpeg. (install.txt has more information about that.) -At present, a 12-bit library can handle *only* 12-bit images, not both -precisions. +12-bit cjpeg or djpeg. At present, a 12-bit library can handle *only* 12-bit +images, not both precisions. Note that a 12-bit library always compresses in Huffman optimization mode, in order to generate valid Huffman tables. This is necessary because our diff --git a/third-party/mozjpeg/mozjpeg/md5/md5hl.c b/third-party/mozjpeg/mozjpeg/md5/md5hl.c index 8a4a762fab8..849a1366778 100644 --- a/third-party/mozjpeg/mozjpeg/md5/md5hl.c +++ b/third-party/mozjpeg/mozjpeg/md5/md5hl.c @@ -6,7 +6,7 @@ * this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp * ---------------------------------------------------------------------------- * libjpeg-turbo Modifications: - * Copyright (C)2016, 2018-2019 D. R. Commander. All Rights Reserved. + * Copyright (C)2016, 2018-2019, 2022 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,6 +34,10 @@ * ---------------------------------------------------------------------------- */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #include #include #include diff --git a/third-party/mozjpeg/mozjpeg/rdbmp.c b/third-party/mozjpeg/mozjpeg/rdbmp.c index edde5fa1136..29e3880f680 100644 --- a/third-party/mozjpeg/mozjpeg/rdbmp.c +++ b/third-party/mozjpeg/mozjpeg/rdbmp.c @@ -6,13 +6,13 @@ * Modified 2009-2017 by Guido Vollbeding. * libjpeg-turbo Modifications: * Modified 2011 by Siarhei Siamashka. - * Copyright (C) 2015, 2017-2018, D. R. Commander. + * Copyright (C) 2015, 2017-2018, 2021-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * * This file contains routines to read input images in Microsoft "BMP" * format (MS Windows 3.x, OS/2 1.x, and OS/2 2.x flavors). - * Currently, only 8-bit and 24-bit images are supported, not 1-bit or + * Currently, only 8-, 24-, and 32-bit images are supported, not 1-bit or * 4-bit (feeding such low-depth images into JPEG would be silly anyway). * Also, we don't support RLE-compressed files. * @@ -34,22 +34,12 @@ /* Macros to deal with unsigned chars as efficiently as compiler allows */ -#ifdef HAVE_UNSIGNED_CHAR typedef unsigned char U_CHAR; #define UCH(x) ((int)(x)) -#else /* !HAVE_UNSIGNED_CHAR */ -#ifdef __CHAR_UNSIGNED__ -typedef char U_CHAR; -#define UCH(x) ((int)(x)) -#else -typedef char U_CHAR; -#define UCH(x) ((int)(x) & 0xFF) -#endif -#endif /* HAVE_UNSIGNED_CHAR */ #define ReadOK(file, buffer, len) \ - (JFREAD(file, buffer, len) == ((size_t)(len))) + (fread(buffer, 1, len, file) == ((size_t)(len))) static int alpha_index[JPEG_NUMCS] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 0, 0, -1 @@ -71,7 +61,7 @@ typedef struct _bmp_source_struct { JDIMENSION source_row; /* Current source row number */ JDIMENSION row_width; /* Physical width of scanlines in file */ - int bits_per_pixel; /* remembers 8- or 24-bit format */ + int bits_per_pixel; /* remembers 8-, 24-, or 32-bit format */ int cmap_length; /* colormap length */ boolean use_inversion_array; /* TRUE = preload the whole image, which is @@ -135,7 +125,8 @@ read_colormap(bmp_source_ptr sinfo, int cmaplen, int mapentrysize) break; } - if (sinfo->cinfo->in_color_space == JCS_UNKNOWN && gray) + if ((sinfo->cinfo->in_color_space == JCS_UNKNOWN || + sinfo->cinfo->in_color_space == JCS_RGB) && gray) sinfo->cinfo->in_color_space = JCS_GRAYSCALE; if (sinfo->cinfo->in_color_space == JCS_GRAYSCALE && !gray) @@ -179,14 +170,14 @@ get_8bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) outptr = source->pub.buffer[0]; if (cinfo->in_color_space == JCS_GRAYSCALE) { for (col = cinfo->image_width; col > 0; col--) { - t = GETJSAMPLE(*inptr++); + t = *inptr++; if (t >= cmaplen) ERREXIT(cinfo, JERR_BMP_OUTOFRANGE); *outptr++ = colormap[0][t]; } } else if (cinfo->in_color_space == JCS_CMYK) { for (col = cinfo->image_width; col > 0; col--) { - t = GETJSAMPLE(*inptr++); + t = *inptr++; if (t >= cmaplen) ERREXIT(cinfo, JERR_BMP_OUTOFRANGE); rgb_to_cmyk(colormap[0][t], colormap[1][t], colormap[2][t], outptr, @@ -202,7 +193,7 @@ get_8bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) if (aindex >= 0) { for (col = cinfo->image_width; col > 0; col--) { - t = GETJSAMPLE(*inptr++); + t = *inptr++; if (t >= cmaplen) ERREXIT(cinfo, JERR_BMP_OUTOFRANGE); outptr[rindex] = colormap[0][t]; @@ -213,7 +204,7 @@ get_8bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) } } else { for (col = cinfo->image_width; col > 0; col--) { - t = GETJSAMPLE(*inptr++); + t = *inptr++; if (t >= cmaplen) ERREXIT(cinfo, JERR_BMP_OUTOFRANGE); outptr[rindex] = colormap[0][t]; @@ -255,10 +246,9 @@ get_24bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) */ outptr = source->pub.buffer[0]; if (cinfo->in_color_space == JCS_EXT_BGR) { - MEMCOPY(outptr, inptr, source->row_width); + memcpy(outptr, inptr, source->row_width); } else if (cinfo->in_color_space == JCS_CMYK) { for (col = cinfo->image_width; col > 0; col--) { - /* can omit GETJSAMPLE() safely */ JSAMPLE b = *inptr++, g = *inptr++, r = *inptr++; rgb_to_cmyk(r, g, b, outptr, outptr + 1, outptr + 2, outptr + 3); outptr += 4; @@ -272,7 +262,7 @@ get_24bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) if (aindex >= 0) { for (col = cinfo->image_width; col > 0; col--) { - outptr[bindex] = *inptr++; /* can omit GETJSAMPLE() safely */ + outptr[bindex] = *inptr++; outptr[gindex] = *inptr++; outptr[rindex] = *inptr++; outptr[aindex] = 0xFF; @@ -280,7 +270,7 @@ get_24bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) } } else { for (col = cinfo->image_width; col > 0; col--) { - outptr[bindex] = *inptr++; /* can omit GETJSAMPLE() safely */ + outptr[bindex] = *inptr++; outptr[gindex] = *inptr++; outptr[rindex] = *inptr++; outptr += ps; @@ -320,10 +310,9 @@ get_32bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) outptr = source->pub.buffer[0]; if (cinfo->in_color_space == JCS_EXT_BGRX || cinfo->in_color_space == JCS_EXT_BGRA) { - MEMCOPY(outptr, inptr, source->row_width); + memcpy(outptr, inptr, source->row_width); } else if (cinfo->in_color_space == JCS_CMYK) { for (col = cinfo->image_width; col > 0; col--) { - /* can omit GETJSAMPLE() safely */ JSAMPLE b = *inptr++, g = *inptr++, r = *inptr++; rgb_to_cmyk(r, g, b, outptr, outptr + 1, outptr + 2, outptr + 3); inptr++; /* skip the 4th byte (Alpha channel) */ @@ -338,7 +327,7 @@ get_32bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) if (aindex >= 0) { for (col = cinfo->image_width; col > 0; col--) { - outptr[bindex] = *inptr++; /* can omit GETJSAMPLE() safely */ + outptr[bindex] = *inptr++; outptr[gindex] = *inptr++; outptr[rindex] = *inptr++; outptr[aindex] = *inptr++; @@ -346,7 +335,7 @@ get_32bit_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) } } else { for (col = cinfo->image_width; col > 0; col--) { - outptr[bindex] = *inptr++; /* can omit GETJSAMPLE() safely */ + outptr[bindex] = *inptr++; outptr[gindex] = *inptr++; outptr[rindex] = *inptr++; inptr++; /* skip the 4th byte (Alpha channel) */ @@ -436,14 +425,14 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) (((unsigned int)UCH(array[offset + 2])) << 16) + \ (((unsigned int)UCH(array[offset + 3])) << 24)) - unsigned int bfOffBits; - unsigned int headerSize; + int bfOffBits; + int headerSize; int biWidth; int biHeight; unsigned short biPlanes; unsigned int biCompression; int biXPelsPerMeter, biYPelsPerMeter; - unsigned int biClrUsed = 0; + int biClrUsed = 0; int mapentrysize = 0; /* 0 indicates no colormap */ int bPad; JDIMENSION row_width = 0; @@ -462,7 +451,7 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) if (!ReadOK(source->pub.input_file, bmpinfoheader, 4)) ERREXIT(cinfo, JERR_INPUT_EOF); headerSize = GET_4B(bmpinfoheader, 0); - if (headerSize < 12 || headerSize > 64) + if (headerSize < 12 || headerSize > 64 || (headerSize + 14) > bfOffBits) ERREXIT(cinfo, JERR_BMP_BADHEADER); if (!ReadOK(source->pub.input_file, bmpinfoheader + 4, headerSize - 4)) ERREXIT(cinfo, JERR_INPUT_EOF); @@ -481,7 +470,9 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) TRACEMS2(cinfo, 1, JTRC_BMP_OS2_MAPPED, biWidth, biHeight); break; case 24: /* RGB image */ - TRACEMS2(cinfo, 1, JTRC_BMP_OS2, biWidth, biHeight); + case 32: /* RGB image + Alpha channel */ + TRACEMS3(cinfo, 1, JTRC_BMP_OS2, biWidth, biHeight, + source->bits_per_pixel); break; default: ERREXIT(cinfo, JERR_BMP_BADDEPTH); @@ -508,10 +499,8 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) TRACEMS2(cinfo, 1, JTRC_BMP_MAPPED, biWidth, biHeight); break; case 24: /* RGB image */ - TRACEMS2(cinfo, 1, JTRC_BMP, biWidth, biHeight); - break; case 32: /* RGB image + Alpha channel */ - TRACEMS2(cinfo, 1, JTRC_BMP, biWidth, biHeight); + TRACEMS3(cinfo, 1, JTRC_BMP, biWidth, biHeight, source->bits_per_pixel); break; default: ERREXIT(cinfo, JERR_BMP_BADDEPTH); @@ -534,6 +523,11 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) if (biWidth <= 0 || biHeight <= 0 || biWidth > 0x7fffffffL || biHeight > 0x7fffffffL) ERREXIT(cinfo, JERR_BMP_EMPTY); +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if (sinfo->max_pixels && + (unsigned long long)biWidth * biHeight > sinfo->max_pixels) + ERREXIT(cinfo, JERR_WIDTH_OVERFLOW); +#endif if (biPlanes != 1) ERREXIT(cinfo, JERR_BMP_BADPLANES); @@ -587,7 +581,9 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) cinfo->input_components = 4; else ERREXIT(cinfo, JERR_BAD_IN_COLORSPACE); - row_width = (JDIMENSION)(biWidth * 3); + if ((unsigned long long)biWidth * 3ULL > 0xFFFFFFFFULL) + ERREXIT(cinfo, JERR_WIDTH_OVERFLOW); + row_width = (JDIMENSION)biWidth * 3; break; case 32: if (cinfo->in_color_space == JCS_UNKNOWN) @@ -598,7 +594,9 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) cinfo->input_components = 4; else ERREXIT(cinfo, JERR_BAD_IN_COLORSPACE); - row_width = (JDIMENSION)(biWidth * 4); + if ((unsigned long long)biWidth * 4ULL > 0xFFFFFFFFULL) + ERREXIT(cinfo, JERR_WIDTH_OVERFLOW); + row_width = (JDIMENSION)biWidth * 4; break; default: ERREXIT(cinfo, JERR_BMP_BADDEPTH); @@ -643,7 +641,7 @@ start_input_bmp(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) /* Allocate one-row buffer for returned data */ source->pub.buffer = (*cinfo->mem->alloc_sarray) ((j_common_ptr)cinfo, JPOOL_IMAGE, - (JDIMENSION)(biWidth * cinfo->input_components), (JDIMENSION)1); + (JDIMENSION)biWidth * (JDIMENSION)cinfo->input_components, (JDIMENSION)1); source->pub.buffer_height = 1; cinfo->data_precision = 8; @@ -680,6 +678,9 @@ jinit_read_bmp(j_compress_ptr cinfo, boolean use_inversion_array) /* Fill in method ptrs, except get_pixel_rows which start_input sets */ source->pub.start_input = start_input_bmp; source->pub.finish_input = finish_input_bmp; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + source->pub.max_pixels = 0; +#endif source->use_inversion_array = use_inversion_array; diff --git a/third-party/mozjpeg/mozjpeg/rdcolmap.c b/third-party/mozjpeg/mozjpeg/rdcolmap.c index cbbef59d5f5..d2ed95cf804 100644 --- a/third-party/mozjpeg/mozjpeg/rdcolmap.c +++ b/third-party/mozjpeg/mozjpeg/rdcolmap.c @@ -54,9 +54,8 @@ add_map_entry(j_decompress_ptr cinfo, int R, int G, int B) /* Check for duplicate color. */ for (index = 0; index < ncolors; index++) { - if (GETJSAMPLE(colormap0[index]) == R && - GETJSAMPLE(colormap1[index]) == G && - GETJSAMPLE(colormap2[index]) == B) + if (colormap0[index] == R && colormap1[index] == G && + colormap2[index] == B) return; /* color is already in map */ } diff --git a/third-party/mozjpeg/mozjpeg/rdgif.c b/third-party/mozjpeg/mozjpeg/rdgif.c index ff9258d6eb1..bdf74012414 100644 --- a/third-party/mozjpeg/mozjpeg/rdgif.c +++ b/third-party/mozjpeg/mozjpeg/rdgif.c @@ -1,29 +1,698 @@ /* * rdgif.c * + * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. - * This file is part of the Independent JPEG Group's software. + * Modified 2019 by Guido Vollbeding. + * libjpeg-turbo Modifications: + * Copyright (C) 2021-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * * This file contains routines to read input images in GIF format. * - ***************************************************************************** - * NOTE: to avoid entanglements with Unisys' patent on LZW compression, * - * the ability to read GIF files has been removed from the IJG distribution. * - * Sorry about that. * - ***************************************************************************** - * - * We are required to state that - * "The Graphics Interchange Format(c) is the Copyright property of - * CompuServe Incorporated. GIF(sm) is a Service Mark property of - * CompuServe Incorporated." + * These routines may need modification for non-Unix environments or + * specialized applications. As they stand, they assume input from + * an ordinary stdio stream. They further assume that reading begins + * at the start of the file; start_input may need work if the + * user interface has already read some data (e.g., to determine that + * the file is indeed GIF format). + */ + +/* + * This code is loosely based on giftoppm from the PBMPLUS distribution + * of Feb. 1991. That file contains the following copyright notice: + * +-------------------------------------------------------------------+ + * | Copyright 1990, David Koblas. | + * | Permission to use, copy, modify, and distribute this software | + * | and its documentation for any purpose and without fee is hereby | + * | granted, provided that the above copyright notice appear in all | + * | copies and that both that copyright notice and this permission | + * | notice appear in supporting documentation. This software is | + * | provided "as is" without express or implied warranty. | + * +-------------------------------------------------------------------+ */ #include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ #ifdef GIF_SUPPORTED + +/* Macros to deal with unsigned chars as efficiently as compiler allows */ + +typedef unsigned char U_CHAR; +#define UCH(x) ((int)(x)) + + +#define ReadOK(file, buffer, len) \ + (fread(buffer, 1, len, file) == ((size_t)(len))) + + +#define MAXCOLORMAPSIZE 256 /* max # of colors in a GIF colormap */ +#define NUMCOLORS 3 /* # of colors */ +#define CM_RED 0 /* color component numbers */ +#define CM_GREEN 1 +#define CM_BLUE 2 + +#define MAX_LZW_BITS 12 /* maximum LZW code size */ +#define LZW_TABLE_SIZE (1 << MAX_LZW_BITS) /* # of possible LZW symbols */ + +/* Macros for extracting header data --- note we assume chars may be signed */ + +#define LM_to_uint(array, offset) \ + ((unsigned int)UCH(array[offset]) + \ + (((unsigned int)UCH(array[offset + 1])) << 8)) + +#define BitSet(byte, bit) ((byte) & (bit)) +#define INTERLACE 0x40 /* mask for bit signifying interlaced image */ +#define COLORMAPFLAG 0x80 /* mask for bit signifying colormap presence */ + + +/* + * LZW decompression tables look like this: + * symbol_head[K] = prefix symbol of any LZW symbol K (0..LZW_TABLE_SIZE-1) + * symbol_tail[K] = suffix byte of any LZW symbol K (0..LZW_TABLE_SIZE-1) + * Note that entries 0..end_code of the above tables are not used, + * since those symbols represent raw bytes or special codes. + * + * The stack represents the not-yet-used expansion of the last LZW symbol. + * In the worst case, a symbol could expand to as many bytes as there are + * LZW symbols, so we allocate LZW_TABLE_SIZE bytes for the stack. + * (This is conservative since that number includes the raw-byte symbols.) + */ + + +/* Private version of data source object */ + +typedef struct { + struct cjpeg_source_struct pub; /* public fields */ + + j_compress_ptr cinfo; /* back link saves passing separate parm */ + + JSAMPARRAY colormap; /* GIF colormap (converted to my format) */ + + /* State for GetCode and LZWReadByte */ + U_CHAR code_buf[256 + 4]; /* current input data block */ + int last_byte; /* # of bytes in code_buf */ + int last_bit; /* # of bits in code_buf */ + int cur_bit; /* next bit index to read */ + boolean first_time; /* flags first call to GetCode */ + boolean out_of_blocks; /* TRUE if hit terminator data block */ + + int input_code_size; /* codesize given in GIF file */ + int clear_code, end_code; /* values for Clear and End codes */ + + int code_size; /* current actual code size */ + int limit_code; /* 2^code_size */ + int max_code; /* first unused code value */ + + /* Private state for LZWReadByte */ + int oldcode; /* previous LZW symbol */ + int firstcode; /* first byte of oldcode's expansion */ + + /* LZW symbol table and expansion stack */ + UINT16 *symbol_head; /* => table of prefix symbols */ + UINT8 *symbol_tail; /* => table of suffix bytes */ + UINT8 *symbol_stack; /* => stack for symbol expansions */ + UINT8 *sp; /* stack pointer */ + + /* State for interlaced image processing */ + boolean is_interlaced; /* TRUE if have interlaced image */ + jvirt_sarray_ptr interlaced_image; /* full image in interlaced order */ + JDIMENSION cur_row_number; /* need to know actual row number */ + JDIMENSION pass2_offset; /* # of pixel rows in pass 1 */ + JDIMENSION pass3_offset; /* # of pixel rows in passes 1&2 */ + JDIMENSION pass4_offset; /* # of pixel rows in passes 1,2,3 */ +} gif_source_struct; + +typedef gif_source_struct *gif_source_ptr; + + +/* Forward declarations */ +METHODDEF(JDIMENSION) get_pixel_rows(j_compress_ptr cinfo, + cjpeg_source_ptr sinfo); +METHODDEF(JDIMENSION) load_interlaced_image(j_compress_ptr cinfo, + cjpeg_source_ptr sinfo); +METHODDEF(JDIMENSION) get_interlaced_row(j_compress_ptr cinfo, + cjpeg_source_ptr sinfo); + + +LOCAL(int) +ReadByte(gif_source_ptr sinfo) +/* Read next byte from GIF file */ +{ + register FILE *infile = sinfo->pub.input_file; + register int c; + + if ((c = getc(infile)) == EOF) + ERREXIT(sinfo->cinfo, JERR_INPUT_EOF); + return c; +} + + +LOCAL(int) +GetDataBlock(gif_source_ptr sinfo, U_CHAR *buf) +/* Read a GIF data block, which has a leading count byte */ +/* A zero-length block marks the end of a data block sequence */ +{ + int count; + + count = ReadByte(sinfo); + if (count > 0) { + if (!ReadOK(sinfo->pub.input_file, buf, count)) + ERREXIT(sinfo->cinfo, JERR_INPUT_EOF); + } + return count; +} + + +LOCAL(void) +SkipDataBlocks(gif_source_ptr sinfo) +/* Skip a series of data blocks, until a block terminator is found */ +{ + U_CHAR buf[256]; + + while (GetDataBlock(sinfo, buf) > 0) + /* skip */; +} + + +LOCAL(void) +ReInitLZW(gif_source_ptr sinfo) +/* (Re)initialize LZW state; shared code for startup and Clear processing */ +{ + sinfo->code_size = sinfo->input_code_size + 1; + sinfo->limit_code = sinfo->clear_code << 1; /* 2^code_size */ + sinfo->max_code = sinfo->clear_code + 2; /* first unused code value */ + sinfo->sp = sinfo->symbol_stack; /* init stack to empty */ +} + + +LOCAL(void) +InitLZWCode(gif_source_ptr sinfo) +/* Initialize for a series of LZWReadByte (and hence GetCode) calls */ +{ + /* GetCode initialization */ + sinfo->last_byte = 2; /* make safe to "recopy last two bytes" */ + sinfo->code_buf[0] = 0; + sinfo->code_buf[1] = 0; + sinfo->last_bit = 0; /* nothing in the buffer */ + sinfo->cur_bit = 0; /* force buffer load on first call */ + sinfo->first_time = TRUE; + sinfo->out_of_blocks = FALSE; + + /* LZWReadByte initialization: */ + /* compute special code values (note that these do not change later) */ + sinfo->clear_code = 1 << sinfo->input_code_size; + sinfo->end_code = sinfo->clear_code + 1; + ReInitLZW(sinfo); +} + + +LOCAL(int) +GetCode(gif_source_ptr sinfo) +/* Fetch the next code_size bits from the GIF data */ +/* We assume code_size is less than 16 */ +{ + register int accum; + int offs, count; + + while (sinfo->cur_bit + sinfo->code_size > sinfo->last_bit) { + /* Time to reload the buffer */ + /* First time, share code with Clear case */ + if (sinfo->first_time) { + sinfo->first_time = FALSE; + return sinfo->clear_code; + } + if (sinfo->out_of_blocks) { + WARNMS(sinfo->cinfo, JWRN_GIF_NOMOREDATA); + return sinfo->end_code; /* fake something useful */ + } + /* preserve last two bytes of what we have -- assume code_size <= 16 */ + sinfo->code_buf[0] = sinfo->code_buf[sinfo->last_byte-2]; + sinfo->code_buf[1] = sinfo->code_buf[sinfo->last_byte-1]; + /* Load more bytes; set flag if we reach the terminator block */ + if ((count = GetDataBlock(sinfo, &sinfo->code_buf[2])) == 0) { + sinfo->out_of_blocks = TRUE; + WARNMS(sinfo->cinfo, JWRN_GIF_NOMOREDATA); + return sinfo->end_code; /* fake something useful */ + } + /* Reset counters */ + sinfo->cur_bit = (sinfo->cur_bit - sinfo->last_bit) + 16; + sinfo->last_byte = 2 + count; + sinfo->last_bit = sinfo->last_byte * 8; + } + + /* Form up next 24 bits in accum */ + offs = sinfo->cur_bit >> 3; /* byte containing cur_bit */ + accum = UCH(sinfo->code_buf[offs + 2]); + accum <<= 8; + accum |= UCH(sinfo->code_buf[offs + 1]); + accum <<= 8; + accum |= UCH(sinfo->code_buf[offs]); + + /* Right-align cur_bit in accum, then mask off desired number of bits */ + accum >>= (sinfo->cur_bit & 7); + sinfo->cur_bit += sinfo->code_size; + return accum & ((1 << sinfo->code_size) - 1); +} + + +LOCAL(int) +LZWReadByte(gif_source_ptr sinfo) +/* Read an LZW-compressed byte */ +{ + register int code; /* current working code */ + int incode; /* saves actual input code */ + + /* If any codes are stacked from a previously read symbol, return them */ + if (sinfo->sp > sinfo->symbol_stack) + return (int)(*(--sinfo->sp)); + + /* Time to read a new symbol */ + code = GetCode(sinfo); + + if (code == sinfo->clear_code) { + /* Reinit state, swallow any extra Clear codes, and */ + /* return next code, which is expected to be a raw byte. */ + ReInitLZW(sinfo); + do { + code = GetCode(sinfo); + } while (code == sinfo->clear_code); + if (code > sinfo->clear_code) { /* make sure it is a raw byte */ + WARNMS(sinfo->cinfo, JWRN_GIF_BADDATA); + code = 0; /* use something valid */ + } + /* make firstcode, oldcode valid! */ + sinfo->firstcode = sinfo->oldcode = code; + return code; + } + + if (code == sinfo->end_code) { + /* Skip the rest of the image, unless GetCode already read terminator */ + if (!sinfo->out_of_blocks) { + SkipDataBlocks(sinfo); + sinfo->out_of_blocks = TRUE; + } + /* Complain that there's not enough data */ + WARNMS(sinfo->cinfo, JWRN_GIF_ENDCODE); + /* Pad data with 0's */ + return 0; /* fake something usable */ + } + + /* Got normal raw byte or LZW symbol */ + incode = code; /* save for a moment */ + + if (code >= sinfo->max_code) { /* special case for not-yet-defined symbol */ + /* code == max_code is OK; anything bigger is bad data */ + if (code > sinfo->max_code) { + WARNMS(sinfo->cinfo, JWRN_GIF_BADDATA); + incode = 0; /* prevent creation of loops in symbol table */ + } + /* this symbol will be defined as oldcode/firstcode */ + *(sinfo->sp++) = (UINT8)sinfo->firstcode; + code = sinfo->oldcode; + } + + /* If it's a symbol, expand it into the stack */ + while (code >= sinfo->clear_code) { + *(sinfo->sp++) = sinfo->symbol_tail[code]; /* tail is a byte value */ + code = sinfo->symbol_head[code]; /* head is another LZW symbol */ + } + /* At this point code just represents a raw byte */ + sinfo->firstcode = code; /* save for possible future use */ + + /* If there's room in table... */ + if ((code = sinfo->max_code) < LZW_TABLE_SIZE) { + /* Define a new symbol = prev sym + head of this sym's expansion */ + sinfo->symbol_head[code] = (UINT16)sinfo->oldcode; + sinfo->symbol_tail[code] = (UINT8)sinfo->firstcode; + sinfo->max_code++; + /* Is it time to increase code_size? */ + if (sinfo->max_code >= sinfo->limit_code && + sinfo->code_size < MAX_LZW_BITS) { + sinfo->code_size++; + sinfo->limit_code <<= 1; /* keep equal to 2^code_size */ + } + } + + sinfo->oldcode = incode; /* save last input symbol for future use */ + return sinfo->firstcode; /* return first byte of symbol's expansion */ +} + + +LOCAL(void) +ReadColorMap(gif_source_ptr sinfo, int cmaplen, JSAMPARRAY cmap) +/* Read a GIF colormap */ +{ + int i, gray = 1; + + for (i = 0; i < cmaplen; i++) { +#if BITS_IN_JSAMPLE == 8 +#define UPSCALE(x) (x) +#else +#define UPSCALE(x) ((x) << (BITS_IN_JSAMPLE - 8)) +#endif + cmap[CM_RED][i] = (JSAMPLE)UPSCALE(ReadByte(sinfo)); + cmap[CM_GREEN][i] = (JSAMPLE)UPSCALE(ReadByte(sinfo)); + cmap[CM_BLUE][i] = (JSAMPLE)UPSCALE(ReadByte(sinfo)); + if (cmap[CM_RED][i] != cmap[CM_GREEN][i] || + cmap[CM_GREEN][i] != cmap[CM_BLUE][i]) + gray = 0; + } + + if (sinfo->cinfo->in_color_space == JCS_RGB && gray) { + sinfo->cinfo->in_color_space = JCS_GRAYSCALE; + sinfo->cinfo->input_components = 1; + } +} + + +LOCAL(void) +DoExtension(gif_source_ptr sinfo) +/* Process an extension block */ +/* Currently we ignore 'em all */ +{ + int extlabel; + + /* Read extension label byte */ + extlabel = ReadByte(sinfo); + TRACEMS1(sinfo->cinfo, 1, JTRC_GIF_EXTENSION, extlabel); + /* Skip the data block(s) associated with the extension */ + SkipDataBlocks(sinfo); +} + + +/* + * Read the file header; return image size and component count. + */ + +METHODDEF(void) +start_input_gif(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) +{ + gif_source_ptr source = (gif_source_ptr)sinfo; + U_CHAR hdrbuf[10]; /* workspace for reading control blocks */ + unsigned int width, height; /* image dimensions */ + int colormaplen, aspectRatio; + int c; + + /* Read and verify GIF Header */ + if (!ReadOK(source->pub.input_file, hdrbuf, 6)) + ERREXIT(cinfo, JERR_GIF_NOT); + if (hdrbuf[0] != 'G' || hdrbuf[1] != 'I' || hdrbuf[2] != 'F') + ERREXIT(cinfo, JERR_GIF_NOT); + /* Check for expected version numbers. + * If unknown version, give warning and try to process anyway; + * this is per recommendation in GIF89a standard. + */ + if ((hdrbuf[3] != '8' || hdrbuf[4] != '7' || hdrbuf[5] != 'a') && + (hdrbuf[3] != '8' || hdrbuf[4] != '9' || hdrbuf[5] != 'a')) + TRACEMS3(cinfo, 1, JTRC_GIF_BADVERSION, hdrbuf[3], hdrbuf[4], hdrbuf[5]); + + /* Read and decipher Logical Screen Descriptor */ + if (!ReadOK(source->pub.input_file, hdrbuf, 7)) + ERREXIT(cinfo, JERR_INPUT_EOF); + width = LM_to_uint(hdrbuf, 0); + height = LM_to_uint(hdrbuf, 2); + if (width == 0 || height == 0) + ERREXIT(cinfo, JERR_GIF_EMPTY); +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if (sinfo->max_pixels && + (unsigned long long)width * height > sinfo->max_pixels) + ERREXIT(cinfo, JERR_WIDTH_OVERFLOW); +#endif + /* we ignore the color resolution, sort flag, and background color index */ + aspectRatio = UCH(hdrbuf[6]); + if (aspectRatio != 0 && aspectRatio != 49) + TRACEMS(cinfo, 1, JTRC_GIF_NONSQUARE); + + /* Allocate space to store the colormap */ + source->colormap = (*cinfo->mem->alloc_sarray) + ((j_common_ptr)cinfo, JPOOL_IMAGE, (JDIMENSION)MAXCOLORMAPSIZE, + (JDIMENSION)NUMCOLORS); + colormaplen = 0; /* indicate initialization */ + + /* Read global colormap if header indicates it is present */ + if (BitSet(hdrbuf[4], COLORMAPFLAG)) { + colormaplen = 2 << (hdrbuf[4] & 0x07); + ReadColorMap(source, colormaplen, source->colormap); + } + + /* Scan until we reach start of desired image. + * We don't currently support skipping images, but could add it easily. + */ + for (;;) { + c = ReadByte(source); + + if (c == ';') /* GIF terminator?? */ + ERREXIT(cinfo, JERR_GIF_IMAGENOTFOUND); + + if (c == '!') { /* Extension */ + DoExtension(source); + continue; + } + + if (c != ',') { /* Not an image separator? */ + WARNMS1(cinfo, JWRN_GIF_CHAR, c); + continue; + } + + /* Read and decipher Local Image Descriptor */ + if (!ReadOK(source->pub.input_file, hdrbuf, 9)) + ERREXIT(cinfo, JERR_INPUT_EOF); + /* we ignore top/left position info, also sort flag */ + width = LM_to_uint(hdrbuf, 4); + height = LM_to_uint(hdrbuf, 6); + if (width == 0 || height == 0) + ERREXIT(cinfo, JERR_GIF_EMPTY); +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if (sinfo->max_pixels && + (unsigned long long)width * height > sinfo->max_pixels) + ERREXIT(cinfo, JERR_WIDTH_OVERFLOW); +#endif + source->is_interlaced = (BitSet(hdrbuf[8], INTERLACE) != 0); + + /* Read local colormap if header indicates it is present */ + /* Note: if we wanted to support skipping images, */ + /* we'd need to skip rather than read colormap for ignored images */ + if (BitSet(hdrbuf[8], COLORMAPFLAG)) { + colormaplen = 2 << (hdrbuf[8] & 0x07); + ReadColorMap(source, colormaplen, source->colormap); + } + + source->input_code_size = ReadByte(source); /* get min-code-size byte */ + if (source->input_code_size < 2 || source->input_code_size > 8) + ERREXIT1(cinfo, JERR_GIF_CODESIZE, source->input_code_size); + + /* Reached desired image, so break out of loop */ + /* If we wanted to skip this image, */ + /* we'd call SkipDataBlocks and then continue the loop */ + break; + } + + /* Prepare to read selected image: first initialize LZW decompressor */ + source->symbol_head = (UINT16 *) + (*cinfo->mem->alloc_large) ((j_common_ptr)cinfo, JPOOL_IMAGE, + LZW_TABLE_SIZE * sizeof(UINT16)); + source->symbol_tail = (UINT8 *) + (*cinfo->mem->alloc_large) ((j_common_ptr)cinfo, JPOOL_IMAGE, + LZW_TABLE_SIZE * sizeof(UINT8)); + source->symbol_stack = (UINT8 *) + (*cinfo->mem->alloc_large) ((j_common_ptr)cinfo, JPOOL_IMAGE, + LZW_TABLE_SIZE * sizeof(UINT8)); + InitLZWCode(source); + + /* + * If image is interlaced, we read it into a full-size sample array, + * decompressing as we go; then get_interlaced_row selects rows from the + * sample array in the proper order. + */ + if (source->is_interlaced) { + /* We request the virtual array now, but can't access it until virtual + * arrays have been allocated. Hence, the actual work of reading the + * image is postponed until the first call to get_pixel_rows. + */ + source->interlaced_image = (*cinfo->mem->request_virt_sarray) + ((j_common_ptr)cinfo, JPOOL_IMAGE, FALSE, + (JDIMENSION)width, (JDIMENSION)height, (JDIMENSION)1); + if (cinfo->progress != NULL) { + cd_progress_ptr progress = (cd_progress_ptr)cinfo->progress; + progress->total_extra_passes++; /* count file input as separate pass */ + } + source->pub.get_pixel_rows = load_interlaced_image; + } else { + source->pub.get_pixel_rows = get_pixel_rows; + } + + if (cinfo->in_color_space != JCS_GRAYSCALE) { + cinfo->in_color_space = JCS_RGB; + cinfo->input_components = NUMCOLORS; + } + + /* Create compressor input buffer. */ + source->pub.buffer = (*cinfo->mem->alloc_sarray) + ((j_common_ptr)cinfo, JPOOL_IMAGE, + (JDIMENSION)width * cinfo->input_components, (JDIMENSION)1); + source->pub.buffer_height = 1; + + /* Pad colormap for safety. */ + for (c = colormaplen; c < source->clear_code; c++) { + source->colormap[CM_RED][c] = + source->colormap[CM_GREEN][c] = + source->colormap[CM_BLUE][c] = CENTERJSAMPLE; + } + + /* Return info about the image. */ + cinfo->data_precision = BITS_IN_JSAMPLE; /* we always rescale data to this */ + cinfo->image_width = width; + cinfo->image_height = height; + + TRACEMS3(cinfo, 1, JTRC_GIF, width, height, colormaplen); +} + + +/* + * Read one row of pixels. + * This version is used for noninterlaced GIF images: + * we read directly from the GIF file. + */ + +METHODDEF(JDIMENSION) +get_pixel_rows(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) +{ + gif_source_ptr source = (gif_source_ptr)sinfo; + register int c; + register JSAMPROW ptr; + register JDIMENSION col; + register JSAMPARRAY colormap = source->colormap; + + ptr = source->pub.buffer[0]; + if (cinfo->in_color_space == JCS_GRAYSCALE) { + for (col = cinfo->image_width; col > 0; col--) { + c = LZWReadByte(source); + *ptr++ = colormap[CM_RED][c]; + } + } else { + for (col = cinfo->image_width; col > 0; col--) { + c = LZWReadByte(source); + *ptr++ = colormap[CM_RED][c]; + *ptr++ = colormap[CM_GREEN][c]; + *ptr++ = colormap[CM_BLUE][c]; + } + } + return 1; +} + + +/* + * Read one row of pixels. + * This version is used for the first call on get_pixel_rows when + * reading an interlaced GIF file: we read the whole image into memory. + */ + +METHODDEF(JDIMENSION) +load_interlaced_image(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) +{ + gif_source_ptr source = (gif_source_ptr)sinfo; + register JSAMPROW sptr; + register JDIMENSION col; + JDIMENSION row; + cd_progress_ptr progress = (cd_progress_ptr)cinfo->progress; + + /* Read the interlaced image into the virtual array we've created. */ + for (row = 0; row < cinfo->image_height; row++) { + if (progress != NULL) { + progress->pub.pass_counter = (long)row; + progress->pub.pass_limit = (long)cinfo->image_height; + (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); + } + sptr = *(*cinfo->mem->access_virt_sarray) + ((j_common_ptr)cinfo, source->interlaced_image, row, (JDIMENSION)1, + TRUE); + for (col = cinfo->image_width; col > 0; col--) { + *sptr++ = (JSAMPLE)LZWReadByte(source); + } + } + if (progress != NULL) + progress->completed_extra_passes++; + + /* Replace method pointer so subsequent calls don't come here. */ + source->pub.get_pixel_rows = get_interlaced_row; + /* Initialize for get_interlaced_row, and perform first call on it. */ + source->cur_row_number = 0; + source->pass2_offset = (cinfo->image_height + 7) / 8; + source->pass3_offset = source->pass2_offset + (cinfo->image_height + 3) / 8; + source->pass4_offset = source->pass3_offset + (cinfo->image_height + 1) / 4; + + return get_interlaced_row(cinfo, sinfo); +} + + +/* + * Read one row of pixels. + * This version is used for interlaced GIF images: + * we read from the virtual array. + */ + +METHODDEF(JDIMENSION) +get_interlaced_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) +{ + gif_source_ptr source = (gif_source_ptr)sinfo; + register int c; + register JSAMPROW sptr, ptr; + register JDIMENSION col; + register JSAMPARRAY colormap = source->colormap; + JDIMENSION irow; + + /* Figure out which row of interlaced image is needed, and access it. */ + switch ((int)(source->cur_row_number & 7)) { + case 0: /* first-pass row */ + irow = source->cur_row_number >> 3; + break; + case 4: /* second-pass row */ + irow = (source->cur_row_number >> 3) + source->pass2_offset; + break; + case 2: /* third-pass row */ + case 6: + irow = (source->cur_row_number >> 2) + source->pass3_offset; + break; + default: /* fourth-pass row */ + irow = (source->cur_row_number >> 1) + source->pass4_offset; + } + sptr = *(*cinfo->mem->access_virt_sarray) + ((j_common_ptr)cinfo, source->interlaced_image, irow, (JDIMENSION)1, + FALSE); + /* Scan the row, expand colormap, and output */ + ptr = source->pub.buffer[0]; + if (cinfo->in_color_space == JCS_GRAYSCALE) { + for (col = cinfo->image_width; col > 0; col--) { + c = *sptr++; + *ptr++ = colormap[CM_RED][c]; + } + } else { + for (col = cinfo->image_width; col > 0; col--) { + c = *sptr++; + *ptr++ = colormap[CM_RED][c]; + *ptr++ = colormap[CM_GREEN][c]; + *ptr++ = colormap[CM_BLUE][c]; + } + } + source->cur_row_number++; /* for next time */ + return 1; +} + + +/* + * Finish up at the end of the file. + */ + +METHODDEF(void) +finish_input_gif(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) +{ + /* no work */ +} + + /* * The module selection routine for GIF format input. */ @@ -31,9 +700,21 @@ GLOBAL(cjpeg_source_ptr) jinit_read_gif(j_compress_ptr cinfo) { - fprintf(stderr, "GIF input is unsupported for legal reasons. Sorry.\n"); - exit(EXIT_FAILURE); - return NULL; /* keep compiler happy */ + gif_source_ptr source; + + /* Create module interface object */ + source = (gif_source_ptr) + (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, + sizeof(gif_source_struct)); + source->cinfo = cinfo; /* make back link for subroutines */ + /* Fill in method ptrs, except get_pixel_rows which start_input sets */ + source->pub.start_input = start_input_gif; + source->pub.finish_input = finish_input_gif; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + source->pub.max_pixels = 0; +#endif + + return (cjpeg_source_ptr)source; } #endif /* GIF_SUPPORTED */ diff --git a/third-party/mozjpeg/mozjpeg/rdjpgcom.c b/third-party/mozjpeg/mozjpeg/rdjpgcom.c index e9f31d2ab8f..4de67ccddfe 100644 --- a/third-party/mozjpeg/mozjpeg/rdjpgcom.c +++ b/third-party/mozjpeg/mozjpeg/rdjpgcom.c @@ -4,8 +4,8 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1994-1997, Thomas G. Lane. * Modified 2009 by Bill Allombert, Guido Vollbeding. - * It was modified by The libjpeg-turbo Project to include only code relevant - * to libjpeg-turbo. + * libjpeg-turbo Modifications: + * Copyright (C) 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -15,12 +15,14 @@ * JPEG markers. */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #define JPEG_CJPEG_DJPEG /* to get the command-line config symbols */ #include "jinclude.h" /* get auto-config symbols, */ -#ifdef HAVE_LOCALE_H #include /* Bill Allombert: use locale for isprint */ -#endif #include /* to declare isupper(), tolower() */ #ifdef USE_SETMODE #include /* to declare setmode()'s parameter macros */ @@ -28,16 +30,6 @@ #include /* to declare setmode() */ #endif -#ifdef USE_CCOMMAND /* command-line reader for Macintosh */ -#ifdef __MWERKS__ -#include /* Metrowerks needs this */ -#include /* ... and this */ -#endif -#ifdef THINK_C -#include /* Think declares it here */ -#endif -#endif - #ifdef DONT_USE_B_MODE /* define mode parameters for fopen() */ #define READ_BINARY "r" #else @@ -224,9 +216,7 @@ process_COM(int raw) int lastch = 0; /* Bill Allombert: set locale properly for isprint */ -#ifdef HAVE_LOCALE_H setlocale(LC_CTYPE, ""); -#endif /* Get the marker parameter length count */ length = read_2_bytes(); @@ -254,7 +244,7 @@ process_COM(int raw) } else if (isprint(ch)) { putc(ch, stdout); } else { - printf("\\%03o", ch); + printf("\\%03o", (unsigned int)ch); } lastch = ch; length--; @@ -262,9 +252,7 @@ process_COM(int raw) printf("\n"); /* Bill Allombert: revert to C locale */ -#ifdef HAVE_LOCALE_H setlocale(LC_CTYPE, "C"); -#endif } @@ -453,11 +441,6 @@ main(int argc, char **argv) char *arg; int verbose = 0, raw = 0; - /* On Mac, fetch a command line. */ -#ifdef USE_CCOMMAND - argc = ccommand(&argv); -#endif - progname = argv[0]; if (progname == NULL || progname[0] == 0) progname = "rdjpgcom"; /* in case C library doesn't provide it */ diff --git a/third-party/mozjpeg/mozjpeg/rdppm.c b/third-party/mozjpeg/mozjpeg/rdppm.c index 87bc33090e5..883641d89b1 100644 --- a/third-party/mozjpeg/mozjpeg/rdppm.c +++ b/third-party/mozjpeg/mozjpeg/rdppm.c @@ -5,7 +5,7 @@ * Copyright (C) 1991-1997, Thomas G. Lane. * Modified 2009 by Bill Allombert, Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2015-2017, D. R. Commander. + * Copyright (C) 2015-2017, 2020-2023, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -43,22 +43,12 @@ /* Macros to deal with unsigned chars as efficiently as compiler allows */ -#ifdef HAVE_UNSIGNED_CHAR typedef unsigned char U_CHAR; #define UCH(x) ((int)(x)) -#else /* !HAVE_UNSIGNED_CHAR */ -#ifdef __CHAR_UNSIGNED__ -typedef char U_CHAR; -#define UCH(x) ((int)(x)) -#else -typedef char U_CHAR; -#define UCH(x) ((int)(x) & 0xFF) -#endif -#endif /* HAVE_UNSIGNED_CHAR */ #define ReadOK(file, buffer, len) \ - (JFREAD(file, buffer, len) == ((size_t)(len))) + (fread(buffer, 1, len, file) == ((size_t)(len))) static int alpha_index[JPEG_NUMCS] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 0, 0, -1 @@ -122,11 +112,10 @@ read_pbm_integer(j_compress_ptr cinfo, FILE *infile, unsigned int maxval) while ((ch = pbm_getc(infile)) >= '0' && ch <= '9') { val *= 10; val += ch - '0'; + if (val > maxval) + ERREXIT(cinfo, JERR_PPM_OUTOFRANGE); } - if (val > maxval) - ERREXIT(cinfo, JERR_PPM_OUTOFRANGE); - return val; } @@ -189,16 +178,16 @@ get_text_gray_rgb_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) ptr = source->pub.buffer[0]; if (maxval == MAXJSAMPLE) { if (aindex >= 0) - GRAY_RGB_READ_LOOP(read_pbm_integer(cinfo, infile, maxval), - ptr[aindex] = 0xFF;) + GRAY_RGB_READ_LOOP((JSAMPLE)read_pbm_integer(cinfo, infile, maxval), + ptr[aindex] = MAXJSAMPLE;) else - GRAY_RGB_READ_LOOP(read_pbm_integer(cinfo, infile, maxval),) + GRAY_RGB_READ_LOOP((JSAMPLE)read_pbm_integer(cinfo, infile, maxval), {}) } else { if (aindex >= 0) GRAY_RGB_READ_LOOP(rescale[read_pbm_integer(cinfo, infile, maxval)], - ptr[aindex] = 0xFF;) + ptr[aindex] = MAXJSAMPLE;) else - GRAY_RGB_READ_LOOP(rescale[read_pbm_integer(cinfo, infile, maxval)],) + GRAY_RGB_READ_LOOP(rescale[read_pbm_integer(cinfo, infile, maxval)], {}) } return 1; } @@ -219,7 +208,7 @@ get_text_gray_cmyk_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) ptr = source->pub.buffer[0]; if (maxval == MAXJSAMPLE) { for (col = cinfo->image_width; col > 0; col--) { - JSAMPLE gray = read_pbm_integer(cinfo, infile, maxval); + JSAMPLE gray = (JSAMPLE)read_pbm_integer(cinfo, infile, maxval); rgb_to_cmyk(gray, gray, gray, ptr, ptr + 1, ptr + 2, ptr + 3); ptr += 4; } @@ -263,16 +252,16 @@ get_text_rgb_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) ptr = source->pub.buffer[0]; if (maxval == MAXJSAMPLE) { if (aindex >= 0) - RGB_READ_LOOP(read_pbm_integer(cinfo, infile, maxval), - ptr[aindex] = 0xFF;) + RGB_READ_LOOP((JSAMPLE)read_pbm_integer(cinfo, infile, maxval), + ptr[aindex] = MAXJSAMPLE;) else - RGB_READ_LOOP(read_pbm_integer(cinfo, infile, maxval),) + RGB_READ_LOOP((JSAMPLE)read_pbm_integer(cinfo, infile, maxval), {}) } else { if (aindex >= 0) RGB_READ_LOOP(rescale[read_pbm_integer(cinfo, infile, maxval)], - ptr[aindex] = 0xFF;) + ptr[aindex] = MAXJSAMPLE;) else - RGB_READ_LOOP(rescale[read_pbm_integer(cinfo, infile, maxval)],) + RGB_READ_LOOP(rescale[read_pbm_integer(cinfo, infile, maxval)], {}) } return 1; } @@ -293,9 +282,9 @@ get_text_rgb_cmyk_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) ptr = source->pub.buffer[0]; if (maxval == MAXJSAMPLE) { for (col = cinfo->image_width; col > 0; col--) { - JSAMPLE r = read_pbm_integer(cinfo, infile, maxval); - JSAMPLE g = read_pbm_integer(cinfo, infile, maxval); - JSAMPLE b = read_pbm_integer(cinfo, infile, maxval); + JSAMPLE r = (JSAMPLE)read_pbm_integer(cinfo, infile, maxval); + JSAMPLE g = (JSAMPLE)read_pbm_integer(cinfo, infile, maxval); + JSAMPLE b = (JSAMPLE)read_pbm_integer(cinfo, infile, maxval); rgb_to_cmyk(r, g, b, ptr, ptr + 1, ptr + 2, ptr + 3); ptr += 4; } @@ -356,14 +345,14 @@ get_gray_rgb_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) bufferptr = source->iobuffer; if (maxval == MAXJSAMPLE) { if (aindex >= 0) - GRAY_RGB_READ_LOOP(*bufferptr++, ptr[aindex] = 0xFF;) + GRAY_RGB_READ_LOOP(*bufferptr++, ptr[aindex] = MAXJSAMPLE;) else - GRAY_RGB_READ_LOOP(*bufferptr++,) + GRAY_RGB_READ_LOOP(*bufferptr++, {}) } else { if (aindex >= 0) - GRAY_RGB_READ_LOOP(rescale[UCH(*bufferptr++)], ptr[aindex] = 0xFF;) + GRAY_RGB_READ_LOOP(rescale[UCH(*bufferptr++)], ptr[aindex] = MAXJSAMPLE;) else - GRAY_RGB_READ_LOOP(rescale[UCH(*bufferptr++)],) + GRAY_RGB_READ_LOOP(rescale[UCH(*bufferptr++)], {}) } return 1; } @@ -424,14 +413,14 @@ get_rgb_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) bufferptr = source->iobuffer; if (maxval == MAXJSAMPLE) { if (aindex >= 0) - RGB_READ_LOOP(*bufferptr++, ptr[aindex] = 0xFF;) + RGB_READ_LOOP(*bufferptr++, ptr[aindex] = MAXJSAMPLE;) else - RGB_READ_LOOP(*bufferptr++,) + RGB_READ_LOOP(*bufferptr++, {}) } else { if (aindex >= 0) - RGB_READ_LOOP(rescale[UCH(*bufferptr++)], ptr[aindex] = 0xFF;) + RGB_READ_LOOP(rescale[UCH(*bufferptr++)], ptr[aindex] = MAXJSAMPLE;) else - RGB_READ_LOOP(rescale[UCH(*bufferptr++)],) + RGB_READ_LOOP(rescale[UCH(*bufferptr++)], {}) } return 1; } @@ -526,6 +515,11 @@ get_word_rgb_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) register JSAMPLE *rescale = source->rescale; JDIMENSION col; unsigned int maxval = source->maxval; + register int rindex = rgb_red[cinfo->in_color_space]; + register int gindex = rgb_green[cinfo->in_color_space]; + register int bindex = rgb_blue[cinfo->in_color_space]; + register int aindex = alpha_index[cinfo->in_color_space]; + register int ps = rgb_pixelsize[cinfo->in_color_space]; if (!ReadOK(source->pub.input_file, source->iobuffer, source->buffer_width)) ERREXIT(cinfo, JERR_INPUT_EOF); @@ -537,17 +531,20 @@ get_word_rgb_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) temp |= UCH(*bufferptr++); if (temp > maxval) ERREXIT(cinfo, JERR_PPM_OUTOFRANGE); - *ptr++ = rescale[temp]; + ptr[rindex] = rescale[temp]; temp = UCH(*bufferptr++) << 8; temp |= UCH(*bufferptr++); if (temp > maxval) ERREXIT(cinfo, JERR_PPM_OUTOFRANGE); - *ptr++ = rescale[temp]; + ptr[gindex] = rescale[temp]; temp = UCH(*bufferptr++) << 8; temp |= UCH(*bufferptr++); if (temp > maxval) ERREXIT(cinfo, JERR_PPM_OUTOFRANGE); - *ptr++ = rescale[temp]; + ptr[bindex] = rescale[temp]; + if (aindex >= 0) + ptr[aindex] = MAXJSAMPLE; + ptr += ps; } return 1; } @@ -589,6 +586,10 @@ start_input_ppm(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) if (w <= 0 || h <= 0 || maxval <= 0) /* error check */ ERREXIT(cinfo, JERR_PPM_NOT); +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if (sinfo->max_pixels && (unsigned long long)w * h > sinfo->max_pixels) + ERREXIT(cinfo, JERR_WIDTH_OVERFLOW); +#endif cinfo->data_precision = BITS_IN_JSAMPLE; /* we always rescale data to this */ cinfo->image_width = (JDIMENSION)w; @@ -602,7 +603,8 @@ start_input_ppm(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) switch (c) { case '2': /* it's a text-format PGM file */ - if (cinfo->in_color_space == JCS_UNKNOWN) + if (cinfo->in_color_space == JCS_UNKNOWN || + cinfo->in_color_space == JCS_RGB) cinfo->in_color_space = JCS_GRAYSCALE; TRACEMS2(cinfo, 1, JTRC_PGM_TEXT, w, h); if (cinfo->in_color_space == JCS_GRAYSCALE) @@ -630,11 +632,15 @@ start_input_ppm(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) break; case '5': /* it's a raw-format PGM file */ - if (cinfo->in_color_space == JCS_UNKNOWN) + if (cinfo->in_color_space == JCS_UNKNOWN || + cinfo->in_color_space == JCS_RGB) cinfo->in_color_space = JCS_GRAYSCALE; TRACEMS2(cinfo, 1, JTRC_PGM, w, h); if (maxval > 255) { - source->pub.get_pixel_rows = get_word_gray_row; + if (cinfo->in_color_space == JCS_GRAYSCALE) + source->pub.get_pixel_rows = get_word_gray_row; + else + ERREXIT(cinfo, JERR_BAD_IN_COLORSPACE); } else if (maxval == MAXJSAMPLE && sizeof(JSAMPLE) == sizeof(U_CHAR) && cinfo->in_color_space == JCS_GRAYSCALE) { source->pub.get_pixel_rows = get_raw_row; @@ -657,13 +663,17 @@ start_input_ppm(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) cinfo->in_color_space = JCS_EXT_RGB; TRACEMS2(cinfo, 1, JTRC_PPM, w, h); if (maxval > 255) { - source->pub.get_pixel_rows = get_word_rgb_row; + if (IsExtRGB(cinfo->in_color_space)) + source->pub.get_pixel_rows = get_word_rgb_row; + else + ERREXIT(cinfo, JERR_BAD_IN_COLORSPACE); } else if (maxval == MAXJSAMPLE && sizeof(JSAMPLE) == sizeof(U_CHAR) && - (cinfo->in_color_space == JCS_EXT_RGB #if RGB_RED == 0 && RGB_GREEN == 1 && RGB_BLUE == 2 && RGB_PIXELSIZE == 3 - || cinfo->in_color_space == JCS_RGB + (cinfo->in_color_space == JCS_EXT_RGB || + cinfo->in_color_space == JCS_RGB)) { +#else + cinfo->in_color_space == JCS_EXT_RGB) { #endif - )) { source->pub.get_pixel_rows = get_raw_row; use_raw_buffer = TRUE; need_rescale = FALSE; @@ -720,8 +730,10 @@ start_input_ppm(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) /* On 16-bit-int machines we have to be careful of maxval = 65535 */ source->rescale = (JSAMPLE *) (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, - (size_t)(((long)maxval + 1L) * + (size_t)(((long)MAX(maxval, 255) + 1L) * sizeof(JSAMPLE))); + memset(source->rescale, 0, (size_t)(((long)MAX(maxval, 255) + 1L) * + sizeof(JSAMPLE))); half_maxval = maxval / 2; for (val = 0; val <= (long)maxval; val++) { /* The multiplication here must be done in 32 bits to avoid overflow */ @@ -759,6 +771,9 @@ jinit_read_ppm(j_compress_ptr cinfo) /* Fill in method ptrs, except get_pixel_rows which start_input sets */ source->pub.start_input = start_input_ppm; source->pub.finish_input = finish_input_ppm; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + source->pub.max_pixels = 0; +#endif return (cjpeg_source_ptr)source; } diff --git a/third-party/mozjpeg/mozjpeg/rdrle.c b/third-party/mozjpeg/mozjpeg/rdrle.c deleted file mode 100644 index b6945146a02..00000000000 --- a/third-party/mozjpeg/mozjpeg/rdrle.c +++ /dev/null @@ -1,389 +0,0 @@ -/* - * rdrle.c - * - * This file was part of the Independent JPEG Group's software: - * Copyright (C) 1991-1996, Thomas G. Lane. - * It was modified by The libjpeg-turbo Project to include only code and - * information relevant to libjpeg-turbo. - * For conditions of distribution and use, see the accompanying README.ijg - * file. - * - * This file contains routines to read input images in Utah RLE format. - * The Utah Raster Toolkit library is required (version 3.1 or later). - * - * These routines may need modification for non-Unix environments or - * specialized applications. As they stand, they assume input from - * an ordinary stdio stream. They further assume that reading begins - * at the start of the file; start_input may need work if the - * user interface has already read some data (e.g., to determine that - * the file is indeed RLE format). - * - * Based on code contributed by Mike Lijewski, - * with updates from Robert Hutchinson. - */ - -#include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ - -#ifdef RLE_SUPPORTED - -/* rle.h is provided by the Utah Raster Toolkit. */ - -#include - -/* - * We assume that JSAMPLE has the same representation as rle_pixel, - * to wit, "unsigned char". Hence we can't cope with 12- or 16-bit samples. - */ - -#if BITS_IN_JSAMPLE != 8 - Sorry, this code only copes with 8-bit JSAMPLEs. /* deliberate syntax err */ -#endif - -/* - * We support the following types of RLE files: - * - * GRAYSCALE - 8 bits, no colormap - * MAPPEDGRAY - 8 bits, 1 channel colomap - * PSEUDOCOLOR - 8 bits, 3 channel colormap - * TRUECOLOR - 24 bits, 3 channel colormap - * DIRECTCOLOR - 24 bits, no colormap - * - * For now, we ignore any alpha channel in the image. - */ - -typedef enum - { GRAYSCALE, MAPPEDGRAY, PSEUDOCOLOR, TRUECOLOR, DIRECTCOLOR } rle_kind; - - -/* - * Since RLE stores scanlines bottom-to-top, we have to invert the image - * to conform to JPEG's top-to-bottom order. To do this, we read the - * incoming image into a virtual array on the first get_pixel_rows call, - * then fetch the required row from the virtual array on subsequent calls. - */ - -typedef struct _rle_source_struct *rle_source_ptr; - -typedef struct _rle_source_struct { - struct cjpeg_source_struct pub; /* public fields */ - - rle_kind visual; /* actual type of input file */ - jvirt_sarray_ptr image; /* virtual array to hold the image */ - JDIMENSION row; /* current row # in the virtual array */ - rle_hdr header; /* Input file information */ - rle_pixel **rle_row; /* holds a row returned by rle_getrow() */ - -} rle_source_struct; - - -/* - * Read the file header; return image size and component count. - */ - -METHODDEF(void) -start_input_rle(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) -{ - rle_source_ptr source = (rle_source_ptr)sinfo; - JDIMENSION width, height; -#ifdef PROGRESS_REPORT - cd_progress_ptr progress = (cd_progress_ptr)cinfo->progress; -#endif - - /* Use RLE library routine to get the header info */ - source->header = *rle_hdr_init(NULL); - source->header.rle_file = source->pub.input_file; - switch (rle_get_setup(&(source->header))) { - case RLE_SUCCESS: - /* A-OK */ - break; - case RLE_NOT_RLE: - ERREXIT(cinfo, JERR_RLE_NOT); - break; - case RLE_NO_SPACE: - ERREXIT(cinfo, JERR_RLE_MEM); - break; - case RLE_EMPTY: - ERREXIT(cinfo, JERR_RLE_EMPTY); - break; - case RLE_EOF: - ERREXIT(cinfo, JERR_RLE_EOF); - break; - default: - ERREXIT(cinfo, JERR_RLE_BADERROR); - break; - } - - /* Figure out what we have, set private vars and return values accordingly */ - - width = source->header.xmax - source->header.xmin + 1; - height = source->header.ymax - source->header.ymin + 1; - source->header.xmin = 0; /* realign horizontally */ - source->header.xmax = width - 1; - - cinfo->image_width = width; - cinfo->image_height = height; - cinfo->data_precision = 8; /* we can only handle 8 bit data */ - - if (source->header.ncolors == 1 && source->header.ncmap == 0) { - source->visual = GRAYSCALE; - TRACEMS2(cinfo, 1, JTRC_RLE_GRAY, width, height); - } else if (source->header.ncolors == 1 && source->header.ncmap == 1) { - source->visual = MAPPEDGRAY; - TRACEMS3(cinfo, 1, JTRC_RLE_MAPGRAY, width, height, - 1 << source->header.cmaplen); - } else if (source->header.ncolors == 1 && source->header.ncmap == 3) { - source->visual = PSEUDOCOLOR; - TRACEMS3(cinfo, 1, JTRC_RLE_MAPPED, width, height, - 1 << source->header.cmaplen); - } else if (source->header.ncolors == 3 && source->header.ncmap == 3) { - source->visual = TRUECOLOR; - TRACEMS3(cinfo, 1, JTRC_RLE_FULLMAP, width, height, - 1 << source->header.cmaplen); - } else if (source->header.ncolors == 3 && source->header.ncmap == 0) { - source->visual = DIRECTCOLOR; - TRACEMS2(cinfo, 1, JTRC_RLE, width, height); - } else - ERREXIT(cinfo, JERR_RLE_UNSUPPORTED); - - if (source->visual == GRAYSCALE || source->visual == MAPPEDGRAY) { - cinfo->in_color_space = JCS_GRAYSCALE; - cinfo->input_components = 1; - } else { - cinfo->in_color_space = JCS_RGB; - cinfo->input_components = 3; - } - - /* - * A place to hold each scanline while it's converted. - * (GRAYSCALE scanlines don't need converting) - */ - if (source->visual != GRAYSCALE) { - source->rle_row = (rle_pixel **)(*cinfo->mem->alloc_sarray) - ((j_common_ptr)cinfo, JPOOL_IMAGE, - (JDIMENSION)width, (JDIMENSION)cinfo->input_components); - } - - /* request a virtual array to hold the image */ - source->image = (*cinfo->mem->request_virt_sarray) - ((j_common_ptr)cinfo, JPOOL_IMAGE, FALSE, - (JDIMENSION)(width * source->header.ncolors), - (JDIMENSION)height, (JDIMENSION)1); - -#ifdef PROGRESS_REPORT - if (progress != NULL) { - /* count file input as separate pass */ - progress->total_extra_passes++; - } -#endif - - source->pub.buffer_height = 1; -} - - -/* - * Read one row of pixels. - * Called only after load_image has read the image into the virtual array. - * Used for GRAYSCALE, MAPPEDGRAY, TRUECOLOR, and DIRECTCOLOR images. - */ - -METHODDEF(JDIMENSION) -get_rle_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) -{ - rle_source_ptr source = (rle_source_ptr)sinfo; - - source->row--; - source->pub.buffer = (*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, source->image, source->row, (JDIMENSION)1, FALSE); - - return 1; -} - -/* - * Read one row of pixels. - * Called only after load_image has read the image into the virtual array. - * Used for PSEUDOCOLOR images. - */ - -METHODDEF(JDIMENSION) -get_pseudocolor_row(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) -{ - rle_source_ptr source = (rle_source_ptr)sinfo; - JSAMPROW src_row, dest_row; - JDIMENSION col; - rle_map *colormap; - int val; - - colormap = source->header.cmap; - dest_row = source->pub.buffer[0]; - source->row--; - src_row = *(*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, source->image, source->row, (JDIMENSION)1, FALSE); - - for (col = cinfo->image_width; col > 0; col--) { - val = GETJSAMPLE(*src_row++); - *dest_row++ = (JSAMPLE)(colormap[val ] >> 8); - *dest_row++ = (JSAMPLE)(colormap[val + 256] >> 8); - *dest_row++ = (JSAMPLE)(colormap[val + 512] >> 8); - } - - return 1; -} - - -/* - * Load the image into a virtual array. We have to do this because RLE - * files start at the lower left while the JPEG standard has them starting - * in the upper left. This is called the first time we want to get a row - * of input. What we do is load the RLE data into the array and then call - * the appropriate routine to read one row from the array. Before returning, - * we set source->pub.get_pixel_rows so that subsequent calls go straight to - * the appropriate row-reading routine. - */ - -METHODDEF(JDIMENSION) -load_image(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) -{ - rle_source_ptr source = (rle_source_ptr)sinfo; - JDIMENSION row, col; - JSAMPROW scanline, red_ptr, green_ptr, blue_ptr; - rle_pixel **rle_row; - rle_map *colormap; - char channel; -#ifdef PROGRESS_REPORT - cd_progress_ptr progress = (cd_progress_ptr)cinfo->progress; -#endif - - colormap = source->header.cmap; - rle_row = source->rle_row; - - /* Read the RLE data into our virtual array. - * We assume here that rle_pixel is represented the same as JSAMPLE. - */ - RLE_CLR_BIT(source->header, RLE_ALPHA); /* don't read the alpha channel */ - -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->pub.pass_limit = cinfo->image_height; - progress->pub.pass_counter = 0; - (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); - } -#endif - - switch (source->visual) { - - case GRAYSCALE: - case PSEUDOCOLOR: - for (row = 0; row < cinfo->image_height; row++) { - rle_row = (rle_pixel **)(*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, source->image, row, (JDIMENSION)1, TRUE); - rle_getrow(&source->header, rle_row); -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->pub.pass_counter++; - (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); - } -#endif - } - break; - - case MAPPEDGRAY: - case TRUECOLOR: - for (row = 0; row < cinfo->image_height; row++) { - scanline = *(*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, source->image, row, (JDIMENSION)1, TRUE); - rle_row = source->rle_row; - rle_getrow(&source->header, rle_row); - - for (col = 0; col < cinfo->image_width; col++) { - for (channel = 0; channel < source->header.ncolors; channel++) { - *scanline++ = (JSAMPLE) - (colormap[GETJSAMPLE(rle_row[channel][col]) + 256 * channel] >> 8); - } - } - -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->pub.pass_counter++; - (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); - } -#endif - } - break; - - case DIRECTCOLOR: - for (row = 0; row < cinfo->image_height; row++) { - scanline = *(*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, source->image, row, (JDIMENSION)1, TRUE); - rle_getrow(&source->header, rle_row); - - red_ptr = rle_row[0]; - green_ptr = rle_row[1]; - blue_ptr = rle_row[2]; - - for (col = cinfo->image_width; col > 0; col--) { - *scanline++ = *red_ptr++; - *scanline++ = *green_ptr++; - *scanline++ = *blue_ptr++; - } - -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->pub.pass_counter++; - (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); - } -#endif - } - } - -#ifdef PROGRESS_REPORT - if (progress != NULL) - progress->completed_extra_passes++; -#endif - - /* Set up to call proper row-extraction routine in future */ - if (source->visual == PSEUDOCOLOR) { - source->pub.buffer = source->rle_row; - source->pub.get_pixel_rows = get_pseudocolor_row; - } else { - source->pub.get_pixel_rows = get_rle_row; - } - source->row = cinfo->image_height; - - /* And fetch the topmost (bottommost) row */ - return (*source->pub.get_pixel_rows) (cinfo, sinfo); -} - - -/* - * Finish up at the end of the file. - */ - -METHODDEF(void) -finish_input_rle(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) -{ - /* no work */ -} - - -/* - * The module selection routine for RLE format input. - */ - -GLOBAL(cjpeg_source_ptr) -jinit_read_rle(j_compress_ptr cinfo) -{ - rle_source_ptr source; - - /* Create module interface object */ - source = (rle_source_ptr) - (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, - sizeof(rle_source_struct)); - /* Fill in method ptrs */ - source->pub.start_input = start_input_rle; - source->pub.finish_input = finish_input_rle; - source->pub.get_pixel_rows = load_image; - - return (cjpeg_source_ptr)source; -} - -#endif /* RLE_SUPPORTED */ diff --git a/third-party/mozjpeg/mozjpeg/rdswitch.c b/third-party/mozjpeg/mozjpeg/rdswitch.c index aa4d1e9633b..13cf102026c 100644 --- a/third-party/mozjpeg/mozjpeg/rdswitch.c +++ b/third-party/mozjpeg/mozjpeg/rdswitch.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1996, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2010, 2018, D. R. Commander. + * Copyright (C) 2010, 2018, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -17,6 +17,10 @@ * -sample HxV[,HxV,...] Set component sampling factors */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ #include /* to declare isdigit(), isspace() */ @@ -189,7 +193,7 @@ read_scan_script (j_compress_ptr cinfo, char *filename) int scanno, ncomps, termchar; long val; jpeg_scan_info *scanptr; -#define MAX_SCANS 100 /* quite arbitrary limit */ +#define MAX_SCANS 64 /* must match scan_buffer size */ jpeg_scan_info scans[MAX_SCANS]; if ((fp = fopen(filename, "r")) == NULL) { @@ -263,7 +267,7 @@ read_scan_script (j_compress_ptr cinfo, char *filename) scanptr = (jpeg_scan_info *) (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_IMAGE, scanno * sizeof(jpeg_scan_info)); - MEMCOPY(scanptr, scans, scanno * sizeof(jpeg_scan_info)); + memcpy(scanptr, scans, scanno * sizeof(jpeg_scan_info)); cinfo->scan_info = scanptr; cinfo->num_scans = scanno; @@ -541,8 +545,8 @@ set_quality_ratings (j_compress_ptr cinfo, char *arg, boolean force_baseline) #else q_scale_factor[tblno] = jpeg_float_quality_scaling(val); #endif - while (*arg && *arg++ != ',') /* advance to next segment of arg string */ - ; + while (*arg && *arg++ != ','); /* advance to next segment of arg + string */ } else { /* reached end of parameter, set remaining factors to last value */ #if JPEG_LIB_VERSION >= 70 @@ -592,8 +596,8 @@ set_quant_slots (j_compress_ptr cinfo, char *arg) return FALSE; } cinfo->comp_info[ci].quant_tbl_no = val; - while (*arg && *arg++ != ',') /* advance to next segment of arg string */ - ; + while (*arg && *arg++ != ','); /* advance to next segment of arg + string */ } else { /* reached end of parameter, set remaining components to last table */ cinfo->comp_info[ci].quant_tbl_no = val; @@ -626,8 +630,8 @@ set_sample_factors (j_compress_ptr cinfo, char *arg) } cinfo->comp_info[ci].h_samp_factor = val1; cinfo->comp_info[ci].v_samp_factor = val2; - while (*arg && *arg++ != ',') /* advance to next segment of arg string */ - ; + while (*arg && *arg++ != ','); /* advance to next segment of arg + string */ } else { /* reached end of parameter, set remaining components to 1x1 sampling */ cinfo->comp_info[ci].h_samp_factor = 1; diff --git a/third-party/mozjpeg/mozjpeg/rdtarga.c b/third-party/mozjpeg/mozjpeg/rdtarga.c index 37bd286ae1d..3ed7eb34d8c 100644 --- a/third-party/mozjpeg/mozjpeg/rdtarga.c +++ b/third-party/mozjpeg/mozjpeg/rdtarga.c @@ -5,7 +5,7 @@ * Copyright (C) 1991-1996, Thomas G. Lane. * Modified 2017 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2018, D. R. Commander. + * Copyright (C) 2018, 2021-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -28,22 +28,12 @@ /* Macros to deal with unsigned chars as efficiently as compiler allows */ -#ifdef HAVE_UNSIGNED_CHAR typedef unsigned char U_CHAR; #define UCH(x) ((int)(x)) -#else /* !HAVE_UNSIGNED_CHAR */ -#ifdef __CHAR_UNSIGNED__ -typedef char U_CHAR; -#define UCH(x) ((int)(x)) -#else -typedef char U_CHAR; -#define UCH(x) ((int)(x) & 0xFF) -#endif -#endif /* HAVE_UNSIGNED_CHAR */ #define ReadOK(file, buffer, len) \ - (JFREAD(file, buffer, len) == ((size_t)(len))) + (fread(buffer, 1, len, file) == ((size_t)(len))) /* Private version of data source object */ @@ -344,8 +334,9 @@ start_input_tga(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) unsigned int width, height, maplen; boolean is_bottom_up; -#define GET_2B(offset) ((unsigned int)UCH(targaheader[offset]) + \ - (((unsigned int)UCH(targaheader[offset + 1])) << 8)) +#define GET_2B(offset) \ + ((unsigned int)UCH(targaheader[offset]) + \ + (((unsigned int)UCH(targaheader[offset + 1])) << 8)) if (!ReadOK(source->pub.input_file, targaheader, 18)) ERREXIT(cinfo, JERR_INPUT_EOF); @@ -372,6 +363,11 @@ start_input_tga(j_compress_ptr cinfo, cjpeg_source_ptr sinfo) interlace_type != 0 || /* currently don't allow interlaced image */ width == 0 || height == 0) /* image width/height must be non-zero */ ERREXIT(cinfo, JERR_TGA_BADPARMS); +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if (sinfo->max_pixels && + (unsigned long long)width * height > sinfo->max_pixels) + ERREXIT(cinfo, JERR_WIDTH_OVERFLOW); +#endif if (subtype > 8) { /* It's an RLE-coded file */ @@ -502,6 +498,9 @@ jinit_read_targa(j_compress_ptr cinfo) /* Fill in method ptrs, except get_pixel_rows which start_input sets */ source->pub.start_input = start_input_tga; source->pub.finish_input = finish_input_tga; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + source->pub.max_pixels = 0; +#endif return (cjpeg_source_ptr)source; } diff --git a/third-party/mozjpeg/mozjpeg/release/Config.cmake.in b/third-party/mozjpeg/mozjpeg/release/Config.cmake.in new file mode 100644 index 00000000000..0c1ba8aa6d9 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/release/Config.cmake.in @@ -0,0 +1,4 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/@CMAKE_PROJECT_NAME@Targets.cmake") +check_required_components("@CMAKE_PROJECT_NAME@") diff --git a/third-party/mozjpeg/mozjpeg/release/License.rtf b/third-party/mozjpeg/mozjpeg/release/License.rtf old mode 100755 new mode 100644 diff --git a/third-party/mozjpeg/mozjpeg/release/ReadMe.txt b/third-party/mozjpeg/mozjpeg/release/ReadMe.txt index 0a087114fff..a71d15c2f6b 100644 --- a/third-party/mozjpeg/mozjpeg/release/ReadMe.txt +++ b/third-party/mozjpeg/mozjpeg/release/ReadMe.txt @@ -1,5 +1,5 @@ -libjpeg-turbo is a JPEG image codec that uses SIMD instructions to accelerate baseline JPEG compression and decompression on x86, x86-64, ARM, PowerPC, and MIPS systems, as well as progressive JPEG compression on x86 and x86-64 systems. On such systems, libjpeg-turbo is generally 2-6x as fast as libjpeg, all else being equal. On other types of systems, libjpeg-turbo can still outperform libjpeg by a significant amount, by virtue of its highly-optimized Huffman coding routines. In many cases, the performance of libjpeg-turbo rivals that of proprietary high-speed JPEG codecs. +libjpeg-turbo is a JPEG image codec that uses SIMD instructions to accelerate baseline JPEG compression and decompression on x86, x86-64, Arm, PowerPC, and MIPS systems, as well as progressive JPEG compression on x86, x86-64, and Arm systems. On such systems, libjpeg-turbo is generally 2-6x as fast as libjpeg, all else being equal. On other types of systems, libjpeg-turbo can still outperform libjpeg by a significant amount, by virtue of its highly-optimized Huffman coding routines. In many cases, the performance of libjpeg-turbo rivals that of proprietary high-speed JPEG codecs. libjpeg-turbo implements both the traditional libjpeg API as well as the less powerful but more straightforward TurboJPEG API. libjpeg-turbo also features colorspace extensions that allow it to compress from/decompress to 32-bit and big-endian pixel buffers (RGBX, XBGR, etc.), as well as a full-featured Java interface. -libjpeg-turbo was originally based on libjpeg/SIMD, an MMX-accelerated derivative of libjpeg v6b developed by Miyasaka Masaru. The TigerVNC and VirtualGL projects made numerous enhancements to the codec in 2009, and in early 2010, libjpeg-turbo spun off into an independent project, with the goal of making high-speed JPEG compression/decompression technology available to a broader range of users and developers. +libjpeg-turbo was originally based on libjpeg/SIMD, an MMX-accelerated derivative of libjpeg v6b developed by Miyasaka Masaru. The TigerVNC and VirtualGL projects made numerous enhancements to the codec in 2009, and in early 2010, libjpeg-turbo spun off into an independent project, with the goal of making high-speed JPEG compression/decompression technology available to a broader range of users and developers. libjpeg-turbo is an ISO/IEC and ITU-T reference implementation of the JPEG standard. diff --git a/third-party/mozjpeg/mozjpeg/release/Welcome.rtf b/third-party/mozjpeg/mozjpeg/release/Welcome.rtf deleted file mode 100755 index 7ca525a972f..00000000000 --- a/third-party/mozjpeg/mozjpeg/release/Welcome.rtf +++ /dev/null @@ -1,18 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf460 -{\fonttbl\f0\fnil\fcharset0 Menlo-Regular;} -{\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red203\green233\blue242;} -\deftab720 -\pard\tx529\tx1059\tx1589\tx2119\tx2649\tx3178\tx3708\tx4238\tx4768\tx5298\tx5827\tx6357\tx6887\tx7417\tx7947\tx8476\tx9006\tx9536\tx10066\tx10596\tx11125\tx11655\tx12185\tx12715\tx13245\tx13774\tx14304\tx14834\tx15364\tx15894\tx16423\tx16953\tx17483\tx18013\tx18543\tx19072\tx19602\tx20132\tx20662\tx21192\tx21722\tx22251\tx22781\tx23311\tx23841\tx24371\tx24900\tx25430\tx25960\tx26490\tx27020\tx27549\tx28079\tx28609\tx29139\tx29669\tx30198\tx30728\tx31258\tx31788\tx32318\tx32847\tx33377\tx33907\tx34437\tx34967\tx35496\tx36026\tx36556\tx37086\tx37616\tx38145\tx38675\tx39205\tx39735\tx40265\tx40794\tx41324\tx41854\tx42384\tx42914\tx43443\tx43973\tx44503\tx45033\tx45563\tx46093\tx46622\tx47152\tx47682\tx48212\tx48742\tx49271\tx49801\tx50331\tx50861\tx51391\tx51920\tx52450\tx52980\pardeftab720\li529\fi-530\partightenfactor0 - -\f0\fs22 \cf2 \CocoaLigature0 TThi /opt/mozjpeg/bin/uninstall\ -\pard\tx529\tx1059\tx1589\tx2119\tx2649\tx3178\tx3708\tx4238\tx4768\tx5298\tx5827\tx6357\tx6887\tx7417\tx7947\tx8476\tx9006\tx9536\tx10066\tx10596\tx11125\tx11655\tx12185\tx12715\tx13245\tx13774\tx14304\tx14834\tx15364\tx15894\tx16423\tx16953\tx17483\tx18013\tx18543\tx19072\tx19602\tx20132\tx20662\tx21192\tx21722\tx22251\tx22781\tx23311\tx23841\tx24371\tx24900\tx25430\tx25960\tx26490\tx27020\tx27549\tx28079\tx28609\tx29139\tx29669\tx30198\tx30728\tx31258\tx31788\tx32318\tx32847\tx33377\tx33907\tx34437\tx34967\tx35496\tx36026\tx36556\tx37086\tx37616\tx38145\tx38675\tx39205\tx39735\tx40265\tx40794\tx41324\tx41854\tx42384\tx42914\tx43443\tx43973\tx44503\tx45033\tx45563\tx46093\tx46622\tx47152\tx47682\tx48212\tx48742\tx49271\tx49801\tx50331\tx50861\tx51391\tx51920\tx52450\tx52980\pardeftab720\li662\fi-663\partightenfactor0 -\cf2 installer will install the mozjpeg SDK and run-time libraries onto your computer so that you can use mozjpeg to build new applications. To remove the mozjpeg package, run\ -\pard\tx529\tx1059\tx1589\tx2119\tx2649\tx3178\tx3708\tx4238\tx4768\tx5298\tx5827\tx6357\tx6887\tx7417\tx7947\tx8476\tx9006\tx9536\tx10066\tx10596\tx11125\tx11655\tx12185\tx12715\tx13245\tx13774\tx14304\tx14834\tx15364\tx15894\tx16423\tx16953\tx17483\tx18013\tx18543\tx19072\tx19602\tx20132\tx20662\tx21192\tx21722\tx22251\tx22781\tx23311\tx23841\tx24371\tx24900\tx25430\tx25960\tx26490\tx27020\tx27549\tx28079\tx28609\tx29139\tx29669\tx30198\tx30728\tx31258\tx31788\tx32318\tx32847\tx33377\tx33907\tx34437\tx34967\tx35496\tx36026\tx36556\tx37086\tx37616\tx38145\tx38675\tx39205\tx39735\tx40265\tx40794\tx41324\tx41854\tx42384\tx42914\tx43443\tx43973\tx44503\tx45033\tx45563\tx46093\tx46622\tx47152\tx47682\tx48212\tx48742\tx49271\tx49801\tx50331\tx50861\tx51391\tx51920\tx52450\tx52980\pardeftab720\li529\fi-530\partightenfactor0 -\cf2 is installer will install the \cb3 mozjpeg\cb1 SDK and run-time libraries onto your computer so that you can use \cb3 mozjpeg\cb1 to build new applications. To remove the \cb3 mozjpeg\cb1 package, run\ -\ -\pard\tx529\tx1059\tx1589\tx2119\tx2649\tx3178\tx3708\tx4238\tx4768\tx5298\tx5827\tx6357\tx6887\tx7417\tx7947\tx8476\tx9006\tx9536\tx10066\tx10596\tx11125\tx11655\tx12185\tx12715\tx13245\tx13774\tx14304\tx14834\tx15364\tx15894\tx16423\tx16953\tx17483\tx18013\tx18543\tx19072\tx19602\tx20132\tx20662\tx21192\tx21722\tx22251\tx22781\tx23311\tx23841\tx24371\tx24900\tx25430\tx25960\tx26490\tx27020\tx27549\tx28079\tx28609\tx29139\tx29669\tx30198\tx30728\tx31258\tx31788\tx32318\tx32847\tx33377\tx33907\tx34437\tx34967\tx35496\tx36026\tx36556\tx37086\tx37616\tx38145\tx38675\tx39205\tx39735\tx40265\tx40794\tx41324\tx41854\tx42384\tx42914\tx43443\tx43973\tx44503\tx45033\tx45563\tx46093\tx46622\tx47152\tx47682\tx48212\tx48742\tx49271\tx49801\tx50331\tx50861\tx51391\tx51920\tx52450\tx52980\pardeftab720\li794\fi-795\partightenfactor0 -\cf2 /opt/\cb3 mozjpeg\cb1 /bin/uninstall\ -\pard\tx529\tx1059\tx1589\tx2119\tx2649\tx3178\tx3708\tx4238\tx4768\tx5298\tx5827\tx6357\tx6887\tx7417\tx7947\tx8476\tx9006\tx9536\tx10066\tx10596\tx11125\tx11655\tx12185\tx12715\tx13245\tx13774\tx14304\tx14834\tx15364\tx15894\tx16423\tx16953\tx17483\tx18013\tx18543\tx19072\tx19602\tx20132\tx20662\tx21192\tx21722\tx22251\tx22781\tx23311\tx23841\tx24371\tx24900\tx25430\tx25960\tx26490\tx27020\tx27549\tx28079\tx28609\tx29139\tx29669\tx30198\tx30728\tx31258\tx31788\tx32318\tx32847\tx33377\tx33907\tx34437\tx34967\tx35496\tx36026\tx36556\tx37086\tx37616\tx38145\tx38675\tx39205\tx39735\tx40265\tx40794\tx41324\tx41854\tx42384\tx42914\tx43443\tx43973\tx44503\tx45033\tx45563\tx46093\tx46622\tx47152\tx47682\tx48212\tx48742\tx49271\tx49801\tx50331\tx50861\tx51391\tx51920\tx52450\tx52980\pardeftab720\li560\fi-561\partightenfactor0 -\cf2 \ -from the command line.\ -} \ No newline at end of file diff --git a/third-party/mozjpeg/mozjpeg/release/Welcome.rtf.in b/third-party/mozjpeg/mozjpeg/release/Welcome.rtf.in new file mode 100644 index 00000000000..e4021cf72b8 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/release/Welcome.rtf.in @@ -0,0 +1,17 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fmodern\fcharset0 CourierNewPSMT;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\deftab720 +\pard\pardeftab720\ql\qnatural + +\f0\fs24 \cf0 This installer will install the libjpeg-turbo SDK and run-time libraries onto your computer so that you can use libjpeg-turbo to build new applications or accelerate existing ones. To remove the libjpeg-turbo package, run\ +\ +\pard\pardeftab720\ql\qnatural + +\f1 \cf0 @CMAKE_INSTALL_FULL_BINDIR@/uninstall\ +\pard\pardeftab720\ql\qnatural + +\f0 \cf0 \ +from the command line.\ +} \ No newline at end of file diff --git a/third-party/mozjpeg/mozjpeg/release/installer.nsi.in b/third-party/mozjpeg/mozjpeg/release/installer.nsi.in old mode 100755 new mode 100644 index 44419fa82c8..65db63da394 --- a/third-party/mozjpeg/mozjpeg/release/installer.nsi.in +++ b/third-party/mozjpeg/mozjpeg/release/installer.nsi.in @@ -71,6 +71,11 @@ Section "@CMAKE_PROJECT_NAME@ SDK for @INST_PLATFORM@ (required)" SetOutPath $INSTDIR\lib\pkgconfig File "@CMAKE_CURRENT_BINARY_DIR@\pkgscripts\libjpeg.pc" File "@CMAKE_CURRENT_BINARY_DIR@\pkgscripts\libturbojpeg.pc" + SetOutPath $INSTDIR\lib\cmake\@CMAKE_PROJECT_NAME@ + File "@CMAKE_CURRENT_BINARY_DIR@\pkgscripts\@CMAKE_PROJECT_NAME@Config.cmake" + File "@CMAKE_CURRENT_BINARY_DIR@\pkgscripts\@CMAKE_PROJECT_NAME@ConfigVersion.cmake" + File "@CMAKE_CURRENT_BINARY_DIR@\win\@CMAKE_PROJECT_NAME@Targets.cmake" + File "@CMAKE_CURRENT_BINARY_DIR@\win\@CMAKE_PROJECT_NAME@Targets-release.cmake" !ifdef JAVA SetOutPath $INSTDIR\classes File "@CMAKE_CURRENT_BINARY_DIR@\java\turbojpeg.jar" @@ -141,6 +146,10 @@ Section "Uninstall" !endif Delete $INSTDIR\lib\pkgconfig\libjpeg.pc Delete $INSTDIR\lib\pkgconfig\libturbojpeg.pc + Delete $INSTDIR\lib\cmake\@CMAKE_PROJECT_NAME@\@CMAKE_PROJECT_NAME@Config.cmake + Delete $INSTDIR\lib\cmake\@CMAKE_PROJECT_NAME@\@CMAKE_PROJECT_NAME@ConfigVersion.cmake + Delete $INSTDIR\lib\cmake\@CMAKE_PROJECT_NAME@\@CMAKE_PROJECT_NAME@Targets.cmake + Delete $INSTDIR\lib\cmake\@CMAKE_PROJECT_NAME@\@CMAKE_PROJECT_NAME@Targets-release.cmake !ifdef JAVA Delete $INSTDIR\classes\turbojpeg.jar !endif @@ -176,6 +185,8 @@ Section "Uninstall" RMDir "$INSTDIR\include" RMDir "$INSTDIR\lib\pkgconfig" + RMDir "$INSTDIR\lib\cmake\@CMAKE_PROJECT_NAME@" + RMDir "$INSTDIR\lib\cmake" RMDir "$INSTDIR\lib" RMDir "$INSTDIR\doc" !ifdef GCC diff --git a/third-party/mozjpeg/mozjpeg/release/makecygwinpkg.in b/third-party/mozjpeg/mozjpeg/release/makecygwinpkg.in deleted file mode 100755 index b7f353e9756..00000000000 --- a/third-party/mozjpeg/mozjpeg/release/makecygwinpkg.in +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/sh - -set -u -set -e -trap onexit INT -trap onexit TERM -trap onexit EXIT - -TMPDIR= - -onexit() -{ - if [ ! "$TMPDIR" = "" ]; then - rm -rf $TMPDIR - fi -} - -safedirmove () -{ - if [ "$1" = "$2" ]; then - return 0 - fi - if [ "$1" = "" -o ! -d "$1" ]; then - echo safedirmove: source dir $1 is not valid - return 1 - fi - if [ "$2" = "" -o -e "$2" ]; then - echo safedirmove: dest dir $2 is not valid - return 1 - fi - if [ "$3" = "" -o -e "$3" ]; then - echo safedirmove: tmp dir $3 is not valid - return 1 - fi - mkdir -p $3 - mv $1/* $3/ - rmdir $1 - mkdir -p $2 - mv $3/* $2/ - rmdir $3 - return 0 -} - -PKGNAME=@PKGNAME@ -VERSION=@VERSION@ -BUILD=@BUILD@ - -PREFIX=@CMAKE_INSTALL_PREFIX@ -DOCDIR=@CMAKE_INSTALL_FULL_DOCDIR@ -LIBDIR=@CMAKE_INSTALL_FULL_LIBDIR@ - -umask 022 -rm -f $PKGNAME-$VERSION-$BUILD.tar.bz2 -TMPDIR=`mktemp -d /tmp/ljtbuild.XXXXXX` -__PWD=`pwd` -make install DESTDIR=$TMPDIR/pkg -if [ "$PREFIX" = "@CMAKE_INSTALL_DEFAULT_PREFIX@" -a "$DOCDIR" = "@CMAKE_INSTALL_DEFAULT_PREFIX@/doc" ]; then - safedirmove $TMPDIR/pkg$DOCDIR $TMPDIR/pkg/usr/share/doc/$PKGNAME-$VERSION $TMPDIR/__tmpdoc - ln -fs /usr/share/doc/$PKGNAME-$VERSION $TMPDIR/pkg$DOCDIR -fi -cd $TMPDIR/pkg -tar cfj ../$PKGNAME-$VERSION-$BUILD.tar.bz2 * -cd $__PWD -mv $TMPDIR/*.tar.bz2 . - -exit 0 diff --git a/third-party/mozjpeg/mozjpeg/release/makedpkg.in b/third-party/mozjpeg/mozjpeg/release/makedpkg.in old mode 100644 new mode 100755 index 77836dd7e54..0b7b7e73c01 --- a/third-party/mozjpeg/mozjpeg/release/makedpkg.in +++ b/third-party/mozjpeg/mozjpeg/release/makedpkg.in @@ -54,7 +54,11 @@ makedeb() if [ $SUPPLEMENT = 1 ]; then PKGNAME=$PKGNAME\32 - DEBARCH=amd64 + if [ "$DEBARCH" = "i386" ]; then + DEBARCH=amd64 + else + DEBARCH=arm64 + fi fi umask 022 @@ -63,7 +67,7 @@ makedeb() mkdir $TMPDIR/DEBIAN if [ $SUPPLEMENT = 1 ]; then - make install DESTDIR=$TMPDIR + DESTDIR=$TMPDIR @CMAKE_MAKE_PROGRAM@ install rm -rf $TMPDIR$BINDIR if [ "$DATAROOTDIR" != "$PREFIX" ]; then rm -rf $TMPDIR$DATAROOTDIR @@ -75,7 +79,7 @@ makedeb() rm -rf $TMPDIR$INCLUDEDIR rm -rf $TMPDIR$MANDIR else - make install DESTDIR=$TMPDIR + DESTDIR=$TMPDIR @CMAKE_MAKE_PROGRAM@ install if [ "$PREFIX" = "@CMAKE_INSTALL_DEFAULT_PREFIX@" -a "$DOCDIR" = "@CMAKE_INSTALL_DEFAULT_PREFIX@/doc" ]; then safedirmove $TMPDIR/$DOCDIR $TMPDIR/usr/share/doc/$PKGNAME-$VERSION $TMPDIR/__tmpdoc ln -fs /usr/share/doc/$DIRNAME-$VERSION $TMPDIR$DOCDIR @@ -110,6 +114,8 @@ if [ ! `uid` -eq 0 ]; then fi makedeb 0 -if [ "$DEBARCH" = "i386" ]; then makedeb 1; fi +if [ "$DEBARCH" = "i386" -o "$DEBARCH" = "armel" -o "$DEBARCH" = "armhf" ]; then + makedeb 1 +fi exit diff --git a/third-party/mozjpeg/mozjpeg/release/makemacpkg.in b/third-party/mozjpeg/mozjpeg/release/makemacpkg.in old mode 100644 new mode 100755 index fb16f41d6a0..3adf541e54b --- a/third-party/mozjpeg/mozjpeg/release/makemacpkg.in +++ b/third-party/mozjpeg/mozjpeg/release/makemacpkg.in @@ -43,23 +43,18 @@ safedirmove () usage() { - echo "$0 [universal] [-lipo [path to lipo]]" + echo "$0 [-lipo [path to lipo]]" exit 1 } -UNIVERSAL=0 - PKGNAME=@PKGNAME@ VERSION=@VERSION@ BUILD=@BUILD@ SRCDIR=@CMAKE_CURRENT_SOURCE_DIR@ -BUILDDIR32=@OSX_32BIT_BUILD@ -BUILDDIRARMV7=@IOS_ARMV7_BUILD@ -BUILDDIRARMV7S=@IOS_ARMV7S_BUILD@ -BUILDDIRARMV8=@IOS_ARMV8_BUILD@ +BUILDDIRARMV8=@ARMV8_BUILD@ WITH_JAVA=@WITH_JAVA@ -OSX_APP_CERT_NAME="@OSX_APP_CERT_NAME@" -OSX_INST_CERT_NAME="@OSX_INST_CERT_NAME@" +MACOS_APP_CERT_NAME="@MACOS_APP_CERT_NAME@" +MACOS_INST_CERT_NAME="@MACOS_INST_CERT_NAME@" LIPO=lipo PREFIX=@CMAKE_INSTALL_PREFIX@ @@ -82,9 +77,6 @@ while [ $# -gt 0 ]; do fi fi ;; - universal) - UNIVERSAL=1 - ;; esac shift done @@ -98,7 +90,7 @@ TMPDIR=`mktemp -d /tmp/$PKGNAME-build.XXXXXX` PKGROOT=$TMPDIR/pkg/Package_Root mkdir -p $PKGROOT -make install DESTDIR=$PKGROOT +DESTDIR=$PKGROOT @CMAKE_MAKE_PROGRAM@ install if [ "$PREFIX" = "@CMAKE_INSTALL_DEFAULT_PREFIX@" -a "$DOCDIR" = "@CMAKE_INSTALL_DEFAULT_PREFIX@/doc" ]; then mkdir -p $PKGROOT/Library/Documentation @@ -106,62 +98,7 @@ if [ "$PREFIX" = "@CMAKE_INSTALL_DEFAULT_PREFIX@" -a "$DOCDIR" = "@CMAKE_INSTALL ln -fs /Library/Documentation/$PKGNAME $PKGROOT$DOCDIR fi -if [ $UNIVERSAL = 1 -a "$BUILDDIR32" != "" ]; then - if [ ! -d $BUILDDIR32 ]; then - echo ERROR: 32-bit build directory $BUILDDIR32 does not exist - exit 1 - fi - if [ ! -f $BUILDDIR32/Makefile ]; then - echo ERROR: 32-bit build directory $BUILDDIR32 is not configured - exit 1 - fi - mkdir -p $TMPDIR/dist.x86 - pushd $BUILDDIR32 - make install DESTDIR=$TMPDIR/dist.x86 - popd - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$LIBDIR/$LIBJPEG_DSO_NAME \ - -arch x86_64 $PKGROOT/$LIBDIR/$LIBJPEG_DSO_NAME \ - -output $PKGROOT/$LIBDIR/$LIBJPEG_DSO_NAME - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$LIBDIR/libjpeg.a \ - -arch x86_64 $PKGROOT/$LIBDIR/libjpeg.a \ - -output $PKGROOT/$LIBDIR/libjpeg.a - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$LIBDIR/$TURBOJPEG_DSO_NAME \ - -arch x86_64 $PKGROOT/$LIBDIR/$TURBOJPEG_DSO_NAME \ - -output $PKGROOT/$LIBDIR/$TURBOJPEG_DSO_NAME - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$LIBDIR/libturbojpeg.a \ - -arch x86_64 $PKGROOT/$LIBDIR/libturbojpeg.a \ - -output $PKGROOT/$LIBDIR/libturbojpeg.a - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$BINDIR/cjpeg \ - -arch x86_64 $PKGROOT/$BINDIR/cjpeg \ - -output $PKGROOT/$BINDIR/cjpeg - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$BINDIR/djpeg \ - -arch x86_64 $PKGROOT/$BINDIR/djpeg \ - -output $PKGROOT/$BINDIR/djpeg - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$BINDIR/jpegtran \ - -arch x86_64 $PKGROOT/$BINDIR/jpegtran \ - -output $PKGROOT/$BINDIR/jpegtran - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$BINDIR/tjbench \ - -arch x86_64 $PKGROOT/$BINDIR/tjbench \ - -output $PKGROOT/$BINDIR/tjbench - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$BINDIR/rdjpgcom \ - -arch x86_64 $PKGROOT/$BINDIR/rdjpgcom \ - -output $PKGROOT/$BINDIR/rdjpgcom - $LIPO -create \ - -arch i386 $TMPDIR/dist.x86/$BINDIR/wrjpgcom \ - -arch x86_64 $PKGROOT/$BINDIR/wrjpgcom \ - -output $PKGROOT/$BINDIR/wrjpgcom -fi - -install_ios() +install_subbuild() { BUILDDIR=$1 ARCHNAME=$2 @@ -172,19 +109,19 @@ install_ios() echo ERROR: $ARCHNAME build directory $BUILDDIR does not exist exit 1 fi - if [ ! -f $BUILDDIR/Makefile ]; then + if [ ! -f $BUILDDIR/Makefile -a ! -f $BUILDDIR/build.ninja ]; then echo ERROR: $ARCHNAME build directory $BUILDDIR is not configured exit 1 fi mkdir -p $TMPDIR/dist.$DIRNAME pushd $BUILDDIR - make install DESTDIR=$TMPDIR/dist.$DIRNAME + DESTDIR=$TMPDIR/dist.$DIRNAME @CMAKE_MAKE_PROGRAM@ install popd - $LIPO -create \ + $LIPO -create \ $PKGROOT/$LIBDIR/$LIBJPEG_DSO_NAME \ -arch $LIPOARCH $TMPDIR/dist.$DIRNAME/$LIBDIR/$LIBJPEG_DSO_NAME \ -output $PKGROOT/$LIBDIR/$LIBJPEG_DSO_NAME - $LIPO -create \ + $LIPO -create \ $PKGROOT/$LIBDIR/libjpeg.a \ -arch $LIPOARCH $TMPDIR/dist.$DIRNAME/$LIBDIR/libjpeg.a \ -output $PKGROOT/$LIBDIR/libjpeg.a @@ -222,28 +159,14 @@ install_ios() -output $PKGROOT/$BINDIR/wrjpgcom } -if [ $UNIVERSAL = 1 -a "$BUILDDIRARMV7" != "" ]; then - install_ios $BUILDDIRARMV7 ARMv7 armv7 arm - fi - -if [ $UNIVERSAL = 1 -a "$BUILDDIRARMV7S" != "" ]; then - install_ios $BUILDDIRARMV7S ARMv7s armv7s arm -fi - -if [ $UNIVERSAL = 1 -a "$BUILDDIRARMV8" != "" ]; then - install_ios $BUILDDIRARMV8 ARMv8 armv8 arm64 +if [ "$BUILDDIRARMV8" != "" ]; then + install_subbuild $BUILDDIRARMV8 Armv8 armv8 arm64 fi install_name_tool -id $LIBDIR/$LIBJPEG_DSO_NAME $PKGROOT/$LIBDIR/$LIBJPEG_DSO_NAME install_name_tool -id $LIBDIR/$TURBOJPEG_DSO_NAME $PKGROOT/$LIBDIR/$TURBOJPEG_DSO_NAME -if [ $WITH_JAVA = 1 ]; then - ln -fs $TURBOJPEG_DSO_NAME $PKGROOT/$LIBDIR/libturbojpeg.jnilib -fi if [ "$PREFIX" = "@CMAKE_INSTALL_DEFAULT_PREFIX@" -a "$LIBDIR" = "@CMAKE_INSTALL_DEFAULT_PREFIX@/lib" ]; then - if [ ! -h $PKGROOT/$PREFIX/lib32 ]; then - ln -fs lib $PKGROOT/$PREFIX/lib32 - fi if [ ! -h $PKGROOT/$PREFIX/lib64 ]; then ln -fs lib $PKGROOT/$PREFIX/lib64 fi @@ -255,28 +178,28 @@ install -m 755 pkgscripts/uninstall $PKGROOT/$BINDIR/ find $PKGROOT -type f | while read file; do xattr -c $file; done -cp $SRCDIR/release/License.rtf $SRCDIR/release/Welcome.rtf $SRCDIR/release/ReadMe.txt $TMPDIR/pkg/ +cp $SRCDIR/release/License.rtf pkgscripts/Welcome.rtf $SRCDIR/release/ReadMe.txt $TMPDIR/pkg/ mkdir $TMPDIR/dmg pkgbuild --root $PKGROOT --version $VERSION.$BUILD --identifier @PKGID@ \ $TMPDIR/pkg/$PKGNAME.pkg SUFFIX= -if [ "$OSX_INST_CERT_NAME" != "" ]; then +if [ "$MACOS_INST_CERT_NAME" != "" ]; then SUFFIX=-unsigned fi productbuild --distribution pkgscripts/Distribution.xml \ --package-path $TMPDIR/pkg/ --resources $TMPDIR/pkg/ \ $TMPDIR/dmg/$PKGNAME$SUFFIX.pkg -if [ "$OSX_INST_CERT_NAME" != "" ]; then - productsign --sign "$OSX_INST_CERT_NAME" --timestamp \ +if [ "$MACOS_INST_CERT_NAME" != "" ]; then + productsign --sign "$MACOS_INST_CERT_NAME" --timestamp \ $TMPDIR/dmg/$PKGNAME$SUFFIX.pkg $TMPDIR/dmg/$PKGNAME.pkg rm -r $TMPDIR/dmg/$PKGNAME$SUFFIX.pkg pkgutil --check-signature $TMPDIR/dmg/$PKGNAME.pkg fi hdiutil create -fs HFS+ -volname $PKGNAME-$VERSION \ -srcfolder "$TMPDIR/dmg" $TMPDIR/$PKGNAME-$VERSION.dmg -if [ "$OSX_APP_CERT_NAME" != "" ]; then - codesign -s "$OSX_APP_CERT_NAME" --timestamp $TMPDIR/$PKGNAME-$VERSION.dmg +if [ "$MACOS_APP_CERT_NAME" != "" ]; then + codesign -s "$MACOS_APP_CERT_NAME" --timestamp $TMPDIR/$PKGNAME-$VERSION.dmg codesign -vv $TMPDIR/$PKGNAME-$VERSION.dmg fi cp $TMPDIR/$PKGNAME-$VERSION.dmg . diff --git a/third-party/mozjpeg/mozjpeg/release/makerpm.in b/third-party/mozjpeg/mozjpeg/release/makerpm.in old mode 100644 new mode 100755 diff --git a/third-party/mozjpeg/mozjpeg/release/makesrpm.in b/third-party/mozjpeg/mozjpeg/release/makesrpm.in old mode 100644 new mode 100755 diff --git a/third-party/mozjpeg/mozjpeg/release/maketarball.in b/third-party/mozjpeg/mozjpeg/release/maketarball.in old mode 100644 new mode 100755 index 00a9c7e37a3..e2bbe66e80e --- a/third-party/mozjpeg/mozjpeg/release/maketarball.in +++ b/third-party/mozjpeg/mozjpeg/release/maketarball.in @@ -32,7 +32,7 @@ rm -f $PKGNAME-$VERSION-$OS-$ARCH.tar.bz2 TMPDIR=`mktemp -d /tmp/$PKGNAME-build.XXXXXX` mkdir -p $TMPDIR/install -make install DESTDIR=$TMPDIR/install +DESTDIR=$TMPDIR/install @CMAKE_MAKE_PROGRAM@ install echo tartest >$TMPDIR/tartest GNUTAR=0 BSDTAR=0 diff --git a/third-party/mozjpeg/mozjpeg/release/rpm.spec.in b/third-party/mozjpeg/mozjpeg/release/rpm.spec.in index 83a1669f92d..812cfb5e509 100644 --- a/third-party/mozjpeg/mozjpeg/release/rpm.spec.in +++ b/third-party/mozjpeg/mozjpeg/release/rpm.spec.in @@ -1,36 +1,42 @@ %global _docdir %{_defaultdocdir}/%{name}-%{version} -%define _prefix @CMAKE_INSTALL_PREFIX@ -%define _bindir @CMAKE_INSTALL_FULL_BINDIR@ -%define _datarootdir @CMAKE_INSTALL_FULL_DATAROOTDIR@ -%define _includedir @CMAKE_INSTALL_FULL_INCLUDEDIR@ -%define _javadir @CMAKE_INSTALL_FULL_JAVADIR@ -%define _mandir @CMAKE_INSTALL_FULL_MANDIR@ -%define _enable_static @ENABLE_STATIC@ -%define _enable_shared @ENABLE_SHARED@ -%define _with_turbojpeg @WITH_TURBOJPEG@ -%define _with_java @WITH_JAVA@ +%define _prefix @CMAKE_INSTALL_PREFIX@ +%define _bindir @CMAKE_INSTALL_FULL_BINDIR@ +%define _datarootdir @CMAKE_INSTALL_FULL_DATAROOTDIR@ +%define _includedir @CMAKE_INSTALL_FULL_INCLUDEDIR@ +%define _javadir @CMAKE_INSTALL_FULL_JAVADIR@ +%define _mandir @CMAKE_INSTALL_FULL_MANDIR@ +%define _enable_static @ENABLE_STATIC@ +%define _enable_shared @ENABLE_SHARED@ +%define _with_turbojpeg @WITH_TURBOJPEG@ +%define _with_java @WITH_JAVA@ %if "%{?__isa_bits:1}" == "1" -%define _bits %{__isa_bits} +%define _bits %{__isa_bits} %else # RPM < 4.6 %if "%{_lib}" == "lib64" -%define _bits 64 +%define _bits 64 %else -%define _bits 32 +%define _bits 32 %endif %endif +%if "%{_bits}" == "64" +%define _syslibdir /usr/lib64 +%else +%define _syslibdir /usr/lib +%endif + #-->%if 1 %if "%{_bits}" == "64" -%define _libdir %{_exec_prefix}/lib64 +%define _libdir %{_exec_prefix}/lib64 %else %if "%{_prefix}" == "/opt/libjpeg-turbo" -%define _libdir %{_exec_prefix}/lib32 +%define _libdir %{_exec_prefix}/lib32 %endif %endif #-->%else -%define _libdir @CMAKE_INSTALL_FULL_LIBDIR@ +%define _libdir @CMAKE_INSTALL_FULL_LIBDIR@ #-->%endif Summary: A SIMD-accelerated JPEG codec that provides both the libjpeg and TurboJPEG APIs @@ -52,8 +58,8 @@ Provides: %{name} = %{version}-%{release}, @CMAKE_PROJECT_NAME@ = %{version}-%{r %description libjpeg-turbo is a JPEG image codec that uses SIMD instructions to accelerate -baseline JPEG compression and decompression on x86, x86-64, ARM, PowerPC, and -MIPS systems, as well as progressive JPEG compression on x86 and x86-64 +baseline JPEG compression and decompression on x86, x86-64, Arm, PowerPC, and +MIPS systems, as well as progressive JPEG compression on x86, x86-64, and Arm systems. On such systems, libjpeg-turbo is generally 2-6x as fast as libjpeg, all else being equal. On other types of systems, libjpeg-turbo can still outperform libjpeg by a significant amount, by virtue of its highly-optimized @@ -71,7 +77,8 @@ derivative of libjpeg v6b developed by Miyasaka Masaru. The TigerVNC and VirtualGL projects made numerous enhancements to the codec in 2009, and in early 2010, libjpeg-turbo spun off into an independent project, with the goal of making high-speed JPEG compression/decompression technology available to a -broader range of users and developers. +broader range of users and developers. libjpeg-turbo is an ISO/IEC and ITU-T +reference implementation of the JPEG standard. #-->%prep #-->%setup -q -n @CMAKE_PROJECT_NAME@-%{version} @@ -101,9 +108,8 @@ broader range of users and developers. #-->make DESTDIR=$RPM_BUILD_ROOT %install - rm -rf $RPM_BUILD_ROOT -make install DESTDIR=$RPM_BUILD_ROOT +DESTDIR=$RPM_BUILD_ROOT @CMAKE_MAKE_PROGRAM@ install /sbin/ldconfig -n $RPM_BUILD_ROOT%{_libdir} #-->%if 0 @@ -163,38 +169,43 @@ rm -rf $RPM_BUILD_ROOT %doc %{_docdir}/* %dir %{_prefix} %if "%{_prefix}" == "@CMAKE_INSTALL_DEFAULT_PREFIX@" && "%{_docdir}" != "%{_prefix}/doc" - %{_prefix}/doc + %{_prefix}/doc %endif %dir %{_bindir} %{_bindir}/cjpeg %{_bindir}/djpeg %{_bindir}/jpegtran %if "%{_with_turbojpeg}" == "1" - %{_bindir}/tjbench + %{_bindir}/tjbench %endif %{_bindir}/rdjpgcom %{_bindir}/wrjpgcom +%if "%{_libdir}" != "%{_syslibdir}" %dir %{_libdir} +%endif %if "%{_enable_shared}" == "1" - %{_libdir}/libjpeg.so.@SO_MAJOR_VERSION@.@SO_AGE@.@SO_MINOR_VERSION@ - %{_libdir}/libjpeg.so.@SO_MAJOR_VERSION@ - %{_libdir}/libjpeg.so + %{_libdir}/libjpeg.so.@SO_MAJOR_VERSION@.@SO_AGE@.@SO_MINOR_VERSION@ + %{_libdir}/libjpeg.so.@SO_MAJOR_VERSION@ + %{_libdir}/libjpeg.so %endif %if "%{_enable_static}" == "1" - %{_libdir}/libjpeg.a + %{_libdir}/libjpeg.a %endif %dir %{_libdir}/pkgconfig %{_libdir}/pkgconfig/libjpeg.pc +%dir %{_libdir}/cmake +%dir %{_libdir}/cmake/@CMAKE_PROJECT_NAME@ +%{_libdir}/cmake/@CMAKE_PROJECT_NAME@ %if "%{_with_turbojpeg}" == "1" - %if "%{_enable_shared}" == "1" || "%{_with_java}" == "1" - %{_libdir}/libturbojpeg.so.@TURBOJPEG_SO_VERSION@ - %{_libdir}/libturbojpeg.so.@TURBOJPEG_SO_MAJOR_VERSION@ - %{_libdir}/libturbojpeg.so - %endif - %if "%{_enable_static}" == "1" - %{_libdir}/libturbojpeg.a - %endif - %{_libdir}/pkgconfig/libturbojpeg.pc + %if "%{_enable_shared}" == "1" || "%{_with_java}" == "1" + %{_libdir}/libturbojpeg.so.@TURBOJPEG_SO_VERSION@ + %{_libdir}/libturbojpeg.so.@TURBOJPEG_SO_MAJOR_VERSION@ + %{_libdir}/libturbojpeg.so + %endif + %if "%{_enable_static}" == "1" + %{_libdir}/libturbojpeg.a + %endif + %{_libdir}/pkgconfig/libturbojpeg.pc %endif %dir %{_includedir} %{_includedir}/jconfig.h @@ -202,7 +213,7 @@ rm -rf $RPM_BUILD_ROOT %{_includedir}/jmorecfg.h %{_includedir}/jpeglib.h %if "%{_with_turbojpeg}" == "1" - %{_includedir}/turbojpeg.h + %{_includedir}/turbojpeg.h %endif %dir %{_mandir} %dir %{_mandir}/man1 @@ -212,10 +223,11 @@ rm -rf $RPM_BUILD_ROOT %{_mandir}/man1/rdjpgcom.1* %{_mandir}/man1/wrjpgcom.1* %if "%{_prefix}" != "%{_datarootdir}" - %dir %{_datarootdir} + %dir %{_datarootdir} %endif %if "%{_with_java}" == "1" - %dir %{_javadir} - %{_javadir}/turbojpeg.jar + %dir %{_javadir} + %{_javadir}/turbojpeg.jar %endif + %changelog diff --git a/third-party/mozjpeg/mozjpeg/release/uninstall.in b/third-party/mozjpeg/mozjpeg/release/uninstall.in index 0217b1e9c8a..98e392bcfb5 100644 --- a/third-party/mozjpeg/mozjpeg/release/uninstall.in +++ b/third-party/mozjpeg/mozjpeg/release/uninstall.in @@ -1,4 +1,5 @@ -# Copyright (C)2009-2011, 2013, 2016 D. R. Commander. All Rights Reserved. +# Copyright (C)2009-2011, 2013, 2016, 2020 D. R. Commander. +# All Rights Reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -70,6 +71,12 @@ fi if [ -d $LIBDIR/pkgconfig ]; then rmdir $LIBDIR/pkgconfig 2>&1 || EXITSTATUS=-1 fi +if [ -d $LIBDIR/cmake/@CMAKE_PROJECT_NAME@ ]; then + rmdir $LIBDIR/cmake/@CMAKE_PROJECT_NAME@ || EXITSTATUS=-1 +fi +if [ -d $LIBDIR/cmake ]; then + rmdir $LIBDIR/cmake || EXITSTATUS=-1 +fi if [ -d $LIBDIR ]; then rmdir $LIBDIR 2>&1 || EXITSTATUS=-1 fi @@ -90,7 +97,7 @@ fi if [ -d $MANDIR ]; then rmdir $MANDIR 2>&1 || EXITSTATUS=-1 fi -if [ -d $JAVADIR ]; then +if [ -d "$JAVADIR" ]; then rmdir $JAVADIR 2>&1 || EXITSTATUS=-1 fi if [ -d $DATAROOTDIR -a "$DATAROOTDIR" != "$PREFIX" ]; then diff --git a/third-party/mozjpeg/mozjpeg/sharedlib/CMakeLists.txt b/third-party/mozjpeg/mozjpeg/sharedlib/CMakeLists.txt old mode 100755 new mode 100644 index f0a7d876c75..f13b958fb41 --- a/third-party/mozjpeg/mozjpeg/sharedlib/CMakeLists.txt +++ b/third-party/mozjpeg/mozjpeg/sharedlib/CMakeLists.txt @@ -35,6 +35,11 @@ else() set(DEFFILE ../win/jpeg${SO_MAJOR_VERSION}.def) endif() endif() +if(MSVC) + configure_file(${CMAKE_SOURCE_DIR}/win/jpeg.rc.in + ${CMAKE_BINARY_DIR}/win/jpeg.rc) + set(JPEG_SRCS ${JPEG_SRCS} ${CMAKE_BINARY_DIR}/win/jpeg.rc) +endif() add_library(jpeg SHARED ${JPEG_SRCS} ${DEFFILE} $ ${SIMD_OBJS}) if(UNIX) @@ -72,6 +77,12 @@ else() set(COMPILE_FLAGS "-DBMP_SUPPORTED -DGIF_SUPPORTED -DPPM_SUPPORTED -DTARGA_SUPPORTED ${USE_SETMODE}") set(CJPEG_BMP_SOURCES ../rdbmp.c ../rdtarga.c) set(DJPEG_BMP_SOURCES ../wrbmp.c ../wrtarga.c) + + if(PNG_SUPPORTED) + report_option(PNG_SUPPORTED "PNG reading support") + set(COMPILE_FLAGS "${COMPILE_FLAGS} -DPNG_SUPPORTED") + set(CJPEG_BMP_SOURCES ${CJPEG_BMP_SOURCES} ../rdpng.c) + endif() endif() add_executable(cjpeg ../cjpeg.c ../cdjpeg.c ../rdgif.c ../rdppm.c ../rdjpeg.c @@ -79,6 +90,21 @@ add_executable(cjpeg ../cjpeg.c ../cdjpeg.c ../rdgif.c ../rdppm.c ../rdjpeg.c set_property(TARGET cjpeg PROPERTY COMPILE_FLAGS ${COMPILE_FLAGS}) target_link_libraries(cjpeg jpeg) +if(PNG_SUPPORTED) + # to avoid finding static library from CMake cache + unset(PNG_LIBRARY CACHE) + unset(PNG_LIBRARY_RELEASE CACHE) + unset(PNG_LIBRARY_DEBUG CACHE) + unset(ZLIB_LIBRARY CACHE) + unset(ZLIB_LIBRARY_RELEASE CACHE) + unset(ZLIB_LIBRARY_DEBUG CACHE) + + find_package(PNG 1.6 REQUIRED) + find_package(ZLIB REQUIRED) + target_include_directories(cjpeg PUBLIC ${PNG_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) + target_link_libraries(cjpeg ${PNG_LIBRARY} ${ZLIB_LIBRARY}) +endif() + add_executable(djpeg ../djpeg.c ../cdjpeg.c ../rdcolmap.c ../rdswitch.c ../wrgif.c ../wrppm.c ${DJPEG_BMP_SOURCES}) set_property(TARGET djpeg PROPERTY COMPILE_FLAGS ${COMPILE_FLAGS}) @@ -91,10 +117,13 @@ set_property(TARGET jpegtran PROPERTY COMPILE_FLAGS "${USE_SETMODE}") add_executable(jcstest ../jcstest.c) target_link_libraries(jcstest jpeg) -install(TARGETS jpeg cjpeg djpeg jpegtran +install(TARGETS jpeg EXPORT ${CMAKE_PROJECT_NAME}Targets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(TARGETS cjpeg djpeg jpegtran + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) if(NOT CMAKE_VERSION VERSION_LESS "3.1" AND MSVC AND CMAKE_C_LINKER_SUPPORTS_PDB) install(FILES "$" diff --git a/third-party/mozjpeg/mozjpeg/simd/CMakeLists.txt b/third-party/mozjpeg/mozjpeg/simd/CMakeLists.txt old mode 100755 new mode 100644 index 60eda18f4ea..13fb835a837 --- a/third-party/mozjpeg/mozjpeg/simd/CMakeLists.txt +++ b/third-party/mozjpeg/mozjpeg/simd/CMakeLists.txt @@ -21,7 +21,7 @@ set(CMAKE_ASM_NASM_FLAGS_RELWITHDEBINFO_INIT "-g") # environment variable. This should happen automatically, but unfortunately # enable_language(ASM_NASM) doesn't parse the ASM_NASM environment variable # until after CMAKE_ASM_NASM_COMPILER has been populated with the results of -# searching for NASM or YASM in the PATH. +# searching for NASM or Yasm in the PATH. if(NOT DEFINED CMAKE_ASM_NASM_COMPILER AND DEFINED ENV{ASM_NASM}) set(CMAKE_ASM_NASM_COMPILER $ENV{ASM_NASM}) endif() @@ -30,6 +30,9 @@ if(CPU_TYPE STREQUAL "x86_64") if(CYGWIN) set(CMAKE_ASM_NASM_OBJECT_FORMAT win64) endif() + if(CMAKE_C_COMPILER_ABI MATCHES "ELF X32") + set(CMAKE_ASM_NASM_OBJECT_FORMAT elfx32) + endif() elseif(CPU_TYPE STREQUAL "i386") if(BORLAND) set(CMAKE_ASM_NASM_OBJECT_FORMAT obj) @@ -49,9 +52,9 @@ endif() enable_language(ASM_NASM) message(STATUS "CMAKE_ASM_NASM_COMPILER = ${CMAKE_ASM_NASM_COMPILER}") -if(CMAKE_ASM_NASM_OBJECT_FORMAT MATCHES "macho*") +if(CMAKE_ASM_NASM_OBJECT_FORMAT MATCHES "^macho") set(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -DMACHO") -elseif(CMAKE_ASM_NASM_OBJECT_FORMAT MATCHES "elf*") +elseif(CMAKE_ASM_NASM_OBJECT_FORMAT MATCHES "^elf") set(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -DELF") set(CMAKE_ASM_NASM_DEBUG_FORMAT "dwarf2") endif() @@ -205,64 +208,191 @@ endif() ############################################################################### -# ARM (GAS) +# Arm (Intrinsics or GAS) ############################################################################### elseif(CPU_TYPE STREQUAL "arm64" OR CPU_TYPE STREQUAL "arm") -enable_language(ASM) - -set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_ASM_FLAGS}") - -string(TOUPPER ${CMAKE_BUILD_TYPE} CMAKE_BUILD_TYPE_UC) -set(EFFECTIVE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${CMAKE_ASM_FLAGS_${CMAKE_BUILD_TYPE_UC}}") -message(STATUS "CMAKE_ASM_FLAGS = ${EFFECTIVE_ASM_FLAGS}") +# If Neon instructions are not explicitly enabled at compile time (e.g. using +# -mfpu=neon) with an AArch32 Linux or Android build, then the AArch32 SIMD +# dispatcher will parse /proc/cpuinfo to determine whether the Neon SIMD +# extensions can be enabled at run time. In order to support all AArch32 CPUs +# using the same code base, i.e. to support run-time FPU and Neon +# auto-detection, it is necessary to compile the scalar C source code using +# -mfloat-abi=soft (which is usually the default) but compile the intrinsics +# implementation of the Neon SIMD extensions using -mfloat-abi=softfp. The +# following test determines whether -mfloat-abi=softfp should be explicitly +# added to the compile flags for the intrinsics implementation of the Neon SIMD +# extensions. +if(BITS EQUAL 32) + check_c_source_compiles(" + #if defined(__ARM_NEON__) || (!defined(__linux__) && !defined(ANDROID) && !defined(__ANDROID__)) + #error \"Neon run-time auto-detection will not be used\" + #endif + #if __ARM_PCS_VFP == 1 + #error \"float ABI = hard\" + #endif + #if __SOFTFP__ != 1 + #error \"float ABI = softfp\" + #endif + int main(void) { return 0; }" NEED_SOFTFP_FOR_INTRINSICS) + if(NEED_SOFTFP_FOR_INTRINSICS) + set(SOFTFP_FLAG -mfloat-abi=softfp) + endif() +endif() -# Test whether we need gas-preprocessor.pl -if(CPU_TYPE STREQUAL "arm") - file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gastest.S " - .text - .fpu neon - .arch armv7a - .object_arch armv4 - .arm - pld [r0] - vmovn.u16 d0, q0") +if(BITS EQUAL 32) + set(CMAKE_REQUIRED_FLAGS "-mfpu=neon ${SOFTFP_FLAG}") + check_c_source_compiles(" + #include + int main(int argc, char **argv) { + uint16x8_t input = vdupq_n_u16((uint16_t)argc); + uint8x8_t output = vmovn_u16(input); + return (int)output[0]; + }" HAVE_NEON) + if(NOT HAVE_NEON) + simd_fail("SIMD extensions not available for this architecture") + return() + endif() +endif() +check_c_source_compiles(" + #include + int main(int argc, char **argv) { + int16_t input[] = { + (int16_t)argc, (int16_t)argc, (int16_t)argc, (int16_t)argc, + (int16_t)argc, (int16_t)argc, (int16_t)argc, (int16_t)argc, + (int16_t)argc, (int16_t)argc, (int16_t)argc, (int16_t)argc + }; + int16x4x3_t output = vld1_s16_x3(input); + vst3_s16(input, output); + return (int)input[0]; + }" HAVE_VLD1_S16_X3) +check_c_source_compiles(" + #include + int main(int argc, char **argv) { + uint16_t input[] = { + (uint16_t)argc, (uint16_t)argc, (uint16_t)argc, (uint16_t)argc, + (uint16_t)argc, (uint16_t)argc, (uint16_t)argc, (uint16_t)argc + }; + uint16x4x2_t output = vld1_u16_x2(input); + vst2_u16(input, output); + return (int)input[0]; + }" HAVE_VLD1_U16_X2) +check_c_source_compiles(" + #include + int main(int argc, char **argv) { + uint8_t input[] = { + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, + (uint8_t)argc, (uint8_t)argc, (uint8_t)argc, (uint8_t)argc + }; + uint8x16x4_t output = vld1q_u8_x4(input); + vst4q_u8(input, output); + return (int)input[0]; + }" HAVE_VLD1Q_U8_X4) +if(BITS EQUAL 32) + unset(CMAKE_REQUIRED_FLAGS) +endif() +configure_file(arm/neon-compat.h.in arm/neon-compat.h @ONLY) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/arm) + +# GCC 11 and earlier and some older versions of Clang do not have a full or +# optimal set of Neon intrinsics, so for performance reasons, when using those +# compilers, we default to using the older GAS implementation of the Neon SIMD +# extensions for certain algorithms. The presence or absence of the three +# intrinsics we tested above is a reasonable proxy for this, except with GCC 10 +# and 11. +if((HAVE_VLD1_S16_X3 AND HAVE_VLD1_U16_X2 AND HAVE_VLD1Q_U8_X4 AND + (NOT CMAKE_COMPILER_IS_GNUCC OR + CMAKE_C_COMPILER_VERSION VERSION_EQUAL 12.0.0 OR + CMAKE_C_COMPILER_VERSION VERSION_GREATER 12.0.0))) + set(DEFAULT_NEON_INTRINSICS 1) else() - file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gastest.S " - .text - MYVAR .req x0 - movi v0.16b, #100 - mov MYVAR, #100 - .unreq MYVAR") -endif() - -separate_arguments(CMAKE_ASM_FLAGS_SEP UNIX_COMMAND "${CMAKE_ASM_FLAGS}") - -execute_process(COMMAND ${CMAKE_ASM_COMPILER} ${CMAKE_ASM_FLAGS_SEP} - -x assembler-with-cpp -c ${CMAKE_CURRENT_BINARY_DIR}/gastest.S - RESULT_VARIABLE RESULT OUTPUT_VARIABLE OUTPUT ERROR_VARIABLE ERROR) -if(NOT RESULT EQUAL 0) - message(STATUS "GAS appears to be broken. Trying gas-preprocessor.pl ...") - execute_process(COMMAND gas-preprocessor.pl ${CMAKE_ASM_COMPILER} - ${CMAKE_ASM_FLAGS_SEP} -x assembler-with-cpp -c - ${CMAKE_CURRENT_BINARY_DIR}/gastest.S + set(DEFAULT_NEON_INTRINSICS 0) +endif() +option(NEON_INTRINSICS + "Because GCC (as of this writing) and some older versions of Clang do not have a full or optimal set of Neon intrinsics, for performance reasons, the default when building libjpeg-turbo with those compilers is to continue using the older GAS implementation of the Neon SIMD extensions for certain algorithms. Setting this option forces the full Neon intrinsics implementation to be used with all compilers. Unsetting this option forces the hybrid GAS/intrinsics implementation to be used with all compilers." + ${DEFAULT_NEON_INTRINSICS}) +if(NOT NEON_INTRINSICS) + enable_language(ASM) + + set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_ASM_FLAGS}") + + # Test whether gas-preprocessor.pl would be needed to build the GAS + # implementation of the Neon SIMD extensions. If so, then automatically + # enable the full Neon intrinsics implementation. + if(CPU_TYPE STREQUAL "arm") + file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gastest.S " + .text + .fpu neon + .arch armv7a + .object_arch armv4 + .arm + pld [r0] + vmovn.u16 d0, q0") + else() + file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gastest.S " + .text + MYVAR .req x0 + movi v0.16b, #100 + mov MYVAR, #100 + .unreq MYVAR") + endif() + separate_arguments(CMAKE_ASM_FLAGS_SEP UNIX_COMMAND "${CMAKE_ASM_FLAGS}") + execute_process(COMMAND ${CMAKE_ASM_COMPILER} ${CMAKE_ASM_FLAGS_SEP} + -x assembler-with-cpp -c ${CMAKE_CURRENT_BINARY_DIR}/gastest.S RESULT_VARIABLE RESULT OUTPUT_VARIABLE OUTPUT ERROR_VARIABLE ERROR) if(NOT RESULT EQUAL 0) - simd_fail("SIMD extensions disabled: GAS is not working properly") - return() - else() - message(STATUS "Using gas-preprocessor.pl") - configure_file(gas-preprocessor.in gas-preprocessor @ONLY) - set(CMAKE_ASM_COMPILER ${CMAKE_CURRENT_BINARY_DIR}/gas-preprocessor) + message(WARNING "GAS appears to be broken. Using the full Neon SIMD intrinsics implementation.") + set(NEON_INTRINSICS 1 CACHE INTERNAL "" FORCE) endif() +endif() +boolean_number(NEON_INTRINSICS PARENT_SCOPE) +if(NEON_INTRINSICS) + add_definitions(-DNEON_INTRINSICS) + message(STATUS "Use full Neon SIMD intrinsics implementation (NEON_INTRINSICS = ${NEON_INTRINSICS})") else() - message(STATUS "GAS is working properly") + message(STATUS "Use partial Neon SIMD intrinsics implementation (NEON_INTRINSICS = ${NEON_INTRINSICS})") endif() -file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/gastest.S) +set(SIMD_SOURCES arm/jcgray-neon.c arm/jcphuff-neon.c arm/jcsample-neon.c + arm/jdmerge-neon.c arm/jdsample-neon.c arm/jfdctfst-neon.c + arm/jidctred-neon.c arm/jquanti-neon.c) +if(NEON_INTRINSICS) + set(SIMD_SOURCES ${SIMD_SOURCES} arm/jccolor-neon.c arm/jidctint-neon.c) +endif() +if(NEON_INTRINSICS OR BITS EQUAL 64) + set(SIMD_SOURCES ${SIMD_SOURCES} arm/jidctfst-neon.c) +endif() +if(NEON_INTRINSICS OR BITS EQUAL 32) + set(SIMD_SOURCES ${SIMD_SOURCES} arm/aarch${BITS}/jchuff-neon.c + arm/jdcolor-neon.c arm/jfdctint-neon.c) +endif() +if(BITS EQUAL 32) + set_source_files_properties(${SIMD_SOURCES} COMPILE_FLAGS "-mfpu=neon ${SOFTFP_FLAG}") +endif() +if(NOT NEON_INTRINSICS) + string(TOUPPER ${CMAKE_BUILD_TYPE} CMAKE_BUILD_TYPE_UC) + set(EFFECTIVE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${CMAKE_ASM_FLAGS_${CMAKE_BUILD_TYPE_UC}}") + message(STATUS "CMAKE_ASM_FLAGS = ${EFFECTIVE_ASM_FLAGS}") + + set(SIMD_SOURCES ${SIMD_SOURCES} arm/aarch${BITS}/jsimd_neon.S) +endif() -add_library(simd OBJECT ${CPU_TYPE}/jsimd_neon.S ${CPU_TYPE}/jsimd.c) +add_library(simd OBJECT ${SIMD_SOURCES} arm/aarch${BITS}/jsimd.c) if(CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED) set_target_properties(simd PROPERTIES POSITION_INDEPENDENT_CODE 1) @@ -311,14 +441,38 @@ if(CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED) endif() ############################################################################### -# Loongson (Intrinsics) +# MIPS64 (Intrinsics) ############################################################################### -elseif(CPU_TYPE STREQUAL "loongson") +elseif(CPU_TYPE STREQUAL "loongson" OR CPU_TYPE MATCHES "^mips64") + +set(CMAKE_REQUIRED_FLAGS -Wa,-mloongson-mmi,-mloongson-ext) + +check_c_source_compiles(" + #if !(defined(__mips__) && __mips_isa_rev < 6) + #error Loongson MMI can't work with MIPS Release 6+ + #endif + int main(void) { + int c = 0, a = 0, b = 0; + asm ( + \"paddb %0, %1, %2\" + : \"=f\" (c) + : \"f\" (a), \"f\" (b) + ); + return c; + }" HAVE_MMI) + +unset(CMAKE_REQUIRED_FLAGS) -set(SIMD_SOURCES loongson/jccolor-mmi.c loongson/jcsample-mmi.c - loongson/jdcolor-mmi.c loongson/jdsample-mmi.c loongson/jfdctint-mmi.c - loongson/jidctint-mmi.c loongson/jquanti-mmi.c) +if(NOT HAVE_MMI) + simd_fail("SIMD extensions not available for this CPU") + return() +endif() + +set(SIMD_SOURCES mips64/jccolor-mmi.c mips64/jcgray-mmi.c mips64/jcsample-mmi.c + mips64/jdcolor-mmi.c mips64/jdmerge-mmi.c mips64/jdsample-mmi.c + mips64/jfdctfst-mmi.c mips64/jfdctint-mmi.c mips64/jidctfst-mmi.c + mips64/jidctint-mmi.c mips64/jquanti-mmi.c) if(CMAKE_COMPILER_IS_GNUCC) foreach(file ${SIMD_SOURCES}) @@ -326,8 +480,12 @@ if(CMAKE_COMPILER_IS_GNUCC) " -fno-strict-aliasing") endforeach() endif() +foreach(file ${SIMD_SOURCES}) + set_property(SOURCE ${file} APPEND_STRING PROPERTY COMPILE_FLAGS + " -Wa,-mloongson-mmi,-mloongson-ext") +endforeach() -add_library(simd OBJECT ${SIMD_SOURCES} loongson/jsimd.c) +add_library(simd OBJECT ${SIMD_SOURCES} mips64/jsimd.c) if(CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED) set_target_properties(simd PROPERTIES POSITION_INDEPENDENT_CODE 1) diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jccolext-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jccolext-neon.c new file mode 100644 index 00000000000..362102d2b2d --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jccolext-neon.c @@ -0,0 +1,148 @@ +/* + * jccolext-neon.c - colorspace conversion (32-bit Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jccolor-neon.c */ + + +/* RGB -> YCbCr conversion is defined by the following equations: + * Y = 0.29900 * R + 0.58700 * G + 0.11400 * B + * Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + 128 + * Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + 128 + * + * Avoid floating point arithmetic by using shifted integer constants: + * 0.29899597 = 19595 * 2^-16 + * 0.58700561 = 38470 * 2^-16 + * 0.11399841 = 7471 * 2^-16 + * 0.16874695 = 11059 * 2^-16 + * 0.33125305 = 21709 * 2^-16 + * 0.50000000 = 32768 * 2^-16 + * 0.41868592 = 27439 * 2^-16 + * 0.08131409 = 5329 * 2^-16 + * These constants are defined in jccolor-neon.c + * + * We add the fixed-point equivalent of 0.5 to Cb and Cr, which effectively + * rounds up or down the result via integer truncation. + */ + +void jsimd_rgb_ycc_convert_neon(JDIMENSION image_width, JSAMPARRAY input_buf, + JSAMPIMAGE output_buf, JDIMENSION output_row, + int num_rows) +{ + /* Pointer to RGB(X/A) input data */ + JSAMPROW inptr; + /* Pointers to Y, Cb, and Cr output data */ + JSAMPROW outptr0, outptr1, outptr2; + /* Allocate temporary buffer for final (image_width % 8) pixels in row. */ + ALIGN(16) uint8_t tmp_buf[8 * RGB_PIXELSIZE]; + + /* Set up conversion constants. */ +#ifdef HAVE_VLD1_U16_X2 + const uint16x4x2_t consts = vld1_u16_x2(jsimd_rgb_ycc_neon_consts); +#else + /* GCC does not currently support the intrinsic vld1__x2(). */ + const uint16x4_t consts1 = vld1_u16(jsimd_rgb_ycc_neon_consts); + const uint16x4_t consts2 = vld1_u16(jsimd_rgb_ycc_neon_consts + 4); + const uint16x4x2_t consts = { { consts1, consts2 } }; +#endif + const uint32x4_t scaled_128_5 = vdupq_n_u32((128 << 16) + 32767); + + while (--num_rows >= 0) { + inptr = *input_buf++; + outptr0 = output_buf[0][output_row]; + outptr1 = output_buf[1][output_row]; + outptr2 = output_buf[2][output_row]; + output_row++; + + int cols_remaining = image_width; + for (; cols_remaining > 0; cols_remaining -= 8) { + + /* To prevent buffer overread by the vector load instructions, the last + * (image_width % 8) columns of data are first memcopied to a temporary + * buffer large enough to accommodate the vector load. + */ + if (cols_remaining < 8) { + memcpy(tmp_buf, inptr, cols_remaining * RGB_PIXELSIZE); + inptr = tmp_buf; + } + +#if RGB_PIXELSIZE == 4 + uint8x8x4_t input_pixels = vld4_u8(inptr); +#else + uint8x8x3_t input_pixels = vld3_u8(inptr); +#endif + uint16x8_t r = vmovl_u8(input_pixels.val[RGB_RED]); + uint16x8_t g = vmovl_u8(input_pixels.val[RGB_GREEN]); + uint16x8_t b = vmovl_u8(input_pixels.val[RGB_BLUE]); + + /* Compute Y = 0.29900 * R + 0.58700 * G + 0.11400 * B */ + uint32x4_t y_low = vmull_lane_u16(vget_low_u16(r), consts.val[0], 0); + y_low = vmlal_lane_u16(y_low, vget_low_u16(g), consts.val[0], 1); + y_low = vmlal_lane_u16(y_low, vget_low_u16(b), consts.val[0], 2); + uint32x4_t y_high = vmull_lane_u16(vget_high_u16(r), consts.val[0], 0); + y_high = vmlal_lane_u16(y_high, vget_high_u16(g), consts.val[0], 1); + y_high = vmlal_lane_u16(y_high, vget_high_u16(b), consts.val[0], 2); + + /* Compute Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + 128 */ + uint32x4_t cb_low = scaled_128_5; + cb_low = vmlsl_lane_u16(cb_low, vget_low_u16(r), consts.val[0], 3); + cb_low = vmlsl_lane_u16(cb_low, vget_low_u16(g), consts.val[1], 0); + cb_low = vmlal_lane_u16(cb_low, vget_low_u16(b), consts.val[1], 1); + uint32x4_t cb_high = scaled_128_5; + cb_high = vmlsl_lane_u16(cb_high, vget_high_u16(r), consts.val[0], 3); + cb_high = vmlsl_lane_u16(cb_high, vget_high_u16(g), consts.val[1], 0); + cb_high = vmlal_lane_u16(cb_high, vget_high_u16(b), consts.val[1], 1); + + /* Compute Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + 128 */ + uint32x4_t cr_low = scaled_128_5; + cr_low = vmlal_lane_u16(cr_low, vget_low_u16(r), consts.val[1], 1); + cr_low = vmlsl_lane_u16(cr_low, vget_low_u16(g), consts.val[1], 2); + cr_low = vmlsl_lane_u16(cr_low, vget_low_u16(b), consts.val[1], 3); + uint32x4_t cr_high = scaled_128_5; + cr_high = vmlal_lane_u16(cr_high, vget_high_u16(r), consts.val[1], 1); + cr_high = vmlsl_lane_u16(cr_high, vget_high_u16(g), consts.val[1], 2); + cr_high = vmlsl_lane_u16(cr_high, vget_high_u16(b), consts.val[1], 3); + + /* Descale Y values (rounding right shift) and narrow to 16-bit. */ + uint16x8_t y_u16 = vcombine_u16(vrshrn_n_u32(y_low, 16), + vrshrn_n_u32(y_high, 16)); + /* Descale Cb values (right shift) and narrow to 16-bit. */ + uint16x8_t cb_u16 = vcombine_u16(vshrn_n_u32(cb_low, 16), + vshrn_n_u32(cb_high, 16)); + /* Descale Cr values (right shift) and narrow to 16-bit. */ + uint16x8_t cr_u16 = vcombine_u16(vshrn_n_u32(cr_low, 16), + vshrn_n_u32(cr_high, 16)); + /* Narrow Y, Cb, and Cr values to 8-bit and store to memory. Buffer + * overwrite is permitted up to the next multiple of ALIGN_SIZE bytes. + */ + vst1_u8(outptr0, vmovn_u16(y_u16)); + vst1_u8(outptr1, vmovn_u16(cb_u16)); + vst1_u8(outptr2, vmovn_u16(cr_u16)); + + /* Increment pointers. */ + inptr += (8 * RGB_PIXELSIZE); + outptr0 += 8; + outptr1 += 8; + outptr2 += 8; + } + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jchuff-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jchuff-neon.c new file mode 100644 index 00000000000..19d94f720da --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jchuff-neon.c @@ -0,0 +1,334 @@ +/* + * jchuff-neon.c - Huffman entropy encoding (32-bit Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * NOTE: All referenced figures are from + * Recommendation ITU-T T.81 (1992) | ISO/IEC 10918-1:1994. + */ + +#define JPEG_INTERNALS +#include "../../../jinclude.h" +#include "../../../jpeglib.h" +#include "../../../jsimd.h" +#include "../../../jdct.h" +#include "../../../jsimddct.h" +#include "../../jsimd.h" +#include "../jchuff.h" +#include "neon-compat.h" + +#include + +#include + + +JOCTET *jsimd_huff_encode_one_block_neon(void *state, JOCTET *buffer, + JCOEFPTR block, int last_dc_val, + c_derived_tbl *dctbl, + c_derived_tbl *actbl) +{ + uint8_t block_nbits[DCTSIZE2]; + uint16_t block_diff[DCTSIZE2]; + + /* Load rows of coefficients from DCT block in zig-zag order. */ + + /* Compute DC coefficient difference value. (F.1.1.5.1) */ + int16x8_t row0 = vdupq_n_s16(block[0] - last_dc_val); + row0 = vld1q_lane_s16(block + 1, row0, 1); + row0 = vld1q_lane_s16(block + 8, row0, 2); + row0 = vld1q_lane_s16(block + 16, row0, 3); + row0 = vld1q_lane_s16(block + 9, row0, 4); + row0 = vld1q_lane_s16(block + 2, row0, 5); + row0 = vld1q_lane_s16(block + 3, row0, 6); + row0 = vld1q_lane_s16(block + 10, row0, 7); + + int16x8_t row1 = vld1q_dup_s16(block + 17); + row1 = vld1q_lane_s16(block + 24, row1, 1); + row1 = vld1q_lane_s16(block + 32, row1, 2); + row1 = vld1q_lane_s16(block + 25, row1, 3); + row1 = vld1q_lane_s16(block + 18, row1, 4); + row1 = vld1q_lane_s16(block + 11, row1, 5); + row1 = vld1q_lane_s16(block + 4, row1, 6); + row1 = vld1q_lane_s16(block + 5, row1, 7); + + int16x8_t row2 = vld1q_dup_s16(block + 12); + row2 = vld1q_lane_s16(block + 19, row2, 1); + row2 = vld1q_lane_s16(block + 26, row2, 2); + row2 = vld1q_lane_s16(block + 33, row2, 3); + row2 = vld1q_lane_s16(block + 40, row2, 4); + row2 = vld1q_lane_s16(block + 48, row2, 5); + row2 = vld1q_lane_s16(block + 41, row2, 6); + row2 = vld1q_lane_s16(block + 34, row2, 7); + + int16x8_t row3 = vld1q_dup_s16(block + 27); + row3 = vld1q_lane_s16(block + 20, row3, 1); + row3 = vld1q_lane_s16(block + 13, row3, 2); + row3 = vld1q_lane_s16(block + 6, row3, 3); + row3 = vld1q_lane_s16(block + 7, row3, 4); + row3 = vld1q_lane_s16(block + 14, row3, 5); + row3 = vld1q_lane_s16(block + 21, row3, 6); + row3 = vld1q_lane_s16(block + 28, row3, 7); + + int16x8_t abs_row0 = vabsq_s16(row0); + int16x8_t abs_row1 = vabsq_s16(row1); + int16x8_t abs_row2 = vabsq_s16(row2); + int16x8_t abs_row3 = vabsq_s16(row3); + + int16x8_t row0_lz = vclzq_s16(abs_row0); + int16x8_t row1_lz = vclzq_s16(abs_row1); + int16x8_t row2_lz = vclzq_s16(abs_row2); + int16x8_t row3_lz = vclzq_s16(abs_row3); + + /* Compute number of bits required to represent each coefficient. */ + uint8x8_t row0_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row0_lz))); + uint8x8_t row1_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row1_lz))); + uint8x8_t row2_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row2_lz))); + uint8x8_t row3_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row3_lz))); + + vst1_u8(block_nbits + 0 * DCTSIZE, row0_nbits); + vst1_u8(block_nbits + 1 * DCTSIZE, row1_nbits); + vst1_u8(block_nbits + 2 * DCTSIZE, row2_nbits); + vst1_u8(block_nbits + 3 * DCTSIZE, row3_nbits); + + uint16x8_t row0_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row0, 15)), + vnegq_s16(row0_lz)); + uint16x8_t row1_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row1, 15)), + vnegq_s16(row1_lz)); + uint16x8_t row2_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row2, 15)), + vnegq_s16(row2_lz)); + uint16x8_t row3_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row3, 15)), + vnegq_s16(row3_lz)); + + uint16x8_t row0_diff = veorq_u16(vreinterpretq_u16_s16(abs_row0), row0_mask); + uint16x8_t row1_diff = veorq_u16(vreinterpretq_u16_s16(abs_row1), row1_mask); + uint16x8_t row2_diff = veorq_u16(vreinterpretq_u16_s16(abs_row2), row2_mask); + uint16x8_t row3_diff = veorq_u16(vreinterpretq_u16_s16(abs_row3), row3_mask); + + /* Store diff values for rows 0, 1, 2, and 3. */ + vst1q_u16(block_diff + 0 * DCTSIZE, row0_diff); + vst1q_u16(block_diff + 1 * DCTSIZE, row1_diff); + vst1q_u16(block_diff + 2 * DCTSIZE, row2_diff); + vst1q_u16(block_diff + 3 * DCTSIZE, row3_diff); + + /* Load last four rows of coefficients from DCT block in zig-zag order. */ + int16x8_t row4 = vld1q_dup_s16(block + 35); + row4 = vld1q_lane_s16(block + 42, row4, 1); + row4 = vld1q_lane_s16(block + 49, row4, 2); + row4 = vld1q_lane_s16(block + 56, row4, 3); + row4 = vld1q_lane_s16(block + 57, row4, 4); + row4 = vld1q_lane_s16(block + 50, row4, 5); + row4 = vld1q_lane_s16(block + 43, row4, 6); + row4 = vld1q_lane_s16(block + 36, row4, 7); + + int16x8_t row5 = vld1q_dup_s16(block + 29); + row5 = vld1q_lane_s16(block + 22, row5, 1); + row5 = vld1q_lane_s16(block + 15, row5, 2); + row5 = vld1q_lane_s16(block + 23, row5, 3); + row5 = vld1q_lane_s16(block + 30, row5, 4); + row5 = vld1q_lane_s16(block + 37, row5, 5); + row5 = vld1q_lane_s16(block + 44, row5, 6); + row5 = vld1q_lane_s16(block + 51, row5, 7); + + int16x8_t row6 = vld1q_dup_s16(block + 58); + row6 = vld1q_lane_s16(block + 59, row6, 1); + row6 = vld1q_lane_s16(block + 52, row6, 2); + row6 = vld1q_lane_s16(block + 45, row6, 3); + row6 = vld1q_lane_s16(block + 38, row6, 4); + row6 = vld1q_lane_s16(block + 31, row6, 5); + row6 = vld1q_lane_s16(block + 39, row6, 6); + row6 = vld1q_lane_s16(block + 46, row6, 7); + + int16x8_t row7 = vld1q_dup_s16(block + 53); + row7 = vld1q_lane_s16(block + 60, row7, 1); + row7 = vld1q_lane_s16(block + 61, row7, 2); + row7 = vld1q_lane_s16(block + 54, row7, 3); + row7 = vld1q_lane_s16(block + 47, row7, 4); + row7 = vld1q_lane_s16(block + 55, row7, 5); + row7 = vld1q_lane_s16(block + 62, row7, 6); + row7 = vld1q_lane_s16(block + 63, row7, 7); + + int16x8_t abs_row4 = vabsq_s16(row4); + int16x8_t abs_row5 = vabsq_s16(row5); + int16x8_t abs_row6 = vabsq_s16(row6); + int16x8_t abs_row7 = vabsq_s16(row7); + + int16x8_t row4_lz = vclzq_s16(abs_row4); + int16x8_t row5_lz = vclzq_s16(abs_row5); + int16x8_t row6_lz = vclzq_s16(abs_row6); + int16x8_t row7_lz = vclzq_s16(abs_row7); + + /* Compute number of bits required to represent each coefficient. */ + uint8x8_t row4_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row4_lz))); + uint8x8_t row5_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row5_lz))); + uint8x8_t row6_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row6_lz))); + uint8x8_t row7_nbits = vsub_u8(vdup_n_u8(16), + vmovn_u16(vreinterpretq_u16_s16(row7_lz))); + + vst1_u8(block_nbits + 4 * DCTSIZE, row4_nbits); + vst1_u8(block_nbits + 5 * DCTSIZE, row5_nbits); + vst1_u8(block_nbits + 6 * DCTSIZE, row6_nbits); + vst1_u8(block_nbits + 7 * DCTSIZE, row7_nbits); + + uint16x8_t row4_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row4, 15)), + vnegq_s16(row4_lz)); + uint16x8_t row5_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row5, 15)), + vnegq_s16(row5_lz)); + uint16x8_t row6_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row6, 15)), + vnegq_s16(row6_lz)); + uint16x8_t row7_mask = + vshlq_u16(vreinterpretq_u16_s16(vshrq_n_s16(row7, 15)), + vnegq_s16(row7_lz)); + + uint16x8_t row4_diff = veorq_u16(vreinterpretq_u16_s16(abs_row4), row4_mask); + uint16x8_t row5_diff = veorq_u16(vreinterpretq_u16_s16(abs_row5), row5_mask); + uint16x8_t row6_diff = veorq_u16(vreinterpretq_u16_s16(abs_row6), row6_mask); + uint16x8_t row7_diff = veorq_u16(vreinterpretq_u16_s16(abs_row7), row7_mask); + + /* Store diff values for rows 4, 5, 6, and 7. */ + vst1q_u16(block_diff + 4 * DCTSIZE, row4_diff); + vst1q_u16(block_diff + 5 * DCTSIZE, row5_diff); + vst1q_u16(block_diff + 6 * DCTSIZE, row6_diff); + vst1q_u16(block_diff + 7 * DCTSIZE, row7_diff); + + /* Construct bitmap to accelerate encoding of AC coefficients. A set bit + * means that the corresponding coefficient != 0. + */ + uint8x8_t row0_nbits_gt0 = vcgt_u8(row0_nbits, vdup_n_u8(0)); + uint8x8_t row1_nbits_gt0 = vcgt_u8(row1_nbits, vdup_n_u8(0)); + uint8x8_t row2_nbits_gt0 = vcgt_u8(row2_nbits, vdup_n_u8(0)); + uint8x8_t row3_nbits_gt0 = vcgt_u8(row3_nbits, vdup_n_u8(0)); + uint8x8_t row4_nbits_gt0 = vcgt_u8(row4_nbits, vdup_n_u8(0)); + uint8x8_t row5_nbits_gt0 = vcgt_u8(row5_nbits, vdup_n_u8(0)); + uint8x8_t row6_nbits_gt0 = vcgt_u8(row6_nbits, vdup_n_u8(0)); + uint8x8_t row7_nbits_gt0 = vcgt_u8(row7_nbits, vdup_n_u8(0)); + + /* { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 } */ + const uint8x8_t bitmap_mask = + vreinterpret_u8_u64(vmov_n_u64(0x0102040810204080)); + + row0_nbits_gt0 = vand_u8(row0_nbits_gt0, bitmap_mask); + row1_nbits_gt0 = vand_u8(row1_nbits_gt0, bitmap_mask); + row2_nbits_gt0 = vand_u8(row2_nbits_gt0, bitmap_mask); + row3_nbits_gt0 = vand_u8(row3_nbits_gt0, bitmap_mask); + row4_nbits_gt0 = vand_u8(row4_nbits_gt0, bitmap_mask); + row5_nbits_gt0 = vand_u8(row5_nbits_gt0, bitmap_mask); + row6_nbits_gt0 = vand_u8(row6_nbits_gt0, bitmap_mask); + row7_nbits_gt0 = vand_u8(row7_nbits_gt0, bitmap_mask); + + uint8x8_t bitmap_rows_10 = vpadd_u8(row1_nbits_gt0, row0_nbits_gt0); + uint8x8_t bitmap_rows_32 = vpadd_u8(row3_nbits_gt0, row2_nbits_gt0); + uint8x8_t bitmap_rows_54 = vpadd_u8(row5_nbits_gt0, row4_nbits_gt0); + uint8x8_t bitmap_rows_76 = vpadd_u8(row7_nbits_gt0, row6_nbits_gt0); + uint8x8_t bitmap_rows_3210 = vpadd_u8(bitmap_rows_32, bitmap_rows_10); + uint8x8_t bitmap_rows_7654 = vpadd_u8(bitmap_rows_76, bitmap_rows_54); + uint8x8_t bitmap = vpadd_u8(bitmap_rows_7654, bitmap_rows_3210); + + /* Shift left to remove DC bit. */ + bitmap = vreinterpret_u8_u64(vshl_n_u64(vreinterpret_u64_u8(bitmap), 1)); + /* Move bitmap to 32-bit scalar registers. */ + uint32_t bitmap_1_32 = vget_lane_u32(vreinterpret_u32_u8(bitmap), 1); + uint32_t bitmap_33_63 = vget_lane_u32(vreinterpret_u32_u8(bitmap), 0); + + /* Set up state and bit buffer for output bitstream. */ + working_state *state_ptr = (working_state *)state; + int free_bits = state_ptr->cur.free_bits; + size_t put_buffer = state_ptr->cur.put_buffer; + + /* Encode DC coefficient. */ + + unsigned int nbits = block_nbits[0]; + /* Emit Huffman-coded symbol and additional diff bits. */ + unsigned int diff = block_diff[0]; + PUT_CODE(dctbl->ehufco[nbits], dctbl->ehufsi[nbits], diff) + + /* Encode AC coefficients. */ + + unsigned int r = 0; /* r = run length of zeros */ + unsigned int i = 1; /* i = number of coefficients encoded */ + /* Code and size information for a run length of 16 zero coefficients */ + const unsigned int code_0xf0 = actbl->ehufco[0xf0]; + const unsigned int size_0xf0 = actbl->ehufsi[0xf0]; + + while (bitmap_1_32 != 0) { + r = BUILTIN_CLZ(bitmap_1_32); + i += r; + bitmap_1_32 <<= r; + nbits = block_nbits[i]; + diff = block_diff[i]; + while (r > 15) { + /* If run length > 15, emit special run-length-16 codes. */ + PUT_BITS(code_0xf0, size_0xf0) + r -= 16; + } + /* Emit Huffman symbol for run length / number of bits. (F.1.2.2.1) */ + unsigned int rs = (r << 4) + nbits; + PUT_CODE(actbl->ehufco[rs], actbl->ehufsi[rs], diff) + i++; + bitmap_1_32 <<= 1; + } + + r = 33 - i; + i = 33; + + while (bitmap_33_63 != 0) { + unsigned int leading_zeros = BUILTIN_CLZ(bitmap_33_63); + r += leading_zeros; + i += leading_zeros; + bitmap_33_63 <<= leading_zeros; + nbits = block_nbits[i]; + diff = block_diff[i]; + while (r > 15) { + /* If run length > 15, emit special run-length-16 codes. */ + PUT_BITS(code_0xf0, size_0xf0) + r -= 16; + } + /* Emit Huffman symbol for run length / number of bits. (F.1.2.2.1) */ + unsigned int rs = (r << 4) + nbits; + PUT_CODE(actbl->ehufco[rs], actbl->ehufsi[rs], diff) + r = 0; + i++; + bitmap_33_63 <<= 1; + } + + /* If the last coefficient(s) were zero, emit an end-of-block (EOB) code. + * The value of RS for the EOB code is 0. + */ + if (i != 64) { + PUT_BITS(actbl->ehufco[0], actbl->ehufsi[0]) + } + + state_ptr->cur.put_buffer = put_buffer; + state_ptr->cur.free_bits = free_bits; + + return buffer; +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jsimd.c b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jsimd.c similarity index 65% rename from third-party/mozjpeg/mozjpeg/simd/arm/jsimd.c rename to third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jsimd.c index 45f9b047f6b..04d64526fb2 100644 --- a/third-party/mozjpeg/mozjpeg/simd/arm/jsimd.c +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jsimd.c @@ -3,9 +3,10 @@ * * Copyright 2009 Pierre Ossman for Cendio AB * Copyright (C) 2011, Nokia Corporation and/or its subsidiary(-ies). - * Copyright (C) 2009-2011, 2013-2014, 2016, 2018, D. R. Commander. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2009-2011, 2013-2014, 2016, 2018, 2022, D. R. Commander. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. * Copyright (C) 2019, Google LLC. + * Copyright (C) 2020, Arm Limited. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -13,23 +14,21 @@ * * This file contains the interface between the "normal" portions * of the library and the SIMD implementations when running on a - * 32-bit ARM architecture. + * 32-bit Arm architecture. */ #define JPEG_INTERNALS -#include "../../jinclude.h" -#include "../../jpeglib.h" +#include "../../../jinclude.h" +#include "../../../jpeglib.h" +#include "../../../jsimd.h" +#include "../../../jdct.h" +#include "../../../jsimddct.h" #include "../../jsimd.h" -#include "../../jdct.h" -#include "../../jsimddct.h" -#include "../jsimd.h" -#include -#include #include -static unsigned int simd_support = ~0; -static unsigned int simd_huffman = 1; +static THREAD_LOCAL unsigned int simd_support = ~0; +static THREAD_LOCAL unsigned int simd_huffman = 1; #if !defined(__ARM_NEON__) && (defined(__linux__) || defined(ANDROID) || defined(__ANDROID__)) @@ -97,14 +96,12 @@ parse_proc_cpuinfo(int bufsize) /* * Check what SIMD accelerations are supported. - * - * FIXME: This code is racy under a multi-threaded environment. */ LOCAL(void) init_simd(void) { #ifndef NO_GETENV - char *env = NULL; + char env[2] = { 0 }; #endif #if !defined(__ARM_NEON__) && (defined(__linux__) || defined(ANDROID) || defined(__ANDROID__)) int bufsize = 1024; /* an initial guess for the line buffer size limit */ @@ -118,7 +115,7 @@ init_simd(void) #if defined(__ARM_NEON__) simd_support |= JSIMD_NEON; #elif defined(__linux__) || defined(ANDROID) || defined(__ANDROID__) - /* We still have a chance to use NEON regardless of globally used + /* We still have a chance to use Neon regardless of globally used * -mcpu/-mfpu options passed to gcc by performing runtime detection via * /proc/cpuinfo parsing on linux/android */ while (!parse_proc_cpuinfo(bufsize)) { @@ -130,14 +127,11 @@ init_simd(void) #ifndef NO_GETENV /* Force different settings through environment variables */ - env = getenv("JSIMD_FORCENEON"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCENEON") && !strcmp(env, "1")) simd_support = JSIMD_NEON; - env = getenv("JSIMD_FORCENONE"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCENONE") && !strcmp(env, "1")) simd_support = 0; - env = getenv("JSIMD_NOHUFFENC"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_NOHUFFENC") && !strcmp(env, "1")) simd_huffman = 0; #endif } @@ -164,6 +158,19 @@ jsimd_can_rgb_ycc(void) GLOBAL(int) jsimd_can_rgb_gray(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + if ((RGB_PIXELSIZE != 3) && (RGB_PIXELSIZE != 4)) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -246,6 +253,37 @@ jsimd_rgb_gray_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows) { + void (*neonfct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); + + switch (cinfo->in_color_space) { + case JCS_EXT_RGB: + neonfct = jsimd_extrgb_gray_convert_neon; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + neonfct = jsimd_extrgbx_gray_convert_neon; + break; + case JCS_EXT_BGR: + neonfct = jsimd_extbgr_gray_convert_neon; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + neonfct = jsimd_extbgrx_gray_convert_neon; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + neonfct = jsimd_extxbgr_gray_convert_neon; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + neonfct = jsimd_extxrgb_gray_convert_neon; + break; + default: + neonfct = jsimd_extrgb_gray_convert_neon; + break; + } + + neonfct(cinfo->image_width, input_buf, output_buf, output_row, num_rows); } GLOBAL(void) @@ -298,12 +336,38 @@ jsimd_ycc_rgb565_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, GLOBAL(int) jsimd_can_h2v2_downsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (DCTSIZE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_can_h2v1_downsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (DCTSIZE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -311,23 +375,50 @@ GLOBAL(void) jsimd_h2v2_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY output_data) { + jsimd_h2v2_downsample_neon(cinfo->image_width, cinfo->max_v_samp_factor, + compptr->v_samp_factor, compptr->width_in_blocks, + input_data, output_data); } GLOBAL(void) jsimd_h2v1_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY output_data) { + jsimd_h2v1_downsample_neon(cinfo->image_width, cinfo->max_v_samp_factor, + compptr->v_samp_factor, compptr->width_in_blocks, + input_data, output_data); } GLOBAL(int) jsimd_can_h2v2_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_can_h2v1_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -335,17 +426,32 @@ GLOBAL(void) jsimd_h2v2_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v2_upsample_neon(cinfo->max_v_samp_factor, cinfo->output_width, + input_data, output_data_ptr); } GLOBAL(void) jsimd_h2v1_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v1_upsample_neon(cinfo->max_v_samp_factor, cinfo->output_width, + input_data, output_data_ptr); } GLOBAL(int) jsimd_can_h2v2_fancy_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -366,10 +472,30 @@ jsimd_can_h2v1_fancy_upsample(void) return 0; } +GLOBAL(int) +jsimd_can_h1v2_fancy_upsample(void) +{ + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + + return 0; +} + GLOBAL(void) jsimd_h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v2_fancy_upsample_neon(cinfo->max_v_samp_factor, + compptr->downsampled_width, input_data, + output_data_ptr); } GLOBAL(void) @@ -381,15 +507,46 @@ jsimd_h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, output_data_ptr); } +GLOBAL(void) +jsimd_h1v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, + JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) +{ + jsimd_h1v2_fancy_upsample_neon(cinfo->max_v_samp_factor, + compptr->downsampled_width, input_data, + output_data_ptr); +} + GLOBAL(int) jsimd_can_h2v2_merged_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_can_h2v1_merged_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -397,12 +554,74 @@ GLOBAL(void) jsimd_h2v2_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { + void (*neonfct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + + switch (cinfo->out_color_space) { + case JCS_EXT_RGB: + neonfct = jsimd_h2v2_extrgb_merged_upsample_neon; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + neonfct = jsimd_h2v2_extrgbx_merged_upsample_neon; + break; + case JCS_EXT_BGR: + neonfct = jsimd_h2v2_extbgr_merged_upsample_neon; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + neonfct = jsimd_h2v2_extbgrx_merged_upsample_neon; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + neonfct = jsimd_h2v2_extxbgr_merged_upsample_neon; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + neonfct = jsimd_h2v2_extxrgb_merged_upsample_neon; + break; + default: + neonfct = jsimd_h2v2_extrgb_merged_upsample_neon; + break; + } + + neonfct(cinfo->output_width, input_buf, in_row_group_ctr, output_buf); } GLOBAL(void) jsimd_h2v1_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { + void (*neonfct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + + switch (cinfo->out_color_space) { + case JCS_EXT_RGB: + neonfct = jsimd_h2v1_extrgb_merged_upsample_neon; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + neonfct = jsimd_h2v1_extrgbx_merged_upsample_neon; + break; + case JCS_EXT_BGR: + neonfct = jsimd_h2v1_extbgr_merged_upsample_neon; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + neonfct = jsimd_h2v1_extbgrx_merged_upsample_neon; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + neonfct = jsimd_h2v1_extxbgr_merged_upsample_neon; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + neonfct = jsimd_h2v1_extxrgb_merged_upsample_neon; + break; + default: + neonfct = jsimd_h2v1_extrgb_merged_upsample_neon; + break; + } + + neonfct(cinfo->output_width, input_buf, in_row_group_ctr, output_buf); } GLOBAL(int) @@ -448,6 +667,17 @@ jsimd_convsamp_float(JSAMPARRAY sample_data, JDIMENSION start_col, GLOBAL(int) jsimd_can_fdct_islow(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (DCTSIZE != 8) + return 0; + if (sizeof(DCTELEM) != 2) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -477,6 +707,7 @@ jsimd_can_fdct_float(void) GLOBAL(void) jsimd_fdct_islow(DCTELEM *data) { + jsimd_fdct_islow_neon(data); } GLOBAL(void) @@ -696,26 +927,50 @@ jsimd_huff_encode_one_block(void *state, JOCTET *buffer, JCOEFPTR block, GLOBAL(int) jsimd_can_encode_mcu_AC_first_prepare(void) { + init_simd(); + + if (DCTSIZE != 8) + return 0; + if (sizeof(JCOEF) != 2) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { + jsimd_encode_mcu_AC_first_prepare_neon(block, jpeg_natural_order_start, + Sl, Al, values, zerobits); } GLOBAL(int) jsimd_can_encode_mcu_AC_refine_prepare(void) { + init_simd(); + + if (DCTSIZE != 8) + return 0; + if (sizeof(JCOEF) != 2) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { - return 0; + return jsimd_encode_mcu_AC_refine_prepare_neon(block, + jpeg_natural_order_start, Sl, + Al, absvalues, bits); } diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jsimd_neon.S b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jsimd_neon.S new file mode 100644 index 00000000000..7e1e2b14512 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch32/jsimd_neon.S @@ -0,0 +1,1200 @@ +/* + * Armv7 Neon optimizations for libjpeg-turbo + * + * Copyright (C) 2009-2011, Nokia Corporation and/or its subsidiary(-ies). + * All Rights Reserved. + * Author: Siarhei Siamashka + * Copyright (C) 2014, Siarhei Siamashka. All Rights Reserved. + * Copyright (C) 2014, Linaro Limited. All Rights Reserved. + * Copyright (C) 2015, D. R. Commander. All Rights Reserved. + * Copyright (C) 2015-2016, 2018, Matthieu Darbois. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#if defined(__linux__) && defined(__ELF__) +.section .note.GNU-stack, "", %progbits /* mark stack as non-executable */ +#endif + +.text +.fpu neon +.arch armv7a +.object_arch armv4 +.arm +.syntax unified + + +/*****************************************************************************/ + +/* Supplementary macro for setting function attributes */ +.macro asm_function fname +#ifdef __APPLE__ + .private_extern _\fname + .globl _\fname +_\fname: +#else + .global \fname +#ifdef __ELF__ + .hidden \fname + .type \fname, %function +#endif +\fname: +#endif +.endm + + +#define CENTERJSAMPLE 128 + +/*****************************************************************************/ + +/* + * Perform dequantization and inverse DCT on one block of coefficients. + * + * GLOBAL(void) + * jsimd_idct_islow_neon(void *dct_table, JCOEFPTR coef_block, + * JSAMPARRAY output_buf, JDIMENSION output_col) + */ + +#define FIX_0_298631336 (2446) +#define FIX_0_390180644 (3196) +#define FIX_0_541196100 (4433) +#define FIX_0_765366865 (6270) +#define FIX_0_899976223 (7373) +#define FIX_1_175875602 (9633) +#define FIX_1_501321110 (12299) +#define FIX_1_847759065 (15137) +#define FIX_1_961570560 (16069) +#define FIX_2_053119869 (16819) +#define FIX_2_562915447 (20995) +#define FIX_3_072711026 (25172) + +#define FIX_1_175875602_MINUS_1_961570560 (FIX_1_175875602 - FIX_1_961570560) +#define FIX_1_175875602_MINUS_0_390180644 (FIX_1_175875602 - FIX_0_390180644) +#define FIX_0_541196100_MINUS_1_847759065 (FIX_0_541196100 - FIX_1_847759065) +#define FIX_3_072711026_MINUS_2_562915447 (FIX_3_072711026 - FIX_2_562915447) +#define FIX_0_298631336_MINUS_0_899976223 (FIX_0_298631336 - FIX_0_899976223) +#define FIX_1_501321110_MINUS_0_899976223 (FIX_1_501321110 - FIX_0_899976223) +#define FIX_2_053119869_MINUS_2_562915447 (FIX_2_053119869 - FIX_2_562915447) +#define FIX_0_541196100_PLUS_0_765366865 (FIX_0_541196100 + FIX_0_765366865) + +/* + * Reference SIMD-friendly 1-D ISLOW iDCT C implementation. + * Uses some ideas from the comments in 'simd/jiss2int-64.asm' + */ +#define REF_1D_IDCT(xrow0, xrow1, xrow2, xrow3, xrow4, xrow5, xrow6, xrow7) { \ + DCTELEM row0, row1, row2, row3, row4, row5, row6, row7; \ + JLONG q1, q2, q3, q4, q5, q6, q7; \ + JLONG tmp11_plus_tmp2, tmp11_minus_tmp2; \ + \ + /* 1-D iDCT input data */ \ + row0 = xrow0; \ + row1 = xrow1; \ + row2 = xrow2; \ + row3 = xrow3; \ + row4 = xrow4; \ + row5 = xrow5; \ + row6 = xrow6; \ + row7 = xrow7; \ + \ + q5 = row7 + row3; \ + q4 = row5 + row1; \ + q6 = MULTIPLY(q5, FIX_1_175875602_MINUS_1_961570560) + \ + MULTIPLY(q4, FIX_1_175875602); \ + q7 = MULTIPLY(q5, FIX_1_175875602) + \ + MULTIPLY(q4, FIX_1_175875602_MINUS_0_390180644); \ + q2 = MULTIPLY(row2, FIX_0_541196100) + \ + MULTIPLY(row6, FIX_0_541196100_MINUS_1_847759065); \ + q4 = q6; \ + q3 = ((JLONG)row0 - (JLONG)row4) << 13; \ + q6 += MULTIPLY(row5, -FIX_2_562915447) + \ + MULTIPLY(row3, FIX_3_072711026_MINUS_2_562915447); \ + /* now we can use q1 (reloadable constants have been used up) */ \ + q1 = q3 + q2; \ + q4 += MULTIPLY(row7, FIX_0_298631336_MINUS_0_899976223) + \ + MULTIPLY(row1, -FIX_0_899976223); \ + q5 = q7; \ + q1 = q1 + q6; \ + q7 += MULTIPLY(row7, -FIX_0_899976223) + \ + MULTIPLY(row1, FIX_1_501321110_MINUS_0_899976223); \ + \ + /* (tmp11 + tmp2) has been calculated (out_row1 before descale) */ \ + tmp11_plus_tmp2 = q1; \ + row1 = 0; \ + \ + q1 = q1 - q6; \ + q5 += MULTIPLY(row5, FIX_2_053119869_MINUS_2_562915447) + \ + MULTIPLY(row3, -FIX_2_562915447); \ + q1 = q1 - q6; \ + q6 = MULTIPLY(row2, FIX_0_541196100_PLUS_0_765366865) + \ + MULTIPLY(row6, FIX_0_541196100); \ + q3 = q3 - q2; \ + \ + /* (tmp11 - tmp2) has been calculated (out_row6 before descale) */ \ + tmp11_minus_tmp2 = q1; \ + \ + q1 = ((JLONG)row0 + (JLONG)row4) << 13; \ + q2 = q1 + q6; \ + q1 = q1 - q6; \ + \ + /* pick up the results */ \ + tmp0 = q4; \ + tmp1 = q5; \ + tmp2 = (tmp11_plus_tmp2 - tmp11_minus_tmp2) / 2; \ + tmp3 = q7; \ + tmp10 = q2; \ + tmp11 = (tmp11_plus_tmp2 + tmp11_minus_tmp2) / 2; \ + tmp12 = q3; \ + tmp13 = q1; \ +} + +#define XFIX_0_899976223 d0[0] +#define XFIX_0_541196100 d0[1] +#define XFIX_2_562915447 d0[2] +#define XFIX_0_298631336_MINUS_0_899976223 d0[3] +#define XFIX_1_501321110_MINUS_0_899976223 d1[0] +#define XFIX_2_053119869_MINUS_2_562915447 d1[1] +#define XFIX_0_541196100_PLUS_0_765366865 d1[2] +#define XFIX_1_175875602 d1[3] +#define XFIX_1_175875602_MINUS_0_390180644 d2[0] +#define XFIX_0_541196100_MINUS_1_847759065 d2[1] +#define XFIX_3_072711026_MINUS_2_562915447 d2[2] +#define XFIX_1_175875602_MINUS_1_961570560 d2[3] + +.balign 16 +jsimd_idct_islow_neon_consts: + .short FIX_0_899976223 /* d0[0] */ + .short FIX_0_541196100 /* d0[1] */ + .short FIX_2_562915447 /* d0[2] */ + .short FIX_0_298631336_MINUS_0_899976223 /* d0[3] */ + .short FIX_1_501321110_MINUS_0_899976223 /* d1[0] */ + .short FIX_2_053119869_MINUS_2_562915447 /* d1[1] */ + .short FIX_0_541196100_PLUS_0_765366865 /* d1[2] */ + .short FIX_1_175875602 /* d1[3] */ + /* reloadable constants */ + .short FIX_1_175875602_MINUS_0_390180644 /* d2[0] */ + .short FIX_0_541196100_MINUS_1_847759065 /* d2[1] */ + .short FIX_3_072711026_MINUS_2_562915447 /* d2[2] */ + .short FIX_1_175875602_MINUS_1_961570560 /* d2[3] */ + +asm_function jsimd_idct_islow_neon + + DCT_TABLE .req r0 + COEF_BLOCK .req r1 + OUTPUT_BUF .req r2 + OUTPUT_COL .req r3 + TMP1 .req r0 + TMP2 .req r1 + TMP3 .req r2 + TMP4 .req ip + + ROW0L .req d16 + ROW0R .req d17 + ROW1L .req d18 + ROW1R .req d19 + ROW2L .req d20 + ROW2R .req d21 + ROW3L .req d22 + ROW3R .req d23 + ROW4L .req d24 + ROW4R .req d25 + ROW5L .req d26 + ROW5R .req d27 + ROW6L .req d28 + ROW6R .req d29 + ROW7L .req d30 + ROW7R .req d31 + + /* Load and dequantize coefficients into Neon registers + * with the following allocation: + * 0 1 2 3 | 4 5 6 7 + * ---------+-------- + * 0 | d16 | d17 ( q8 ) + * 1 | d18 | d19 ( q9 ) + * 2 | d20 | d21 ( q10 ) + * 3 | d22 | d23 ( q11 ) + * 4 | d24 | d25 ( q12 ) + * 5 | d26 | d27 ( q13 ) + * 6 | d28 | d29 ( q14 ) + * 7 | d30 | d31 ( q15 ) + */ + adr ip, jsimd_idct_islow_neon_consts + vld1.16 {d16, d17, d18, d19}, [COEF_BLOCK, :128]! + vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! + vld1.16 {d20, d21, d22, d23}, [COEF_BLOCK, :128]! + vmul.s16 q8, q8, q0 + vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! + vmul.s16 q9, q9, q1 + vld1.16 {d24, d25, d26, d27}, [COEF_BLOCK, :128]! + vmul.s16 q10, q10, q2 + vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! + vmul.s16 q11, q11, q3 + vld1.16 {d28, d29, d30, d31}, [COEF_BLOCK, :128] + vmul.s16 q12, q12, q0 + vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! + vmul.s16 q14, q14, q2 + vmul.s16 q13, q13, q1 + vld1.16 {d0, d1, d2, d3}, [ip, :128] /* load constants */ + add ip, ip, #16 + vmul.s16 q15, q15, q3 + vpush {d8 - d15} /* save Neon registers */ + /* 1-D IDCT, pass 1, left 4x8 half */ + vadd.s16 d4, ROW7L, ROW3L + vadd.s16 d5, ROW5L, ROW1L + vmull.s16 q6, d4, XFIX_1_175875602_MINUS_1_961570560 + vmlal.s16 q6, d5, XFIX_1_175875602 + vmull.s16 q7, d4, XFIX_1_175875602 + /* Check for the zero coefficients in the right 4x8 half */ + push {r4, r5} + vmlal.s16 q7, d5, XFIX_1_175875602_MINUS_0_390180644 + vsubl.s16 q3, ROW0L, ROW4L + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 1 * 8))] + vmull.s16 q2, ROW2L, XFIX_0_541196100 + vmlal.s16 q2, ROW6L, XFIX_0_541196100_MINUS_1_847759065 + orr r0, r4, r5 + vmov q4, q6 + vmlsl.s16 q6, ROW5L, XFIX_2_562915447 + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 2 * 8))] + vmlal.s16 q6, ROW3L, XFIX_3_072711026_MINUS_2_562915447 + vshl.s32 q3, q3, #13 + orr r0, r0, r4 + vmlsl.s16 q4, ROW1L, XFIX_0_899976223 + orr r0, r0, r5 + vadd.s32 q1, q3, q2 + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 3 * 8))] + vmov q5, q7 + vadd.s32 q1, q1, q6 + orr r0, r0, r4 + vmlsl.s16 q7, ROW7L, XFIX_0_899976223 + orr r0, r0, r5 + vmlal.s16 q7, ROW1L, XFIX_1_501321110_MINUS_0_899976223 + vrshrn.s32 ROW1L, q1, #11 + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 4 * 8))] + vsub.s32 q1, q1, q6 + vmlal.s16 q5, ROW5L, XFIX_2_053119869_MINUS_2_562915447 + orr r0, r0, r4 + vmlsl.s16 q5, ROW3L, XFIX_2_562915447 + orr r0, r0, r5 + vsub.s32 q1, q1, q6 + vmull.s16 q6, ROW2L, XFIX_0_541196100_PLUS_0_765366865 + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 5 * 8))] + vmlal.s16 q6, ROW6L, XFIX_0_541196100 + vsub.s32 q3, q3, q2 + orr r0, r0, r4 + vrshrn.s32 ROW6L, q1, #11 + orr r0, r0, r5 + vadd.s32 q1, q3, q5 + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 6 * 8))] + vsub.s32 q3, q3, q5 + vaddl.s16 q5, ROW0L, ROW4L + orr r0, r0, r4 + vrshrn.s32 ROW2L, q1, #11 + orr r0, r0, r5 + vrshrn.s32 ROW5L, q3, #11 + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 7 * 8))] + vshl.s32 q5, q5, #13 + vmlal.s16 q4, ROW7L, XFIX_0_298631336_MINUS_0_899976223 + orr r0, r0, r4 + vadd.s32 q2, q5, q6 + orrs r0, r0, r5 + vsub.s32 q1, q5, q6 + vadd.s32 q6, q2, q7 + ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 0 * 8))] + vsub.s32 q2, q2, q7 + vadd.s32 q5, q1, q4 + orr r0, r4, r5 + vsub.s32 q3, q1, q4 + pop {r4, r5} + vrshrn.s32 ROW7L, q2, #11 + vrshrn.s32 ROW3L, q5, #11 + vrshrn.s32 ROW0L, q6, #11 + vrshrn.s32 ROW4L, q3, #11 + + beq 3f /* Go to do some special handling for the sparse + right 4x8 half */ + + /* 1-D IDCT, pass 1, right 4x8 half */ + vld1.s16 {d2}, [ip, :64] /* reload constants */ + vadd.s16 d10, ROW7R, ROW3R + vadd.s16 d8, ROW5R, ROW1R + /* Transpose left 4x8 half */ + vtrn.16 ROW6L, ROW7L + vmull.s16 q6, d10, XFIX_1_175875602_MINUS_1_961570560 + vmlal.s16 q6, d8, XFIX_1_175875602 + vtrn.16 ROW2L, ROW3L + vmull.s16 q7, d10, XFIX_1_175875602 + vmlal.s16 q7, d8, XFIX_1_175875602_MINUS_0_390180644 + vtrn.16 ROW0L, ROW1L + vsubl.s16 q3, ROW0R, ROW4R + vmull.s16 q2, ROW2R, XFIX_0_541196100 + vmlal.s16 q2, ROW6R, XFIX_0_541196100_MINUS_1_847759065 + vtrn.16 ROW4L, ROW5L + vmov q4, q6 + vmlsl.s16 q6, ROW5R, XFIX_2_562915447 + vmlal.s16 q6, ROW3R, XFIX_3_072711026_MINUS_2_562915447 + vtrn.32 ROW1L, ROW3L + vshl.s32 q3, q3, #13 + vmlsl.s16 q4, ROW1R, XFIX_0_899976223 + vtrn.32 ROW4L, ROW6L + vadd.s32 q1, q3, q2 + vmov q5, q7 + vadd.s32 q1, q1, q6 + vtrn.32 ROW0L, ROW2L + vmlsl.s16 q7, ROW7R, XFIX_0_899976223 + vmlal.s16 q7, ROW1R, XFIX_1_501321110_MINUS_0_899976223 + vrshrn.s32 ROW1R, q1, #11 + vtrn.32 ROW5L, ROW7L + vsub.s32 q1, q1, q6 + vmlal.s16 q5, ROW5R, XFIX_2_053119869_MINUS_2_562915447 + vmlsl.s16 q5, ROW3R, XFIX_2_562915447 + vsub.s32 q1, q1, q6 + vmull.s16 q6, ROW2R, XFIX_0_541196100_PLUS_0_765366865 + vmlal.s16 q6, ROW6R, XFIX_0_541196100 + vsub.s32 q3, q3, q2 + vrshrn.s32 ROW6R, q1, #11 + vadd.s32 q1, q3, q5 + vsub.s32 q3, q3, q5 + vaddl.s16 q5, ROW0R, ROW4R + vrshrn.s32 ROW2R, q1, #11 + vrshrn.s32 ROW5R, q3, #11 + vshl.s32 q5, q5, #13 + vmlal.s16 q4, ROW7R, XFIX_0_298631336_MINUS_0_899976223 + vadd.s32 q2, q5, q6 + vsub.s32 q1, q5, q6 + vadd.s32 q6, q2, q7 + vsub.s32 q2, q2, q7 + vadd.s32 q5, q1, q4 + vsub.s32 q3, q1, q4 + vrshrn.s32 ROW7R, q2, #11 + vrshrn.s32 ROW3R, q5, #11 + vrshrn.s32 ROW0R, q6, #11 + vrshrn.s32 ROW4R, q3, #11 + /* Transpose right 4x8 half */ + vtrn.16 ROW6R, ROW7R + vtrn.16 ROW2R, ROW3R + vtrn.16 ROW0R, ROW1R + vtrn.16 ROW4R, ROW5R + vtrn.32 ROW1R, ROW3R + vtrn.32 ROW4R, ROW6R + vtrn.32 ROW0R, ROW2R + vtrn.32 ROW5R, ROW7R + +1: /* 1-D IDCT, pass 2 (normal variant), left 4x8 half */ + vld1.s16 {d2}, [ip, :64] /* reload constants */ + vmull.s16 q6, ROW1R, XFIX_1_175875602 /* ROW5L <-> ROW1R */ + vmlal.s16 q6, ROW1L, XFIX_1_175875602 + vmlal.s16 q6, ROW3R, XFIX_1_175875602_MINUS_1_961570560 /* ROW7L <-> ROW3R */ + vmlal.s16 q6, ROW3L, XFIX_1_175875602_MINUS_1_961570560 + vmull.s16 q7, ROW3R, XFIX_1_175875602 /* ROW7L <-> ROW3R */ + vmlal.s16 q7, ROW3L, XFIX_1_175875602 + vmlal.s16 q7, ROW1R, XFIX_1_175875602_MINUS_0_390180644 /* ROW5L <-> ROW1R */ + vmlal.s16 q7, ROW1L, XFIX_1_175875602_MINUS_0_390180644 + vsubl.s16 q3, ROW0L, ROW0R /* ROW4L <-> ROW0R */ + vmull.s16 q2, ROW2L, XFIX_0_541196100 + vmlal.s16 q2, ROW2R, XFIX_0_541196100_MINUS_1_847759065 /* ROW6L <-> ROW2R */ + vmov q4, q6 + vmlsl.s16 q6, ROW1R, XFIX_2_562915447 /* ROW5L <-> ROW1R */ + vmlal.s16 q6, ROW3L, XFIX_3_072711026_MINUS_2_562915447 + vshl.s32 q3, q3, #13 + vmlsl.s16 q4, ROW1L, XFIX_0_899976223 + vadd.s32 q1, q3, q2 + vmov q5, q7 + vadd.s32 q1, q1, q6 + vmlsl.s16 q7, ROW3R, XFIX_0_899976223 /* ROW7L <-> ROW3R */ + vmlal.s16 q7, ROW1L, XFIX_1_501321110_MINUS_0_899976223 + vshrn.s32 ROW1L, q1, #16 + vsub.s32 q1, q1, q6 + vmlal.s16 q5, ROW1R, XFIX_2_053119869_MINUS_2_562915447 /* ROW5L <-> ROW1R */ + vmlsl.s16 q5, ROW3L, XFIX_2_562915447 + vsub.s32 q1, q1, q6 + vmull.s16 q6, ROW2L, XFIX_0_541196100_PLUS_0_765366865 + vmlal.s16 q6, ROW2R, XFIX_0_541196100 /* ROW6L <-> ROW2R */ + vsub.s32 q3, q3, q2 + vshrn.s32 ROW2R, q1, #16 /* ROW6L <-> ROW2R */ + vadd.s32 q1, q3, q5 + vsub.s32 q3, q3, q5 + vaddl.s16 q5, ROW0L, ROW0R /* ROW4L <-> ROW0R */ + vshrn.s32 ROW2L, q1, #16 + vshrn.s32 ROW1R, q3, #16 /* ROW5L <-> ROW1R */ + vshl.s32 q5, q5, #13 + vmlal.s16 q4, ROW3R, XFIX_0_298631336_MINUS_0_899976223 /* ROW7L <-> ROW3R */ + vadd.s32 q2, q5, q6 + vsub.s32 q1, q5, q6 + vadd.s32 q6, q2, q7 + vsub.s32 q2, q2, q7 + vadd.s32 q5, q1, q4 + vsub.s32 q3, q1, q4 + vshrn.s32 ROW3R, q2, #16 /* ROW7L <-> ROW3R */ + vshrn.s32 ROW3L, q5, #16 + vshrn.s32 ROW0L, q6, #16 + vshrn.s32 ROW0R, q3, #16 /* ROW4L <-> ROW0R */ + /* 1-D IDCT, pass 2, right 4x8 half */ + vld1.s16 {d2}, [ip, :64] /* reload constants */ + vmull.s16 q6, ROW5R, XFIX_1_175875602 + vmlal.s16 q6, ROW5L, XFIX_1_175875602 /* ROW5L <-> ROW1R */ + vmlal.s16 q6, ROW7R, XFIX_1_175875602_MINUS_1_961570560 + vmlal.s16 q6, ROW7L, XFIX_1_175875602_MINUS_1_961570560 /* ROW7L <-> ROW3R */ + vmull.s16 q7, ROW7R, XFIX_1_175875602 + vmlal.s16 q7, ROW7L, XFIX_1_175875602 /* ROW7L <-> ROW3R */ + vmlal.s16 q7, ROW5R, XFIX_1_175875602_MINUS_0_390180644 + vmlal.s16 q7, ROW5L, XFIX_1_175875602_MINUS_0_390180644 /* ROW5L <-> ROW1R */ + vsubl.s16 q3, ROW4L, ROW4R /* ROW4L <-> ROW0R */ + vmull.s16 q2, ROW6L, XFIX_0_541196100 /* ROW6L <-> ROW2R */ + vmlal.s16 q2, ROW6R, XFIX_0_541196100_MINUS_1_847759065 + vmov q4, q6 + vmlsl.s16 q6, ROW5R, XFIX_2_562915447 + vmlal.s16 q6, ROW7L, XFIX_3_072711026_MINUS_2_562915447 /* ROW7L <-> ROW3R */ + vshl.s32 q3, q3, #13 + vmlsl.s16 q4, ROW5L, XFIX_0_899976223 /* ROW5L <-> ROW1R */ + vadd.s32 q1, q3, q2 + vmov q5, q7 + vadd.s32 q1, q1, q6 + vmlsl.s16 q7, ROW7R, XFIX_0_899976223 + vmlal.s16 q7, ROW5L, XFIX_1_501321110_MINUS_0_899976223 /* ROW5L <-> ROW1R */ + vshrn.s32 ROW5L, q1, #16 /* ROW5L <-> ROW1R */ + vsub.s32 q1, q1, q6 + vmlal.s16 q5, ROW5R, XFIX_2_053119869_MINUS_2_562915447 + vmlsl.s16 q5, ROW7L, XFIX_2_562915447 /* ROW7L <-> ROW3R */ + vsub.s32 q1, q1, q6 + vmull.s16 q6, ROW6L, XFIX_0_541196100_PLUS_0_765366865 /* ROW6L <-> ROW2R */ + vmlal.s16 q6, ROW6R, XFIX_0_541196100 + vsub.s32 q3, q3, q2 + vshrn.s32 ROW6R, q1, #16 + vadd.s32 q1, q3, q5 + vsub.s32 q3, q3, q5 + vaddl.s16 q5, ROW4L, ROW4R /* ROW4L <-> ROW0R */ + vshrn.s32 ROW6L, q1, #16 /* ROW6L <-> ROW2R */ + vshrn.s32 ROW5R, q3, #16 + vshl.s32 q5, q5, #13 + vmlal.s16 q4, ROW7R, XFIX_0_298631336_MINUS_0_899976223 + vadd.s32 q2, q5, q6 + vsub.s32 q1, q5, q6 + vadd.s32 q6, q2, q7 + vsub.s32 q2, q2, q7 + vadd.s32 q5, q1, q4 + vsub.s32 q3, q1, q4 + vshrn.s32 ROW7R, q2, #16 + vshrn.s32 ROW7L, q5, #16 /* ROW7L <-> ROW3R */ + vshrn.s32 ROW4L, q6, #16 /* ROW4L <-> ROW0R */ + vshrn.s32 ROW4R, q3, #16 + +2: /* Descale to 8-bit and range limit */ + vqrshrn.s16 d16, q8, #2 + vqrshrn.s16 d17, q9, #2 + vqrshrn.s16 d18, q10, #2 + vqrshrn.s16 d19, q11, #2 + vpop {d8 - d15} /* restore Neon registers */ + vqrshrn.s16 d20, q12, #2 + /* Transpose the final 8-bit samples and do signed->unsigned conversion */ + vtrn.16 q8, q9 + vqrshrn.s16 d21, q13, #2 + vqrshrn.s16 d22, q14, #2 + vmov.u8 q0, #(CENTERJSAMPLE) + vqrshrn.s16 d23, q15, #2 + vtrn.8 d16, d17 + vtrn.8 d18, d19 + vadd.u8 q8, q8, q0 + vadd.u8 q9, q9, q0 + vtrn.16 q10, q11 + /* Store results to the output buffer */ + ldmia OUTPUT_BUF!, {TMP1, TMP2} + add TMP1, TMP1, OUTPUT_COL + add TMP2, TMP2, OUTPUT_COL + vst1.8 {d16}, [TMP1] + vtrn.8 d20, d21 + vst1.8 {d17}, [TMP2] + ldmia OUTPUT_BUF!, {TMP1, TMP2} + add TMP1, TMP1, OUTPUT_COL + add TMP2, TMP2, OUTPUT_COL + vst1.8 {d18}, [TMP1] + vadd.u8 q10, q10, q0 + vst1.8 {d19}, [TMP2] + ldmia OUTPUT_BUF, {TMP1, TMP2, TMP3, TMP4} + add TMP1, TMP1, OUTPUT_COL + add TMP2, TMP2, OUTPUT_COL + add TMP3, TMP3, OUTPUT_COL + add TMP4, TMP4, OUTPUT_COL + vtrn.8 d22, d23 + vst1.8 {d20}, [TMP1] + vadd.u8 q11, q11, q0 + vst1.8 {d21}, [TMP2] + vst1.8 {d22}, [TMP3] + vst1.8 {d23}, [TMP4] + bx lr + +3: /* Left 4x8 half is done, right 4x8 half contains mostly zeros */ + + /* Transpose left 4x8 half */ + vtrn.16 ROW6L, ROW7L + vtrn.16 ROW2L, ROW3L + vtrn.16 ROW0L, ROW1L + vtrn.16 ROW4L, ROW5L + vshl.s16 ROW0R, ROW0R, #2 /* PASS1_BITS */ + vtrn.32 ROW1L, ROW3L + vtrn.32 ROW4L, ROW6L + vtrn.32 ROW0L, ROW2L + vtrn.32 ROW5L, ROW7L + + cmp r0, #0 + beq 4f /* Right 4x8 half has all zeros, go to 'sparse' second + pass */ + + /* Only row 0 is non-zero for the right 4x8 half */ + vdup.s16 ROW1R, ROW0R[1] + vdup.s16 ROW2R, ROW0R[2] + vdup.s16 ROW3R, ROW0R[3] + vdup.s16 ROW4R, ROW0R[0] + vdup.s16 ROW5R, ROW0R[1] + vdup.s16 ROW6R, ROW0R[2] + vdup.s16 ROW7R, ROW0R[3] + vdup.s16 ROW0R, ROW0R[0] + b 1b /* Go to 'normal' second pass */ + +4: /* 1-D IDCT, pass 2 (sparse variant with zero rows 4-7), left 4x8 half */ + vld1.s16 {d2}, [ip, :64] /* reload constants */ + vmull.s16 q6, ROW1L, XFIX_1_175875602 + vmlal.s16 q6, ROW3L, XFIX_1_175875602_MINUS_1_961570560 + vmull.s16 q7, ROW3L, XFIX_1_175875602 + vmlal.s16 q7, ROW1L, XFIX_1_175875602_MINUS_0_390180644 + vmull.s16 q2, ROW2L, XFIX_0_541196100 + vshll.s16 q3, ROW0L, #13 + vmov q4, q6 + vmlal.s16 q6, ROW3L, XFIX_3_072711026_MINUS_2_562915447 + vmlsl.s16 q4, ROW1L, XFIX_0_899976223 + vadd.s32 q1, q3, q2 + vmov q5, q7 + vmlal.s16 q7, ROW1L, XFIX_1_501321110_MINUS_0_899976223 + vadd.s32 q1, q1, q6 + vadd.s32 q6, q6, q6 + vmlsl.s16 q5, ROW3L, XFIX_2_562915447 + vshrn.s32 ROW1L, q1, #16 + vsub.s32 q1, q1, q6 + vmull.s16 q6, ROW2L, XFIX_0_541196100_PLUS_0_765366865 + vsub.s32 q3, q3, q2 + vshrn.s32 ROW2R, q1, #16 /* ROW6L <-> ROW2R */ + vadd.s32 q1, q3, q5 + vsub.s32 q3, q3, q5 + vshll.s16 q5, ROW0L, #13 + vshrn.s32 ROW2L, q1, #16 + vshrn.s32 ROW1R, q3, #16 /* ROW5L <-> ROW1R */ + vadd.s32 q2, q5, q6 + vsub.s32 q1, q5, q6 + vadd.s32 q6, q2, q7 + vsub.s32 q2, q2, q7 + vadd.s32 q5, q1, q4 + vsub.s32 q3, q1, q4 + vshrn.s32 ROW3R, q2, #16 /* ROW7L <-> ROW3R */ + vshrn.s32 ROW3L, q5, #16 + vshrn.s32 ROW0L, q6, #16 + vshrn.s32 ROW0R, q3, #16 /* ROW4L <-> ROW0R */ + /* 1-D IDCT, pass 2 (sparse variant with zero rows 4-7), right 4x8 half */ + vld1.s16 {d2}, [ip, :64] /* reload constants */ + vmull.s16 q6, ROW5L, XFIX_1_175875602 + vmlal.s16 q6, ROW7L, XFIX_1_175875602_MINUS_1_961570560 + vmull.s16 q7, ROW7L, XFIX_1_175875602 + vmlal.s16 q7, ROW5L, XFIX_1_175875602_MINUS_0_390180644 + vmull.s16 q2, ROW6L, XFIX_0_541196100 + vshll.s16 q3, ROW4L, #13 + vmov q4, q6 + vmlal.s16 q6, ROW7L, XFIX_3_072711026_MINUS_2_562915447 + vmlsl.s16 q4, ROW5L, XFIX_0_899976223 + vadd.s32 q1, q3, q2 + vmov q5, q7 + vmlal.s16 q7, ROW5L, XFIX_1_501321110_MINUS_0_899976223 + vadd.s32 q1, q1, q6 + vadd.s32 q6, q6, q6 + vmlsl.s16 q5, ROW7L, XFIX_2_562915447 + vshrn.s32 ROW5L, q1, #16 /* ROW5L <-> ROW1R */ + vsub.s32 q1, q1, q6 + vmull.s16 q6, ROW6L, XFIX_0_541196100_PLUS_0_765366865 + vsub.s32 q3, q3, q2 + vshrn.s32 ROW6R, q1, #16 + vadd.s32 q1, q3, q5 + vsub.s32 q3, q3, q5 + vshll.s16 q5, ROW4L, #13 + vshrn.s32 ROW6L, q1, #16 /* ROW6L <-> ROW2R */ + vshrn.s32 ROW5R, q3, #16 + vadd.s32 q2, q5, q6 + vsub.s32 q1, q5, q6 + vadd.s32 q6, q2, q7 + vsub.s32 q2, q2, q7 + vadd.s32 q5, q1, q4 + vsub.s32 q3, q1, q4 + vshrn.s32 ROW7R, q2, #16 + vshrn.s32 ROW7L, q5, #16 /* ROW7L <-> ROW3R */ + vshrn.s32 ROW4L, q6, #16 /* ROW4L <-> ROW0R */ + vshrn.s32 ROW4R, q3, #16 + b 2b /* Go to epilogue */ + + .unreq DCT_TABLE + .unreq COEF_BLOCK + .unreq OUTPUT_BUF + .unreq OUTPUT_COL + .unreq TMP1 + .unreq TMP2 + .unreq TMP3 + .unreq TMP4 + + .unreq ROW0L + .unreq ROW0R + .unreq ROW1L + .unreq ROW1R + .unreq ROW2L + .unreq ROW2R + .unreq ROW3L + .unreq ROW3R + .unreq ROW4L + .unreq ROW4R + .unreq ROW5L + .unreq ROW5R + .unreq ROW6L + .unreq ROW6R + .unreq ROW7L + .unreq ROW7R + + +/*****************************************************************************/ + +/* + * jsimd_idct_ifast_neon + * + * This function contains a fast, not so accurate integer implementation of + * the inverse DCT (Discrete Cosine Transform). It uses the same calculations + * and produces exactly the same output as IJG's original 'jpeg_idct_ifast' + * function from jidctfst.c + * + * Normally 1-D AAN DCT needs 5 multiplications and 29 additions. + * But in Arm Neon case some extra additions are required because VQDMULH + * instruction can't handle the constants larger than 1. So the expressions + * like "x * 1.082392200" have to be converted to "x * 0.082392200 + x", + * which introduces an extra addition. Overall, there are 6 extra additions + * per 1-D IDCT pass, totalling to 5 VQDMULH and 35 VADD/VSUB instructions. + */ + +#define XFIX_1_082392200 d0[0] +#define XFIX_1_414213562 d0[1] +#define XFIX_1_847759065 d0[2] +#define XFIX_2_613125930 d0[3] + +.balign 16 +jsimd_idct_ifast_neon_consts: + .short (277 * 128 - 256 * 128) /* XFIX_1_082392200 */ + .short (362 * 128 - 256 * 128) /* XFIX_1_414213562 */ + .short (473 * 128 - 256 * 128) /* XFIX_1_847759065 */ + .short (669 * 128 - 512 * 128) /* XFIX_2_613125930 */ + +asm_function jsimd_idct_ifast_neon + + DCT_TABLE .req r0 + COEF_BLOCK .req r1 + OUTPUT_BUF .req r2 + OUTPUT_COL .req r3 + TMP1 .req r0 + TMP2 .req r1 + TMP3 .req r2 + TMP4 .req ip + + /* Load and dequantize coefficients into Neon registers + * with the following allocation: + * 0 1 2 3 | 4 5 6 7 + * ---------+-------- + * 0 | d16 | d17 ( q8 ) + * 1 | d18 | d19 ( q9 ) + * 2 | d20 | d21 ( q10 ) + * 3 | d22 | d23 ( q11 ) + * 4 | d24 | d25 ( q12 ) + * 5 | d26 | d27 ( q13 ) + * 6 | d28 | d29 ( q14 ) + * 7 | d30 | d31 ( q15 ) + */ + adr ip, jsimd_idct_ifast_neon_consts + vld1.16 {d16, d17, d18, d19}, [COEF_BLOCK, :128]! + vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! + vld1.16 {d20, d21, d22, d23}, [COEF_BLOCK, :128]! + vmul.s16 q8, q8, q0 + vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! + vmul.s16 q9, q9, q1 + vld1.16 {d24, d25, d26, d27}, [COEF_BLOCK, :128]! + vmul.s16 q10, q10, q2 + vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! + vmul.s16 q11, q11, q3 + vld1.16 {d28, d29, d30, d31}, [COEF_BLOCK, :128] + vmul.s16 q12, q12, q0 + vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! + vmul.s16 q14, q14, q2 + vmul.s16 q13, q13, q1 + vld1.16 {d0}, [ip, :64] /* load constants */ + vmul.s16 q15, q15, q3 + vpush {d8 - d13} /* save Neon registers */ + /* 1-D IDCT, pass 1 */ + vsub.s16 q2, q10, q14 + vadd.s16 q14, q10, q14 + vsub.s16 q1, q11, q13 + vadd.s16 q13, q11, q13 + vsub.s16 q5, q9, q15 + vadd.s16 q15, q9, q15 + vqdmulh.s16 q4, q2, XFIX_1_414213562 + vqdmulh.s16 q6, q1, XFIX_2_613125930 + vadd.s16 q3, q1, q1 + vsub.s16 q1, q5, q1 + vadd.s16 q10, q2, q4 + vqdmulh.s16 q4, q1, XFIX_1_847759065 + vsub.s16 q2, q15, q13 + vadd.s16 q3, q3, q6 + vqdmulh.s16 q6, q2, XFIX_1_414213562 + vadd.s16 q1, q1, q4 + vqdmulh.s16 q4, q5, XFIX_1_082392200 + vsub.s16 q10, q10, q14 + vadd.s16 q2, q2, q6 + vsub.s16 q6, q8, q12 + vadd.s16 q12, q8, q12 + vadd.s16 q9, q5, q4 + vadd.s16 q5, q6, q10 + vsub.s16 q10, q6, q10 + vadd.s16 q6, q15, q13 + vadd.s16 q8, q12, q14 + vsub.s16 q3, q6, q3 + vsub.s16 q12, q12, q14 + vsub.s16 q3, q3, q1 + vsub.s16 q1, q9, q1 + vadd.s16 q2, q3, q2 + vsub.s16 q15, q8, q6 + vadd.s16 q1, q1, q2 + vadd.s16 q8, q8, q6 + vadd.s16 q14, q5, q3 + vsub.s16 q9, q5, q3 + vsub.s16 q13, q10, q2 + vadd.s16 q10, q10, q2 + /* Transpose */ + vtrn.16 q8, q9 + vsub.s16 q11, q12, q1 + vtrn.16 q14, q15 + vadd.s16 q12, q12, q1 + vtrn.16 q10, q11 + vtrn.16 q12, q13 + vtrn.32 q9, q11 + vtrn.32 q12, q14 + vtrn.32 q8, q10 + vtrn.32 q13, q15 + vswp d28, d21 + vswp d26, d19 + /* 1-D IDCT, pass 2 */ + vsub.s16 q2, q10, q14 + vswp d30, d23 + vadd.s16 q14, q10, q14 + vswp d24, d17 + vsub.s16 q1, q11, q13 + vadd.s16 q13, q11, q13 + vsub.s16 q5, q9, q15 + vadd.s16 q15, q9, q15 + vqdmulh.s16 q4, q2, XFIX_1_414213562 + vqdmulh.s16 q6, q1, XFIX_2_613125930 + vadd.s16 q3, q1, q1 + vsub.s16 q1, q5, q1 + vadd.s16 q10, q2, q4 + vqdmulh.s16 q4, q1, XFIX_1_847759065 + vsub.s16 q2, q15, q13 + vadd.s16 q3, q3, q6 + vqdmulh.s16 q6, q2, XFIX_1_414213562 + vadd.s16 q1, q1, q4 + vqdmulh.s16 q4, q5, XFIX_1_082392200 + vsub.s16 q10, q10, q14 + vadd.s16 q2, q2, q6 + vsub.s16 q6, q8, q12 + vadd.s16 q12, q8, q12 + vadd.s16 q9, q5, q4 + vadd.s16 q5, q6, q10 + vsub.s16 q10, q6, q10 + vadd.s16 q6, q15, q13 + vadd.s16 q8, q12, q14 + vsub.s16 q3, q6, q3 + vsub.s16 q12, q12, q14 + vsub.s16 q3, q3, q1 + vsub.s16 q1, q9, q1 + vadd.s16 q2, q3, q2 + vsub.s16 q15, q8, q6 + vadd.s16 q1, q1, q2 + vadd.s16 q8, q8, q6 + vadd.s16 q14, q5, q3 + vsub.s16 q9, q5, q3 + vsub.s16 q13, q10, q2 + vpop {d8 - d13} /* restore Neon registers */ + vadd.s16 q10, q10, q2 + vsub.s16 q11, q12, q1 + vadd.s16 q12, q12, q1 + /* Descale to 8-bit and range limit */ + vmov.u8 q0, #0x80 + vqshrn.s16 d16, q8, #5 + vqshrn.s16 d17, q9, #5 + vqshrn.s16 d18, q10, #5 + vqshrn.s16 d19, q11, #5 + vqshrn.s16 d20, q12, #5 + vqshrn.s16 d21, q13, #5 + vqshrn.s16 d22, q14, #5 + vqshrn.s16 d23, q15, #5 + vadd.u8 q8, q8, q0 + vadd.u8 q9, q9, q0 + vadd.u8 q10, q10, q0 + vadd.u8 q11, q11, q0 + /* Transpose the final 8-bit samples */ + vtrn.16 q8, q9 + vtrn.16 q10, q11 + vtrn.32 q8, q10 + vtrn.32 q9, q11 + vtrn.8 d16, d17 + vtrn.8 d18, d19 + /* Store results to the output buffer */ + ldmia OUTPUT_BUF!, {TMP1, TMP2} + add TMP1, TMP1, OUTPUT_COL + add TMP2, TMP2, OUTPUT_COL + vst1.8 {d16}, [TMP1] + vst1.8 {d17}, [TMP2] + ldmia OUTPUT_BUF!, {TMP1, TMP2} + add TMP1, TMP1, OUTPUT_COL + add TMP2, TMP2, OUTPUT_COL + vst1.8 {d18}, [TMP1] + vtrn.8 d20, d21 + vst1.8 {d19}, [TMP2] + ldmia OUTPUT_BUF, {TMP1, TMP2, TMP3, TMP4} + add TMP1, TMP1, OUTPUT_COL + add TMP2, TMP2, OUTPUT_COL + add TMP3, TMP3, OUTPUT_COL + add TMP4, TMP4, OUTPUT_COL + vst1.8 {d20}, [TMP1] + vtrn.8 d22, d23 + vst1.8 {d21}, [TMP2] + vst1.8 {d22}, [TMP3] + vst1.8 {d23}, [TMP4] + bx lr + + .unreq DCT_TABLE + .unreq COEF_BLOCK + .unreq OUTPUT_BUF + .unreq OUTPUT_COL + .unreq TMP1 + .unreq TMP2 + .unreq TMP3 + .unreq TMP4 + + +/*****************************************************************************/ + +/* + * jsimd_extrgb_ycc_convert_neon + * jsimd_extbgr_ycc_convert_neon + * jsimd_extrgbx_ycc_convert_neon + * jsimd_extbgrx_ycc_convert_neon + * jsimd_extxbgr_ycc_convert_neon + * jsimd_extxrgb_ycc_convert_neon + * + * Colorspace conversion RGB -> YCbCr + */ + +.macro do_store size + .if \size == 8 + vst1.8 {d20}, [Y]! + vst1.8 {d21}, [U]! + vst1.8 {d22}, [V]! + .elseif \size == 4 + vst1.8 {d20[0]}, [Y]! + vst1.8 {d20[1]}, [Y]! + vst1.8 {d20[2]}, [Y]! + vst1.8 {d20[3]}, [Y]! + vst1.8 {d21[0]}, [U]! + vst1.8 {d21[1]}, [U]! + vst1.8 {d21[2]}, [U]! + vst1.8 {d21[3]}, [U]! + vst1.8 {d22[0]}, [V]! + vst1.8 {d22[1]}, [V]! + vst1.8 {d22[2]}, [V]! + vst1.8 {d22[3]}, [V]! + .elseif \size == 2 + vst1.8 {d20[4]}, [Y]! + vst1.8 {d20[5]}, [Y]! + vst1.8 {d21[4]}, [U]! + vst1.8 {d21[5]}, [U]! + vst1.8 {d22[4]}, [V]! + vst1.8 {d22[5]}, [V]! + .elseif \size == 1 + vst1.8 {d20[6]}, [Y]! + vst1.8 {d21[6]}, [U]! + vst1.8 {d22[6]}, [V]! + .else + .error unsupported macroblock size + .endif +.endm + +.macro do_load bpp, size + .if \bpp == 24 + .if \size == 8 + vld3.8 {d10, d11, d12}, [RGB]! + pld [RGB, #128] + .elseif \size == 4 + vld3.8 {d10[0], d11[0], d12[0]}, [RGB]! + vld3.8 {d10[1], d11[1], d12[1]}, [RGB]! + vld3.8 {d10[2], d11[2], d12[2]}, [RGB]! + vld3.8 {d10[3], d11[3], d12[3]}, [RGB]! + .elseif \size == 2 + vld3.8 {d10[4], d11[4], d12[4]}, [RGB]! + vld3.8 {d10[5], d11[5], d12[5]}, [RGB]! + .elseif \size == 1 + vld3.8 {d10[6], d11[6], d12[6]}, [RGB]! + .else + .error unsupported macroblock size + .endif + .elseif \bpp == 32 + .if \size == 8 + vld4.8 {d10, d11, d12, d13}, [RGB]! + pld [RGB, #128] + .elseif \size == 4 + vld4.8 {d10[0], d11[0], d12[0], d13[0]}, [RGB]! + vld4.8 {d10[1], d11[1], d12[1], d13[1]}, [RGB]! + vld4.8 {d10[2], d11[2], d12[2], d13[2]}, [RGB]! + vld4.8 {d10[3], d11[3], d12[3], d13[3]}, [RGB]! + .elseif \size == 2 + vld4.8 {d10[4], d11[4], d12[4], d13[4]}, [RGB]! + vld4.8 {d10[5], d11[5], d12[5], d13[5]}, [RGB]! + .elseif \size == 1 + vld4.8 {d10[6], d11[6], d12[6], d13[6]}, [RGB]! + .else + .error unsupported macroblock size + .endif + .else + .error unsupported bpp + .endif +.endm + +.macro generate_jsimd_rgb_ycc_convert_neon colorid, bpp, r_offs, g_offs, b_offs + +/* + * 2-stage pipelined RGB->YCbCr conversion + */ + +.macro do_rgb_to_yuv_stage1 + vmovl.u8 q2, d1\r_offs /* r = { d4, d5 } */ + vmovl.u8 q3, d1\g_offs /* g = { d6, d7 } */ + vmovl.u8 q4, d1\b_offs /* b = { d8, d9 } */ + vmull.u16 q7, d4, d0[0] + vmlal.u16 q7, d6, d0[1] + vmlal.u16 q7, d8, d0[2] + vmull.u16 q8, d5, d0[0] + vmlal.u16 q8, d7, d0[1] + vmlal.u16 q8, d9, d0[2] + vrev64.32 q9, q1 + vrev64.32 q13, q1 + vmlsl.u16 q9, d4, d0[3] + vmlsl.u16 q9, d6, d1[0] + vmlal.u16 q9, d8, d1[1] + vmlsl.u16 q13, d5, d0[3] + vmlsl.u16 q13, d7, d1[0] + vmlal.u16 q13, d9, d1[1] + vrev64.32 q14, q1 + vrev64.32 q15, q1 + vmlal.u16 q14, d4, d1[1] + vmlsl.u16 q14, d6, d1[2] + vmlsl.u16 q14, d8, d1[3] + vmlal.u16 q15, d5, d1[1] + vmlsl.u16 q15, d7, d1[2] + vmlsl.u16 q15, d9, d1[3] +.endm + +.macro do_rgb_to_yuv_stage2 + vrshrn.u32 d20, q7, #16 + vrshrn.u32 d21, q8, #16 + vshrn.u32 d22, q9, #16 + vshrn.u32 d23, q13, #16 + vshrn.u32 d24, q14, #16 + vshrn.u32 d25, q15, #16 + vmovn.u16 d20, q10 /* d20 = y */ + vmovn.u16 d21, q11 /* d21 = u */ + vmovn.u16 d22, q12 /* d22 = v */ +.endm + +.macro do_rgb_to_yuv + do_rgb_to_yuv_stage1 + do_rgb_to_yuv_stage2 +.endm + +.macro do_rgb_to_yuv_stage2_store_load_stage1 + vrshrn.u32 d20, q7, #16 + vrshrn.u32 d21, q8, #16 + vshrn.u32 d22, q9, #16 + vrev64.32 q9, q1 + vshrn.u32 d23, q13, #16 + vrev64.32 q13, q1 + vshrn.u32 d24, q14, #16 + vshrn.u32 d25, q15, #16 + do_load \bpp, 8 + vmovn.u16 d20, q10 /* d20 = y */ + vmovl.u8 q2, d1\r_offs /* r = { d4, d5 } */ + vmovn.u16 d21, q11 /* d21 = u */ + vmovl.u8 q3, d1\g_offs /* g = { d6, d7 } */ + vmovn.u16 d22, q12 /* d22 = v */ + vmovl.u8 q4, d1\b_offs /* b = { d8, d9 } */ + vmull.u16 q7, d4, d0[0] + vmlal.u16 q7, d6, d0[1] + vmlal.u16 q7, d8, d0[2] + vst1.8 {d20}, [Y]! + vmull.u16 q8, d5, d0[0] + vmlal.u16 q8, d7, d0[1] + vmlal.u16 q8, d9, d0[2] + vmlsl.u16 q9, d4, d0[3] + vmlsl.u16 q9, d6, d1[0] + vmlal.u16 q9, d8, d1[1] + vst1.8 {d21}, [U]! + vmlsl.u16 q13, d5, d0[3] + vmlsl.u16 q13, d7, d1[0] + vmlal.u16 q13, d9, d1[1] + vrev64.32 q14, q1 + vrev64.32 q15, q1 + vmlal.u16 q14, d4, d1[1] + vmlsl.u16 q14, d6, d1[2] + vmlsl.u16 q14, d8, d1[3] + vst1.8 {d22}, [V]! + vmlal.u16 q15, d5, d1[1] + vmlsl.u16 q15, d7, d1[2] + vmlsl.u16 q15, d9, d1[3] +.endm + +.balign 16 +jsimd_\colorid\()_ycc_neon_consts: + .short 19595, 38470, 7471, 11059 + .short 21709, 32768, 27439, 5329 + .short 32767, 128, 32767, 128 + .short 32767, 128, 32767, 128 + +asm_function jsimd_\colorid\()_ycc_convert_neon + OUTPUT_WIDTH .req r0 + INPUT_BUF .req r1 + OUTPUT_BUF .req r2 + OUTPUT_ROW .req r3 + NUM_ROWS .req r4 + + OUTPUT_BUF0 .req r5 + OUTPUT_BUF1 .req r6 + OUTPUT_BUF2 .req OUTPUT_BUF + + RGB .req r7 + Y .req r8 + U .req r9 + V .req r10 + N .req ip + + /* Load constants to d0, d1, d2, d3 */ + adr ip, jsimd_\colorid\()_ycc_neon_consts + vld1.16 {d0, d1, d2, d3}, [ip, :128] + + /* Save Arm registers and handle input arguments */ + push {r4, r5, r6, r7, r8, r9, r10, lr} + ldr NUM_ROWS, [sp, #(4 * 8)] + ldr OUTPUT_BUF0, [OUTPUT_BUF] + ldr OUTPUT_BUF1, [OUTPUT_BUF, #4] + ldr OUTPUT_BUF2, [OUTPUT_BUF, #8] + .unreq OUTPUT_BUF + + /* Save Neon registers */ + vpush {d8 - d15} + + /* Outer loop over scanlines */ + cmp NUM_ROWS, #1 + blt 9f +0: + ldr Y, [OUTPUT_BUF0, OUTPUT_ROW, lsl #2] + ldr U, [OUTPUT_BUF1, OUTPUT_ROW, lsl #2] + mov N, OUTPUT_WIDTH + ldr V, [OUTPUT_BUF2, OUTPUT_ROW, lsl #2] + add OUTPUT_ROW, OUTPUT_ROW, #1 + ldr RGB, [INPUT_BUF], #4 + + /* Inner loop over pixels */ + subs N, N, #8 + blt 3f + do_load \bpp, 8 + do_rgb_to_yuv_stage1 + subs N, N, #8 + blt 2f +1: + do_rgb_to_yuv_stage2_store_load_stage1 + subs N, N, #8 + bge 1b +2: + do_rgb_to_yuv_stage2 + do_store 8 + tst N, #7 + beq 8f +3: + tst N, #4 + beq 3f + do_load \bpp, 4 +3: + tst N, #2 + beq 4f + do_load \bpp, 2 +4: + tst N, #1 + beq 5f + do_load \bpp, 1 +5: + do_rgb_to_yuv + tst N, #4 + beq 6f + do_store 4 +6: + tst N, #2 + beq 7f + do_store 2 +7: + tst N, #1 + beq 8f + do_store 1 +8: + subs NUM_ROWS, NUM_ROWS, #1 + bgt 0b +9: + /* Restore all registers and return */ + vpop {d8 - d15} + pop {r4, r5, r6, r7, r8, r9, r10, pc} + + .unreq OUTPUT_WIDTH + .unreq OUTPUT_ROW + .unreq INPUT_BUF + .unreq NUM_ROWS + .unreq OUTPUT_BUF0 + .unreq OUTPUT_BUF1 + .unreq OUTPUT_BUF2 + .unreq RGB + .unreq Y + .unreq U + .unreq V + .unreq N + +.purgem do_rgb_to_yuv +.purgem do_rgb_to_yuv_stage1 +.purgem do_rgb_to_yuv_stage2 +.purgem do_rgb_to_yuv_stage2_store_load_stage1 + +.endm + +/*--------------------------------- id ----- bpp R G B */ +generate_jsimd_rgb_ycc_convert_neon extrgb, 24, 0, 1, 2 +generate_jsimd_rgb_ycc_convert_neon extbgr, 24, 2, 1, 0 +generate_jsimd_rgb_ycc_convert_neon extrgbx, 32, 0, 1, 2 +generate_jsimd_rgb_ycc_convert_neon extbgrx, 32, 2, 1, 0 +generate_jsimd_rgb_ycc_convert_neon extxbgr, 32, 3, 2, 1 +generate_jsimd_rgb_ycc_convert_neon extxrgb, 32, 1, 2, 3 + +.purgem do_load +.purgem do_store diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jccolext-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jccolext-neon.c new file mode 100644 index 00000000000..37130c225ed --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jccolext-neon.c @@ -0,0 +1,316 @@ +/* + * jccolext-neon.c - colorspace conversion (64-bit Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jccolor-neon.c */ + + +/* RGB -> YCbCr conversion is defined by the following equations: + * Y = 0.29900 * R + 0.58700 * G + 0.11400 * B + * Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + 128 + * Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + 128 + * + * Avoid floating point arithmetic by using shifted integer constants: + * 0.29899597 = 19595 * 2^-16 + * 0.58700561 = 38470 * 2^-16 + * 0.11399841 = 7471 * 2^-16 + * 0.16874695 = 11059 * 2^-16 + * 0.33125305 = 21709 * 2^-16 + * 0.50000000 = 32768 * 2^-16 + * 0.41868592 = 27439 * 2^-16 + * 0.08131409 = 5329 * 2^-16 + * These constants are defined in jccolor-neon.c + * + * We add the fixed-point equivalent of 0.5 to Cb and Cr, which effectively + * rounds up or down the result via integer truncation. + */ + +void jsimd_rgb_ycc_convert_neon(JDIMENSION image_width, JSAMPARRAY input_buf, + JSAMPIMAGE output_buf, JDIMENSION output_row, + int num_rows) +{ + /* Pointer to RGB(X/A) input data */ + JSAMPROW inptr; + /* Pointers to Y, Cb, and Cr output data */ + JSAMPROW outptr0, outptr1, outptr2; + /* Allocate temporary buffer for final (image_width % 16) pixels in row. */ + ALIGN(16) uint8_t tmp_buf[16 * RGB_PIXELSIZE]; + + /* Set up conversion constants. */ + const uint16x8_t consts = vld1q_u16(jsimd_rgb_ycc_neon_consts); + const uint32x4_t scaled_128_5 = vdupq_n_u32((128 << 16) + 32767); + + while (--num_rows >= 0) { + inptr = *input_buf++; + outptr0 = output_buf[0][output_row]; + outptr1 = output_buf[1][output_row]; + outptr2 = output_buf[2][output_row]; + output_row++; + + int cols_remaining = image_width; + for (; cols_remaining >= 16; cols_remaining -= 16) { + +#if RGB_PIXELSIZE == 4 + uint8x16x4_t input_pixels = vld4q_u8(inptr); +#else + uint8x16x3_t input_pixels = vld3q_u8(inptr); +#endif + uint16x8_t r_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_RED])); + uint16x8_t g_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_GREEN])); + uint16x8_t b_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_BLUE])); + uint16x8_t r_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_RED])); + uint16x8_t g_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_GREEN])); + uint16x8_t b_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_BLUE])); + + /* Compute Y = 0.29900 * R + 0.58700 * G + 0.11400 * B */ + uint32x4_t y_ll = vmull_laneq_u16(vget_low_u16(r_l), consts, 0); + y_ll = vmlal_laneq_u16(y_ll, vget_low_u16(g_l), consts, 1); + y_ll = vmlal_laneq_u16(y_ll, vget_low_u16(b_l), consts, 2); + uint32x4_t y_lh = vmull_laneq_u16(vget_high_u16(r_l), consts, 0); + y_lh = vmlal_laneq_u16(y_lh, vget_high_u16(g_l), consts, 1); + y_lh = vmlal_laneq_u16(y_lh, vget_high_u16(b_l), consts, 2); + uint32x4_t y_hl = vmull_laneq_u16(vget_low_u16(r_h), consts, 0); + y_hl = vmlal_laneq_u16(y_hl, vget_low_u16(g_h), consts, 1); + y_hl = vmlal_laneq_u16(y_hl, vget_low_u16(b_h), consts, 2); + uint32x4_t y_hh = vmull_laneq_u16(vget_high_u16(r_h), consts, 0); + y_hh = vmlal_laneq_u16(y_hh, vget_high_u16(g_h), consts, 1); + y_hh = vmlal_laneq_u16(y_hh, vget_high_u16(b_h), consts, 2); + + /* Compute Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + 128 */ + uint32x4_t cb_ll = scaled_128_5; + cb_ll = vmlsl_laneq_u16(cb_ll, vget_low_u16(r_l), consts, 3); + cb_ll = vmlsl_laneq_u16(cb_ll, vget_low_u16(g_l), consts, 4); + cb_ll = vmlal_laneq_u16(cb_ll, vget_low_u16(b_l), consts, 5); + uint32x4_t cb_lh = scaled_128_5; + cb_lh = vmlsl_laneq_u16(cb_lh, vget_high_u16(r_l), consts, 3); + cb_lh = vmlsl_laneq_u16(cb_lh, vget_high_u16(g_l), consts, 4); + cb_lh = vmlal_laneq_u16(cb_lh, vget_high_u16(b_l), consts, 5); + uint32x4_t cb_hl = scaled_128_5; + cb_hl = vmlsl_laneq_u16(cb_hl, vget_low_u16(r_h), consts, 3); + cb_hl = vmlsl_laneq_u16(cb_hl, vget_low_u16(g_h), consts, 4); + cb_hl = vmlal_laneq_u16(cb_hl, vget_low_u16(b_h), consts, 5); + uint32x4_t cb_hh = scaled_128_5; + cb_hh = vmlsl_laneq_u16(cb_hh, vget_high_u16(r_h), consts, 3); + cb_hh = vmlsl_laneq_u16(cb_hh, vget_high_u16(g_h), consts, 4); + cb_hh = vmlal_laneq_u16(cb_hh, vget_high_u16(b_h), consts, 5); + + /* Compute Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + 128 */ + uint32x4_t cr_ll = scaled_128_5; + cr_ll = vmlal_laneq_u16(cr_ll, vget_low_u16(r_l), consts, 5); + cr_ll = vmlsl_laneq_u16(cr_ll, vget_low_u16(g_l), consts, 6); + cr_ll = vmlsl_laneq_u16(cr_ll, vget_low_u16(b_l), consts, 7); + uint32x4_t cr_lh = scaled_128_5; + cr_lh = vmlal_laneq_u16(cr_lh, vget_high_u16(r_l), consts, 5); + cr_lh = vmlsl_laneq_u16(cr_lh, vget_high_u16(g_l), consts, 6); + cr_lh = vmlsl_laneq_u16(cr_lh, vget_high_u16(b_l), consts, 7); + uint32x4_t cr_hl = scaled_128_5; + cr_hl = vmlal_laneq_u16(cr_hl, vget_low_u16(r_h), consts, 5); + cr_hl = vmlsl_laneq_u16(cr_hl, vget_low_u16(g_h), consts, 6); + cr_hl = vmlsl_laneq_u16(cr_hl, vget_low_u16(b_h), consts, 7); + uint32x4_t cr_hh = scaled_128_5; + cr_hh = vmlal_laneq_u16(cr_hh, vget_high_u16(r_h), consts, 5); + cr_hh = vmlsl_laneq_u16(cr_hh, vget_high_u16(g_h), consts, 6); + cr_hh = vmlsl_laneq_u16(cr_hh, vget_high_u16(b_h), consts, 7); + + /* Descale Y values (rounding right shift) and narrow to 16-bit. */ + uint16x8_t y_l = vcombine_u16(vrshrn_n_u32(y_ll, 16), + vrshrn_n_u32(y_lh, 16)); + uint16x8_t y_h = vcombine_u16(vrshrn_n_u32(y_hl, 16), + vrshrn_n_u32(y_hh, 16)); + /* Descale Cb values (right shift) and narrow to 16-bit. */ + uint16x8_t cb_l = vcombine_u16(vshrn_n_u32(cb_ll, 16), + vshrn_n_u32(cb_lh, 16)); + uint16x8_t cb_h = vcombine_u16(vshrn_n_u32(cb_hl, 16), + vshrn_n_u32(cb_hh, 16)); + /* Descale Cr values (right shift) and narrow to 16-bit. */ + uint16x8_t cr_l = vcombine_u16(vshrn_n_u32(cr_ll, 16), + vshrn_n_u32(cr_lh, 16)); + uint16x8_t cr_h = vcombine_u16(vshrn_n_u32(cr_hl, 16), + vshrn_n_u32(cr_hh, 16)); + /* Narrow Y, Cb, and Cr values to 8-bit and store to memory. Buffer + * overwrite is permitted up to the next multiple of ALIGN_SIZE bytes. + */ + vst1q_u8(outptr0, vcombine_u8(vmovn_u16(y_l), vmovn_u16(y_h))); + vst1q_u8(outptr1, vcombine_u8(vmovn_u16(cb_l), vmovn_u16(cb_h))); + vst1q_u8(outptr2, vcombine_u8(vmovn_u16(cr_l), vmovn_u16(cr_h))); + + /* Increment pointers. */ + inptr += (16 * RGB_PIXELSIZE); + outptr0 += 16; + outptr1 += 16; + outptr2 += 16; + } + + if (cols_remaining > 8) { + /* To prevent buffer overread by the vector load instructions, the last + * (image_width % 16) columns of data are first memcopied to a temporary + * buffer large enough to accommodate the vector load. + */ + memcpy(tmp_buf, inptr, cols_remaining * RGB_PIXELSIZE); + inptr = tmp_buf; + +#if RGB_PIXELSIZE == 4 + uint8x16x4_t input_pixels = vld4q_u8(inptr); +#else + uint8x16x3_t input_pixels = vld3q_u8(inptr); +#endif + uint16x8_t r_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_RED])); + uint16x8_t g_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_GREEN])); + uint16x8_t b_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_BLUE])); + uint16x8_t r_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_RED])); + uint16x8_t g_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_GREEN])); + uint16x8_t b_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_BLUE])); + + /* Compute Y = 0.29900 * R + 0.58700 * G + 0.11400 * B */ + uint32x4_t y_ll = vmull_laneq_u16(vget_low_u16(r_l), consts, 0); + y_ll = vmlal_laneq_u16(y_ll, vget_low_u16(g_l), consts, 1); + y_ll = vmlal_laneq_u16(y_ll, vget_low_u16(b_l), consts, 2); + uint32x4_t y_lh = vmull_laneq_u16(vget_high_u16(r_l), consts, 0); + y_lh = vmlal_laneq_u16(y_lh, vget_high_u16(g_l), consts, 1); + y_lh = vmlal_laneq_u16(y_lh, vget_high_u16(b_l), consts, 2); + uint32x4_t y_hl = vmull_laneq_u16(vget_low_u16(r_h), consts, 0); + y_hl = vmlal_laneq_u16(y_hl, vget_low_u16(g_h), consts, 1); + y_hl = vmlal_laneq_u16(y_hl, vget_low_u16(b_h), consts, 2); + uint32x4_t y_hh = vmull_laneq_u16(vget_high_u16(r_h), consts, 0); + y_hh = vmlal_laneq_u16(y_hh, vget_high_u16(g_h), consts, 1); + y_hh = vmlal_laneq_u16(y_hh, vget_high_u16(b_h), consts, 2); + + /* Compute Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + 128 */ + uint32x4_t cb_ll = scaled_128_5; + cb_ll = vmlsl_laneq_u16(cb_ll, vget_low_u16(r_l), consts, 3); + cb_ll = vmlsl_laneq_u16(cb_ll, vget_low_u16(g_l), consts, 4); + cb_ll = vmlal_laneq_u16(cb_ll, vget_low_u16(b_l), consts, 5); + uint32x4_t cb_lh = scaled_128_5; + cb_lh = vmlsl_laneq_u16(cb_lh, vget_high_u16(r_l), consts, 3); + cb_lh = vmlsl_laneq_u16(cb_lh, vget_high_u16(g_l), consts, 4); + cb_lh = vmlal_laneq_u16(cb_lh, vget_high_u16(b_l), consts, 5); + uint32x4_t cb_hl = scaled_128_5; + cb_hl = vmlsl_laneq_u16(cb_hl, vget_low_u16(r_h), consts, 3); + cb_hl = vmlsl_laneq_u16(cb_hl, vget_low_u16(g_h), consts, 4); + cb_hl = vmlal_laneq_u16(cb_hl, vget_low_u16(b_h), consts, 5); + uint32x4_t cb_hh = scaled_128_5; + cb_hh = vmlsl_laneq_u16(cb_hh, vget_high_u16(r_h), consts, 3); + cb_hh = vmlsl_laneq_u16(cb_hh, vget_high_u16(g_h), consts, 4); + cb_hh = vmlal_laneq_u16(cb_hh, vget_high_u16(b_h), consts, 5); + + /* Compute Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + 128 */ + uint32x4_t cr_ll = scaled_128_5; + cr_ll = vmlal_laneq_u16(cr_ll, vget_low_u16(r_l), consts, 5); + cr_ll = vmlsl_laneq_u16(cr_ll, vget_low_u16(g_l), consts, 6); + cr_ll = vmlsl_laneq_u16(cr_ll, vget_low_u16(b_l), consts, 7); + uint32x4_t cr_lh = scaled_128_5; + cr_lh = vmlal_laneq_u16(cr_lh, vget_high_u16(r_l), consts, 5); + cr_lh = vmlsl_laneq_u16(cr_lh, vget_high_u16(g_l), consts, 6); + cr_lh = vmlsl_laneq_u16(cr_lh, vget_high_u16(b_l), consts, 7); + uint32x4_t cr_hl = scaled_128_5; + cr_hl = vmlal_laneq_u16(cr_hl, vget_low_u16(r_h), consts, 5); + cr_hl = vmlsl_laneq_u16(cr_hl, vget_low_u16(g_h), consts, 6); + cr_hl = vmlsl_laneq_u16(cr_hl, vget_low_u16(b_h), consts, 7); + uint32x4_t cr_hh = scaled_128_5; + cr_hh = vmlal_laneq_u16(cr_hh, vget_high_u16(r_h), consts, 5); + cr_hh = vmlsl_laneq_u16(cr_hh, vget_high_u16(g_h), consts, 6); + cr_hh = vmlsl_laneq_u16(cr_hh, vget_high_u16(b_h), consts, 7); + + /* Descale Y values (rounding right shift) and narrow to 16-bit. */ + uint16x8_t y_l = vcombine_u16(vrshrn_n_u32(y_ll, 16), + vrshrn_n_u32(y_lh, 16)); + uint16x8_t y_h = vcombine_u16(vrshrn_n_u32(y_hl, 16), + vrshrn_n_u32(y_hh, 16)); + /* Descale Cb values (right shift) and narrow to 16-bit. */ + uint16x8_t cb_l = vcombine_u16(vshrn_n_u32(cb_ll, 16), + vshrn_n_u32(cb_lh, 16)); + uint16x8_t cb_h = vcombine_u16(vshrn_n_u32(cb_hl, 16), + vshrn_n_u32(cb_hh, 16)); + /* Descale Cr values (right shift) and narrow to 16-bit. */ + uint16x8_t cr_l = vcombine_u16(vshrn_n_u32(cr_ll, 16), + vshrn_n_u32(cr_lh, 16)); + uint16x8_t cr_h = vcombine_u16(vshrn_n_u32(cr_hl, 16), + vshrn_n_u32(cr_hh, 16)); + /* Narrow Y, Cb, and Cr values to 8-bit and store to memory. Buffer + * overwrite is permitted up to the next multiple of ALIGN_SIZE bytes. + */ + vst1q_u8(outptr0, vcombine_u8(vmovn_u16(y_l), vmovn_u16(y_h))); + vst1q_u8(outptr1, vcombine_u8(vmovn_u16(cb_l), vmovn_u16(cb_h))); + vst1q_u8(outptr2, vcombine_u8(vmovn_u16(cr_l), vmovn_u16(cr_h))); + + } else if (cols_remaining > 0) { + /* To prevent buffer overread by the vector load instructions, the last + * (image_width % 8) columns of data are first memcopied to a temporary + * buffer large enough to accommodate the vector load. + */ + memcpy(tmp_buf, inptr, cols_remaining * RGB_PIXELSIZE); + inptr = tmp_buf; + +#if RGB_PIXELSIZE == 4 + uint8x8x4_t input_pixels = vld4_u8(inptr); +#else + uint8x8x3_t input_pixels = vld3_u8(inptr); +#endif + uint16x8_t r = vmovl_u8(input_pixels.val[RGB_RED]); + uint16x8_t g = vmovl_u8(input_pixels.val[RGB_GREEN]); + uint16x8_t b = vmovl_u8(input_pixels.val[RGB_BLUE]); + + /* Compute Y = 0.29900 * R + 0.58700 * G + 0.11400 * B */ + uint32x4_t y_l = vmull_laneq_u16(vget_low_u16(r), consts, 0); + y_l = vmlal_laneq_u16(y_l, vget_low_u16(g), consts, 1); + y_l = vmlal_laneq_u16(y_l, vget_low_u16(b), consts, 2); + uint32x4_t y_h = vmull_laneq_u16(vget_high_u16(r), consts, 0); + y_h = vmlal_laneq_u16(y_h, vget_high_u16(g), consts, 1); + y_h = vmlal_laneq_u16(y_h, vget_high_u16(b), consts, 2); + + /* Compute Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + 128 */ + uint32x4_t cb_l = scaled_128_5; + cb_l = vmlsl_laneq_u16(cb_l, vget_low_u16(r), consts, 3); + cb_l = vmlsl_laneq_u16(cb_l, vget_low_u16(g), consts, 4); + cb_l = vmlal_laneq_u16(cb_l, vget_low_u16(b), consts, 5); + uint32x4_t cb_h = scaled_128_5; + cb_h = vmlsl_laneq_u16(cb_h, vget_high_u16(r), consts, 3); + cb_h = vmlsl_laneq_u16(cb_h, vget_high_u16(g), consts, 4); + cb_h = vmlal_laneq_u16(cb_h, vget_high_u16(b), consts, 5); + + /* Compute Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + 128 */ + uint32x4_t cr_l = scaled_128_5; + cr_l = vmlal_laneq_u16(cr_l, vget_low_u16(r), consts, 5); + cr_l = vmlsl_laneq_u16(cr_l, vget_low_u16(g), consts, 6); + cr_l = vmlsl_laneq_u16(cr_l, vget_low_u16(b), consts, 7); + uint32x4_t cr_h = scaled_128_5; + cr_h = vmlal_laneq_u16(cr_h, vget_high_u16(r), consts, 5); + cr_h = vmlsl_laneq_u16(cr_h, vget_high_u16(g), consts, 6); + cr_h = vmlsl_laneq_u16(cr_h, vget_high_u16(b), consts, 7); + + /* Descale Y values (rounding right shift) and narrow to 16-bit. */ + uint16x8_t y_u16 = vcombine_u16(vrshrn_n_u32(y_l, 16), + vrshrn_n_u32(y_h, 16)); + /* Descale Cb values (right shift) and narrow to 16-bit. */ + uint16x8_t cb_u16 = vcombine_u16(vshrn_n_u32(cb_l, 16), + vshrn_n_u32(cb_h, 16)); + /* Descale Cr values (right shift) and narrow to 16-bit. */ + uint16x8_t cr_u16 = vcombine_u16(vshrn_n_u32(cr_l, 16), + vshrn_n_u32(cr_h, 16)); + /* Narrow Y, Cb, and Cr values to 8-bit and store to memory. Buffer + * overwrite is permitted up to the next multiple of ALIGN_SIZE bytes. + */ + vst1_u8(outptr0, vmovn_u16(y_u16)); + vst1_u8(outptr1, vmovn_u16(cb_u16)); + vst1_u8(outptr2, vmovn_u16(cr_u16)); + } + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jchuff-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jchuff-neon.c new file mode 100644 index 00000000000..607a116070c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jchuff-neon.c @@ -0,0 +1,411 @@ +/* + * jchuff-neon.c - Huffman entropy encoding (64-bit Arm Neon) + * + * Copyright (C) 2020-2021, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, 2022, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * NOTE: All referenced figures are from + * Recommendation ITU-T T.81 (1992) | ISO/IEC 10918-1:1994. + */ + +#define JPEG_INTERNALS +#include "../../../jinclude.h" +#include "../../../jpeglib.h" +#include "../../../jsimd.h" +#include "../../../jdct.h" +#include "../../../jsimddct.h" +#include "../../jsimd.h" +#include "../align.h" +#include "../jchuff.h" +#include "neon-compat.h" + +#include + +#include + + +ALIGN(16) static const uint8_t jsimd_huff_encode_one_block_consts[] = { + 0, 1, 2, 3, 16, 17, 32, 33, + 18, 19, 4, 5, 6, 7, 20, 21, + 34, 35, 48, 49, 255, 255, 50, 51, + 36, 37, 22, 23, 8, 9, 10, 11, + 255, 255, 6, 7, 20, 21, 34, 35, + 48, 49, 255, 255, 50, 51, 36, 37, + 54, 55, 40, 41, 26, 27, 12, 13, + 14, 15, 28, 29, 42, 43, 56, 57, + 6, 7, 20, 21, 34, 35, 48, 49, + 50, 51, 36, 37, 22, 23, 8, 9, + 26, 27, 12, 13, 255, 255, 14, 15, + 28, 29, 42, 43, 56, 57, 255, 255, + 52, 53, 54, 55, 40, 41, 26, 27, + 12, 13, 255, 255, 14, 15, 28, 29, + 26, 27, 40, 41, 42, 43, 28, 29, + 14, 15, 30, 31, 44, 45, 46, 47 +}; + +/* The AArch64 implementation of the FLUSH() macro triggers a UBSan misaligned + * address warning because the macro sometimes writes a 64-bit value to a + * non-64-bit-aligned address. That behavior is technically undefined per + * the C specification, but it is supported by the AArch64 architecture and + * compilers. + */ +#if defined(__has_feature) +#if __has_feature(undefined_behavior_sanitizer) +__attribute__((no_sanitize("alignment"))) +#endif +#endif +JOCTET *jsimd_huff_encode_one_block_neon(void *state, JOCTET *buffer, + JCOEFPTR block, int last_dc_val, + c_derived_tbl *dctbl, + c_derived_tbl *actbl) +{ + uint16_t block_diff[DCTSIZE2]; + + /* Load lookup table indices for rows of zig-zag ordering. */ +#ifdef HAVE_VLD1Q_U8_X4 + const uint8x16x4_t idx_rows_0123 = + vld1q_u8_x4(jsimd_huff_encode_one_block_consts + 0 * DCTSIZE); + const uint8x16x4_t idx_rows_4567 = + vld1q_u8_x4(jsimd_huff_encode_one_block_consts + 8 * DCTSIZE); +#else + /* GCC does not currently support intrinsics vl1dq__x4(). */ + const uint8x16x4_t idx_rows_0123 = { { + vld1q_u8(jsimd_huff_encode_one_block_consts + 0 * DCTSIZE), + vld1q_u8(jsimd_huff_encode_one_block_consts + 2 * DCTSIZE), + vld1q_u8(jsimd_huff_encode_one_block_consts + 4 * DCTSIZE), + vld1q_u8(jsimd_huff_encode_one_block_consts + 6 * DCTSIZE) + } }; + const uint8x16x4_t idx_rows_4567 = { { + vld1q_u8(jsimd_huff_encode_one_block_consts + 8 * DCTSIZE), + vld1q_u8(jsimd_huff_encode_one_block_consts + 10 * DCTSIZE), + vld1q_u8(jsimd_huff_encode_one_block_consts + 12 * DCTSIZE), + vld1q_u8(jsimd_huff_encode_one_block_consts + 14 * DCTSIZE) + } }; +#endif + + /* Load 8x8 block of DCT coefficients. */ +#ifdef HAVE_VLD1Q_U8_X4 + const int8x16x4_t tbl_rows_0123 = + vld1q_s8_x4((int8_t *)(block + 0 * DCTSIZE)); + const int8x16x4_t tbl_rows_4567 = + vld1q_s8_x4((int8_t *)(block + 4 * DCTSIZE)); +#else + const int8x16x4_t tbl_rows_0123 = { { + vld1q_s8((int8_t *)(block + 0 * DCTSIZE)), + vld1q_s8((int8_t *)(block + 1 * DCTSIZE)), + vld1q_s8((int8_t *)(block + 2 * DCTSIZE)), + vld1q_s8((int8_t *)(block + 3 * DCTSIZE)) + } }; + const int8x16x4_t tbl_rows_4567 = { { + vld1q_s8((int8_t *)(block + 4 * DCTSIZE)), + vld1q_s8((int8_t *)(block + 5 * DCTSIZE)), + vld1q_s8((int8_t *)(block + 6 * DCTSIZE)), + vld1q_s8((int8_t *)(block + 7 * DCTSIZE)) + } }; +#endif + + /* Initialise extra lookup tables. */ + const int8x16x4_t tbl_rows_2345 = { { + tbl_rows_0123.val[2], tbl_rows_0123.val[3], + tbl_rows_4567.val[0], tbl_rows_4567.val[1] + } }; + const int8x16x3_t tbl_rows_567 = + { { tbl_rows_4567.val[1], tbl_rows_4567.val[2], tbl_rows_4567.val[3] } }; + + /* Shuffle coefficients into zig-zag order. */ + int16x8_t row0 = + vreinterpretq_s16_s8(vqtbl4q_s8(tbl_rows_0123, idx_rows_0123.val[0])); + int16x8_t row1 = + vreinterpretq_s16_s8(vqtbl4q_s8(tbl_rows_0123, idx_rows_0123.val[1])); + int16x8_t row2 = + vreinterpretq_s16_s8(vqtbl4q_s8(tbl_rows_2345, idx_rows_0123.val[2])); + int16x8_t row3 = + vreinterpretq_s16_s8(vqtbl4q_s8(tbl_rows_0123, idx_rows_0123.val[3])); + int16x8_t row4 = + vreinterpretq_s16_s8(vqtbl4q_s8(tbl_rows_4567, idx_rows_4567.val[0])); + int16x8_t row5 = + vreinterpretq_s16_s8(vqtbl4q_s8(tbl_rows_2345, idx_rows_4567.val[1])); + int16x8_t row6 = + vreinterpretq_s16_s8(vqtbl4q_s8(tbl_rows_4567, idx_rows_4567.val[2])); + int16x8_t row7 = + vreinterpretq_s16_s8(vqtbl3q_s8(tbl_rows_567, idx_rows_4567.val[3])); + + /* Compute DC coefficient difference value (F.1.1.5.1). */ + row0 = vsetq_lane_s16(block[0] - last_dc_val, row0, 0); + /* Initialize AC coefficient lanes not reachable by lookup tables. */ + row1 = + vsetq_lane_s16(vgetq_lane_s16(vreinterpretq_s16_s8(tbl_rows_4567.val[0]), + 0), row1, 2); + row2 = + vsetq_lane_s16(vgetq_lane_s16(vreinterpretq_s16_s8(tbl_rows_0123.val[1]), + 4), row2, 0); + row2 = + vsetq_lane_s16(vgetq_lane_s16(vreinterpretq_s16_s8(tbl_rows_4567.val[2]), + 0), row2, 5); + row5 = + vsetq_lane_s16(vgetq_lane_s16(vreinterpretq_s16_s8(tbl_rows_0123.val[1]), + 7), row5, 2); + row5 = + vsetq_lane_s16(vgetq_lane_s16(vreinterpretq_s16_s8(tbl_rows_4567.val[2]), + 3), row5, 7); + row6 = + vsetq_lane_s16(vgetq_lane_s16(vreinterpretq_s16_s8(tbl_rows_0123.val[3]), + 7), row6, 5); + + /* DCT block is now in zig-zag order; start Huffman encoding process. */ + + /* Construct bitmap to accelerate encoding of AC coefficients. A set bit + * means that the corresponding coefficient != 0. + */ + uint16x8_t row0_ne_0 = vtstq_s16(row0, row0); + uint16x8_t row1_ne_0 = vtstq_s16(row1, row1); + uint16x8_t row2_ne_0 = vtstq_s16(row2, row2); + uint16x8_t row3_ne_0 = vtstq_s16(row3, row3); + uint16x8_t row4_ne_0 = vtstq_s16(row4, row4); + uint16x8_t row5_ne_0 = vtstq_s16(row5, row5); + uint16x8_t row6_ne_0 = vtstq_s16(row6, row6); + uint16x8_t row7_ne_0 = vtstq_s16(row7, row7); + + uint8x16_t row10_ne_0 = vuzp1q_u8(vreinterpretq_u8_u16(row1_ne_0), + vreinterpretq_u8_u16(row0_ne_0)); + uint8x16_t row32_ne_0 = vuzp1q_u8(vreinterpretq_u8_u16(row3_ne_0), + vreinterpretq_u8_u16(row2_ne_0)); + uint8x16_t row54_ne_0 = vuzp1q_u8(vreinterpretq_u8_u16(row5_ne_0), + vreinterpretq_u8_u16(row4_ne_0)); + uint8x16_t row76_ne_0 = vuzp1q_u8(vreinterpretq_u8_u16(row7_ne_0), + vreinterpretq_u8_u16(row6_ne_0)); + + /* { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 } */ + const uint8x16_t bitmap_mask = + vreinterpretq_u8_u64(vdupq_n_u64(0x0102040810204080)); + + uint8x16_t bitmap_rows_10 = vandq_u8(row10_ne_0, bitmap_mask); + uint8x16_t bitmap_rows_32 = vandq_u8(row32_ne_0, bitmap_mask); + uint8x16_t bitmap_rows_54 = vandq_u8(row54_ne_0, bitmap_mask); + uint8x16_t bitmap_rows_76 = vandq_u8(row76_ne_0, bitmap_mask); + + uint8x16_t bitmap_rows_3210 = vpaddq_u8(bitmap_rows_32, bitmap_rows_10); + uint8x16_t bitmap_rows_7654 = vpaddq_u8(bitmap_rows_76, bitmap_rows_54); + uint8x16_t bitmap_rows_76543210 = vpaddq_u8(bitmap_rows_7654, + bitmap_rows_3210); + uint8x8_t bitmap_all = vpadd_u8(vget_low_u8(bitmap_rows_76543210), + vget_high_u8(bitmap_rows_76543210)); + + /* Shift left to remove DC bit. */ + bitmap_all = + vreinterpret_u8_u64(vshl_n_u64(vreinterpret_u64_u8(bitmap_all), 1)); + /* Count bits set (number of non-zero coefficients) in bitmap. */ + unsigned int non_zero_coefficients = vaddv_u8(vcnt_u8(bitmap_all)); + /* Move bitmap to 64-bit scalar register. */ + uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(bitmap_all), 0); + + /* Set up state and bit buffer for output bitstream. */ + working_state *state_ptr = (working_state *)state; + int free_bits = state_ptr->cur.free_bits; + size_t put_buffer = state_ptr->cur.put_buffer; + + /* Encode DC coefficient. */ + + /* For negative coeffs: diff = abs(coeff) -1 = ~abs(coeff) */ + int16x8_t abs_row0 = vabsq_s16(row0); + int16x8_t row0_lz = vclzq_s16(abs_row0); + uint16x8_t row0_mask = vshlq_u16(vcltzq_s16(row0), vnegq_s16(row0_lz)); + uint16x8_t row0_diff = veorq_u16(vreinterpretq_u16_s16(abs_row0), row0_mask); + /* Find nbits required to specify sign and amplitude of coefficient. */ + unsigned int lz = vgetq_lane_u16(vreinterpretq_u16_s16(row0_lz), 0); + unsigned int nbits = 16 - lz; + /* Emit Huffman-coded symbol and additional diff bits. */ + unsigned int diff = vgetq_lane_u16(row0_diff, 0); + PUT_CODE(dctbl->ehufco[nbits], dctbl->ehufsi[nbits], diff) + + /* Encode AC coefficients. */ + + unsigned int r = 0; /* r = run length of zeros */ + unsigned int i = 1; /* i = number of coefficients encoded */ + /* Code and size information for a run length of 16 zero coefficients */ + const unsigned int code_0xf0 = actbl->ehufco[0xf0]; + const unsigned int size_0xf0 = actbl->ehufsi[0xf0]; + + /* The most efficient method of computing nbits and diff depends on the + * number of non-zero coefficients. If the bitmap is not too sparse (> 8 + * non-zero AC coefficients), it is beneficial to do all of the work using + * Neon; else we do some of the work using Neon and the rest on demand using + * scalar code. + */ + if (non_zero_coefficients > 8) { + uint8_t block_nbits[DCTSIZE2]; + + int16x8_t abs_row1 = vabsq_s16(row1); + int16x8_t abs_row2 = vabsq_s16(row2); + int16x8_t abs_row3 = vabsq_s16(row3); + int16x8_t abs_row4 = vabsq_s16(row4); + int16x8_t abs_row5 = vabsq_s16(row5); + int16x8_t abs_row6 = vabsq_s16(row6); + int16x8_t abs_row7 = vabsq_s16(row7); + int16x8_t row1_lz = vclzq_s16(abs_row1); + int16x8_t row2_lz = vclzq_s16(abs_row2); + int16x8_t row3_lz = vclzq_s16(abs_row3); + int16x8_t row4_lz = vclzq_s16(abs_row4); + int16x8_t row5_lz = vclzq_s16(abs_row5); + int16x8_t row6_lz = vclzq_s16(abs_row6); + int16x8_t row7_lz = vclzq_s16(abs_row7); + /* Narrow leading zero count to 8 bits. */ + uint8x16_t row01_lz = vuzp1q_u8(vreinterpretq_u8_s16(row0_lz), + vreinterpretq_u8_s16(row1_lz)); + uint8x16_t row23_lz = vuzp1q_u8(vreinterpretq_u8_s16(row2_lz), + vreinterpretq_u8_s16(row3_lz)); + uint8x16_t row45_lz = vuzp1q_u8(vreinterpretq_u8_s16(row4_lz), + vreinterpretq_u8_s16(row5_lz)); + uint8x16_t row67_lz = vuzp1q_u8(vreinterpretq_u8_s16(row6_lz), + vreinterpretq_u8_s16(row7_lz)); + /* Compute nbits needed to specify magnitude of each coefficient. */ + uint8x16_t row01_nbits = vsubq_u8(vdupq_n_u8(16), row01_lz); + uint8x16_t row23_nbits = vsubq_u8(vdupq_n_u8(16), row23_lz); + uint8x16_t row45_nbits = vsubq_u8(vdupq_n_u8(16), row45_lz); + uint8x16_t row67_nbits = vsubq_u8(vdupq_n_u8(16), row67_lz); + /* Store nbits. */ + vst1q_u8(block_nbits + 0 * DCTSIZE, row01_nbits); + vst1q_u8(block_nbits + 2 * DCTSIZE, row23_nbits); + vst1q_u8(block_nbits + 4 * DCTSIZE, row45_nbits); + vst1q_u8(block_nbits + 6 * DCTSIZE, row67_nbits); + /* Mask bits not required to specify sign and amplitude of diff. */ + uint16x8_t row1_mask = vshlq_u16(vcltzq_s16(row1), vnegq_s16(row1_lz)); + uint16x8_t row2_mask = vshlq_u16(vcltzq_s16(row2), vnegq_s16(row2_lz)); + uint16x8_t row3_mask = vshlq_u16(vcltzq_s16(row3), vnegq_s16(row3_lz)); + uint16x8_t row4_mask = vshlq_u16(vcltzq_s16(row4), vnegq_s16(row4_lz)); + uint16x8_t row5_mask = vshlq_u16(vcltzq_s16(row5), vnegq_s16(row5_lz)); + uint16x8_t row6_mask = vshlq_u16(vcltzq_s16(row6), vnegq_s16(row6_lz)); + uint16x8_t row7_mask = vshlq_u16(vcltzq_s16(row7), vnegq_s16(row7_lz)); + /* diff = abs(coeff) ^ sign(coeff) [no-op for positive coefficients] */ + uint16x8_t row1_diff = veorq_u16(vreinterpretq_u16_s16(abs_row1), + row1_mask); + uint16x8_t row2_diff = veorq_u16(vreinterpretq_u16_s16(abs_row2), + row2_mask); + uint16x8_t row3_diff = veorq_u16(vreinterpretq_u16_s16(abs_row3), + row3_mask); + uint16x8_t row4_diff = veorq_u16(vreinterpretq_u16_s16(abs_row4), + row4_mask); + uint16x8_t row5_diff = veorq_u16(vreinterpretq_u16_s16(abs_row5), + row5_mask); + uint16x8_t row6_diff = veorq_u16(vreinterpretq_u16_s16(abs_row6), + row6_mask); + uint16x8_t row7_diff = veorq_u16(vreinterpretq_u16_s16(abs_row7), + row7_mask); + /* Store diff bits. */ + vst1q_u16(block_diff + 0 * DCTSIZE, row0_diff); + vst1q_u16(block_diff + 1 * DCTSIZE, row1_diff); + vst1q_u16(block_diff + 2 * DCTSIZE, row2_diff); + vst1q_u16(block_diff + 3 * DCTSIZE, row3_diff); + vst1q_u16(block_diff + 4 * DCTSIZE, row4_diff); + vst1q_u16(block_diff + 5 * DCTSIZE, row5_diff); + vst1q_u16(block_diff + 6 * DCTSIZE, row6_diff); + vst1q_u16(block_diff + 7 * DCTSIZE, row7_diff); + + while (bitmap != 0) { + r = BUILTIN_CLZLL(bitmap); + i += r; + bitmap <<= r; + nbits = block_nbits[i]; + diff = block_diff[i]; + while (r > 15) { + /* If run length > 15, emit special run-length-16 codes. */ + PUT_BITS(code_0xf0, size_0xf0) + r -= 16; + } + /* Emit Huffman symbol for run length / number of bits. (F.1.2.2.1) */ + unsigned int rs = (r << 4) + nbits; + PUT_CODE(actbl->ehufco[rs], actbl->ehufsi[rs], diff) + i++; + bitmap <<= 1; + } + } else if (bitmap != 0) { + uint16_t block_abs[DCTSIZE2]; + /* Compute and store absolute value of coefficients. */ + int16x8_t abs_row1 = vabsq_s16(row1); + int16x8_t abs_row2 = vabsq_s16(row2); + int16x8_t abs_row3 = vabsq_s16(row3); + int16x8_t abs_row4 = vabsq_s16(row4); + int16x8_t abs_row5 = vabsq_s16(row5); + int16x8_t abs_row6 = vabsq_s16(row6); + int16x8_t abs_row7 = vabsq_s16(row7); + vst1q_u16(block_abs + 0 * DCTSIZE, vreinterpretq_u16_s16(abs_row0)); + vst1q_u16(block_abs + 1 * DCTSIZE, vreinterpretq_u16_s16(abs_row1)); + vst1q_u16(block_abs + 2 * DCTSIZE, vreinterpretq_u16_s16(abs_row2)); + vst1q_u16(block_abs + 3 * DCTSIZE, vreinterpretq_u16_s16(abs_row3)); + vst1q_u16(block_abs + 4 * DCTSIZE, vreinterpretq_u16_s16(abs_row4)); + vst1q_u16(block_abs + 5 * DCTSIZE, vreinterpretq_u16_s16(abs_row5)); + vst1q_u16(block_abs + 6 * DCTSIZE, vreinterpretq_u16_s16(abs_row6)); + vst1q_u16(block_abs + 7 * DCTSIZE, vreinterpretq_u16_s16(abs_row7)); + /* Compute diff bits (without nbits mask) and store. */ + uint16x8_t row1_diff = veorq_u16(vreinterpretq_u16_s16(abs_row1), + vcltzq_s16(row1)); + uint16x8_t row2_diff = veorq_u16(vreinterpretq_u16_s16(abs_row2), + vcltzq_s16(row2)); + uint16x8_t row3_diff = veorq_u16(vreinterpretq_u16_s16(abs_row3), + vcltzq_s16(row3)); + uint16x8_t row4_diff = veorq_u16(vreinterpretq_u16_s16(abs_row4), + vcltzq_s16(row4)); + uint16x8_t row5_diff = veorq_u16(vreinterpretq_u16_s16(abs_row5), + vcltzq_s16(row5)); + uint16x8_t row6_diff = veorq_u16(vreinterpretq_u16_s16(abs_row6), + vcltzq_s16(row6)); + uint16x8_t row7_diff = veorq_u16(vreinterpretq_u16_s16(abs_row7), + vcltzq_s16(row7)); + vst1q_u16(block_diff + 0 * DCTSIZE, row0_diff); + vst1q_u16(block_diff + 1 * DCTSIZE, row1_diff); + vst1q_u16(block_diff + 2 * DCTSIZE, row2_diff); + vst1q_u16(block_diff + 3 * DCTSIZE, row3_diff); + vst1q_u16(block_diff + 4 * DCTSIZE, row4_diff); + vst1q_u16(block_diff + 5 * DCTSIZE, row5_diff); + vst1q_u16(block_diff + 6 * DCTSIZE, row6_diff); + vst1q_u16(block_diff + 7 * DCTSIZE, row7_diff); + + /* Same as above but must mask diff bits and compute nbits on demand. */ + while (bitmap != 0) { + r = BUILTIN_CLZLL(bitmap); + i += r; + bitmap <<= r; + lz = BUILTIN_CLZ(block_abs[i]); + nbits = 32 - lz; + diff = ((unsigned int)block_diff[i] << lz) >> lz; + while (r > 15) { + /* If run length > 15, emit special run-length-16 codes. */ + PUT_BITS(code_0xf0, size_0xf0) + r -= 16; + } + /* Emit Huffman symbol for run length / number of bits. (F.1.2.2.1) */ + unsigned int rs = (r << 4) + nbits; + PUT_CODE(actbl->ehufco[rs], actbl->ehufsi[rs], diff) + i++; + bitmap <<= 1; + } + } + + /* If the last coefficient(s) were zero, emit an end-of-block (EOB) code. + * The value of RS for the EOB code is 0. + */ + if (i != 64) { + PUT_BITS(actbl->ehufco[0], actbl->ehufsi[0]) + } + + state_ptr->cur.put_buffer = put_buffer; + state_ptr->cur.free_bits = free_bits; + + return buffer; +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm64/jsimd.c b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jsimd.c similarity index 67% rename from third-party/mozjpeg/mozjpeg/simd/arm64/jsimd.c rename to third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jsimd.c index 0e6c7b9c3fc..358e1597b16 100644 --- a/third-party/mozjpeg/mozjpeg/simd/arm64/jsimd.c +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jsimd.c @@ -3,8 +3,9 @@ * * Copyright 2009 Pierre Ossman for Cendio AB * Copyright (C) 2011, Nokia Corporation and/or its subsidiary(-ies). - * Copyright (C) 2009-2011, 2013-2014, 2016, 2018, D. R. Commander. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2009-2011, 2013-2014, 2016, 2018, 2020, 2022, D. R. Commander. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. + * Copyright (C) 2020, Arm Limited. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -12,29 +13,27 @@ * * This file contains the interface between the "normal" portions * of the library and the SIMD implementations when running on a - * 64-bit ARM architecture. + * 64-bit Arm architecture. */ #define JPEG_INTERNALS -#include "../../jinclude.h" -#include "../../jpeglib.h" +#include "../../../jinclude.h" +#include "../../../jpeglib.h" +#include "../../../jsimd.h" +#include "../../../jdct.h" +#include "../../../jsimddct.h" #include "../../jsimd.h" -#include "../../jdct.h" -#include "../../jsimddct.h" -#include "../jsimd.h" -#include -#include #include #define JSIMD_FASTLD3 1 #define JSIMD_FASTST3 2 #define JSIMD_FASTTBL 4 -static unsigned int simd_support = ~0; -static unsigned int simd_huffman = 1; -static unsigned int simd_features = JSIMD_FASTLD3 | JSIMD_FASTST3 | - JSIMD_FASTTBL; +static THREAD_LOCAL unsigned int simd_support = ~0; +static THREAD_LOCAL unsigned int simd_huffman = 1; +static THREAD_LOCAL unsigned int simd_features = JSIMD_FASTLD3 | + JSIMD_FASTST3 | JSIMD_FASTTBL; #if defined(__linux__) || defined(ANDROID) || defined(__ANDROID__) @@ -109,13 +108,11 @@ parse_proc_cpuinfo(int bufsize) /* * Check what SIMD accelerations are supported. - * - * FIXME: This code is racy under a multi-threaded environment. */ /* - * ARMv8 architectures support NEON extensions by default. - * It is no longer optional as it was with ARMv7. + * Armv8 architectures support Neon extensions by default. + * It is no longer optional as it was with Armv7. */ @@ -123,7 +120,7 @@ LOCAL(void) init_simd(void) { #ifndef NO_GETENV - char *env = NULL; + char env[2] = { 0 }; #endif #if defined(__linux__) || defined(ANDROID) || defined(__ANDROID__) int bufsize = 1024; /* an initial guess for the line buffer size limit */ @@ -145,24 +142,19 @@ init_simd(void) #ifndef NO_GETENV /* Force different settings through environment variables */ - env = getenv("JSIMD_FORCENEON"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCENEON") && !strcmp(env, "1")) simd_support = JSIMD_NEON; - env = getenv("JSIMD_FORCENONE"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCENONE") && !strcmp(env, "1")) simd_support = 0; - env = getenv("JSIMD_NOHUFFENC"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_NOHUFFENC") && !strcmp(env, "1")) simd_huffman = 0; - env = getenv("JSIMD_FASTLD3"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FASTLD3") && !strcmp(env, "1")) simd_features |= JSIMD_FASTLD3; - if ((env != NULL) && (strcmp(env, "0") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FASTLD3") && !strcmp(env, "0")) simd_features &= ~JSIMD_FASTLD3; - env = getenv("JSIMD_FASTST3"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FASTST3") && !strcmp(env, "1")) simd_features |= JSIMD_FASTST3; - if ((env != NULL) && (strcmp(env, "0") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FASTST3") && !strcmp(env, "0")) simd_features &= ~JSIMD_FASTST3; #endif } @@ -189,6 +181,19 @@ jsimd_can_rgb_ycc(void) GLOBAL(int) jsimd_can_rgb_gray(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + if ((RGB_PIXELSIZE != 3) && (RGB_PIXELSIZE != 4)) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -237,20 +242,28 @@ jsimd_rgb_ycc_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, switch (cinfo->in_color_space) { case JCS_EXT_RGB: +#ifndef NEON_INTRINSICS if (simd_features & JSIMD_FASTLD3) +#endif neonfct = jsimd_extrgb_ycc_convert_neon; +#ifndef NEON_INTRINSICS else neonfct = jsimd_extrgb_ycc_convert_neon_slowld3; +#endif break; case JCS_EXT_RGBX: case JCS_EXT_RGBA: neonfct = jsimd_extrgbx_ycc_convert_neon; break; case JCS_EXT_BGR: +#ifndef NEON_INTRINSICS if (simd_features & JSIMD_FASTLD3) +#endif neonfct = jsimd_extbgr_ycc_convert_neon; +#ifndef NEON_INTRINSICS else neonfct = jsimd_extbgr_ycc_convert_neon_slowld3; +#endif break; case JCS_EXT_BGRX: case JCS_EXT_BGRA: @@ -265,10 +278,14 @@ jsimd_rgb_ycc_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, neonfct = jsimd_extxrgb_ycc_convert_neon; break; default: +#ifndef NEON_INTRINSICS if (simd_features & JSIMD_FASTLD3) +#endif neonfct = jsimd_extrgb_ycc_convert_neon; +#ifndef NEON_INTRINSICS else neonfct = jsimd_extrgb_ycc_convert_neon_slowld3; +#endif break; } @@ -280,6 +297,37 @@ jsimd_rgb_gray_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows) { + void (*neonfct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); + + switch (cinfo->in_color_space) { + case JCS_EXT_RGB: + neonfct = jsimd_extrgb_gray_convert_neon; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + neonfct = jsimd_extrgbx_gray_convert_neon; + break; + case JCS_EXT_BGR: + neonfct = jsimd_extbgr_gray_convert_neon; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + neonfct = jsimd_extbgrx_gray_convert_neon; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + neonfct = jsimd_extxbgr_gray_convert_neon; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + neonfct = jsimd_extxrgb_gray_convert_neon; + break; + default: + neonfct = jsimd_extrgb_gray_convert_neon; + break; + } + + neonfct(cinfo->image_width, input_buf, output_buf, output_row, num_rows); } GLOBAL(void) @@ -291,20 +339,28 @@ jsimd_ycc_rgb_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, switch (cinfo->out_color_space) { case JCS_EXT_RGB: +#ifndef NEON_INTRINSICS if (simd_features & JSIMD_FASTST3) +#endif neonfct = jsimd_ycc_extrgb_convert_neon; +#ifndef NEON_INTRINSICS else neonfct = jsimd_ycc_extrgb_convert_neon_slowst3; +#endif break; case JCS_EXT_RGBX: case JCS_EXT_RGBA: neonfct = jsimd_ycc_extrgbx_convert_neon; break; case JCS_EXT_BGR: +#ifndef NEON_INTRINSICS if (simd_features & JSIMD_FASTST3) +#endif neonfct = jsimd_ycc_extbgr_convert_neon; +#ifndef NEON_INTRINSICS else neonfct = jsimd_ycc_extbgr_convert_neon_slowst3; +#endif break; case JCS_EXT_BGRX: case JCS_EXT_BGRA: @@ -319,10 +375,14 @@ jsimd_ycc_rgb_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, neonfct = jsimd_ycc_extxrgb_convert_neon; break; default: +#ifndef NEON_INTRINSICS if (simd_features & JSIMD_FASTST3) +#endif neonfct = jsimd_ycc_extrgb_convert_neon; +#ifndef NEON_INTRINSICS else neonfct = jsimd_ycc_extrgb_convert_neon_slowst3; +#endif break; } @@ -397,12 +457,33 @@ jsimd_h2v1_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, GLOBAL(int) jsimd_can_h2v2_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_can_h2v1_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -410,23 +491,66 @@ GLOBAL(void) jsimd_h2v2_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v2_upsample_neon(cinfo->max_v_samp_factor, cinfo->output_width, + input_data, output_data_ptr); } GLOBAL(void) jsimd_h2v1_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v1_upsample_neon(cinfo->max_v_samp_factor, cinfo->output_width, + input_data, output_data_ptr); } GLOBAL(int) jsimd_can_h2v2_fancy_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_can_h2v1_fancy_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + + return 0; +} + +GLOBAL(int) +jsimd_can_h1v2_fancy_upsample(void) +{ + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -434,23 +558,60 @@ GLOBAL(void) jsimd_h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v2_fancy_upsample_neon(cinfo->max_v_samp_factor, + compptr->downsampled_width, input_data, + output_data_ptr); } GLOBAL(void) jsimd_h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v1_fancy_upsample_neon(cinfo->max_v_samp_factor, + compptr->downsampled_width, input_data, + output_data_ptr); +} + +GLOBAL(void) +jsimd_h1v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, + JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) +{ + jsimd_h1v2_fancy_upsample_neon(cinfo->max_v_samp_factor, + compptr->downsampled_width, input_data, + output_data_ptr); } GLOBAL(int) jsimd_can_h2v2_merged_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_can_h2v1_merged_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } @@ -458,12 +619,74 @@ GLOBAL(void) jsimd_h2v2_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { + void (*neonfct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + + switch (cinfo->out_color_space) { + case JCS_EXT_RGB: + neonfct = jsimd_h2v2_extrgb_merged_upsample_neon; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + neonfct = jsimd_h2v2_extrgbx_merged_upsample_neon; + break; + case JCS_EXT_BGR: + neonfct = jsimd_h2v2_extbgr_merged_upsample_neon; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + neonfct = jsimd_h2v2_extbgrx_merged_upsample_neon; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + neonfct = jsimd_h2v2_extxbgr_merged_upsample_neon; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + neonfct = jsimd_h2v2_extxrgb_merged_upsample_neon; + break; + default: + neonfct = jsimd_h2v2_extrgb_merged_upsample_neon; + break; + } + + neonfct(cinfo->output_width, input_buf, in_row_group_ctr, output_buf); } GLOBAL(void) jsimd_h2v1_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { + void (*neonfct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + + switch (cinfo->out_color_space) { + case JCS_EXT_RGB: + neonfct = jsimd_h2v1_extrgb_merged_upsample_neon; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + neonfct = jsimd_h2v1_extrgbx_merged_upsample_neon; + break; + case JCS_EXT_BGR: + neonfct = jsimd_h2v1_extbgr_merged_upsample_neon; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + neonfct = jsimd_h2v1_extbgrx_merged_upsample_neon; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + neonfct = jsimd_h2v1_extxbgr_merged_upsample_neon; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + neonfct = jsimd_h2v1_extxrgb_merged_upsample_neon; + break; + default: + neonfct = jsimd_h2v1_extrgb_merged_upsample_neon; + break; + } + + neonfct(cinfo->output_width, input_buf, in_row_group_ctr, output_buf); } GLOBAL(int) @@ -762,37 +985,69 @@ jsimd_huff_encode_one_block(void *state, JOCTET *buffer, JCOEFPTR block, int last_dc_val, c_derived_tbl *dctbl, c_derived_tbl *actbl) { +#ifndef NEON_INTRINSICS if (simd_features & JSIMD_FASTTBL) +#endif return jsimd_huff_encode_one_block_neon(state, buffer, block, last_dc_val, dctbl, actbl); +#ifndef NEON_INTRINSICS else return jsimd_huff_encode_one_block_neon_slowtbl(state, buffer, block, last_dc_val, dctbl, actbl); +#endif } GLOBAL(int) jsimd_can_encode_mcu_AC_first_prepare(void) { + init_simd(); + + if (DCTSIZE != 8) + return 0; + if (sizeof(JCOEF) != 2) + return 0; + if (SIZEOF_SIZE_T != 8) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { + jsimd_encode_mcu_AC_first_prepare_neon(block, jpeg_natural_order_start, + Sl, Al, values, zerobits); } GLOBAL(int) jsimd_can_encode_mcu_AC_refine_prepare(void) { + init_simd(); + + if (DCTSIZE != 8) + return 0; + if (sizeof(JCOEF) != 2) + return 0; + if (SIZEOF_SIZE_T != 8) + return 0; + + if (simd_support & JSIMD_NEON) + return 1; + return 0; } GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { - return 0; + return jsimd_encode_mcu_AC_refine_prepare_neon(block, + jpeg_natural_order_start, + Sl, Al, absvalues, bits); } diff --git a/third-party/mozjpeg/mozjpeg/simd/arm64/jsimd_neon.S b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jsimd_neon.S similarity index 65% rename from third-party/mozjpeg/mozjpeg/simd/arm64/jsimd_neon.S rename to third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jsimd_neon.S index a3aa4066c63..738a4f0658d 100644 --- a/third-party/mozjpeg/mozjpeg/simd/arm64/jsimd_neon.S +++ b/third-party/mozjpeg/mozjpeg/simd/arm/aarch64/jsimd_neon.S @@ -1,12 +1,12 @@ /* - * ARMv8 NEON optimizations for libjpeg-turbo + * Armv8 Neon optimizations for libjpeg-turbo * * Copyright (C) 2009-2011, Nokia Corporation and/or its subsidiary(-ies). * All Rights Reserved. * Author: Siarhei Siamashka * Copyright (C) 2013-2014, Linaro Limited. All Rights Reserved. * Author: Ragesh Radhakrishnan - * Copyright (C) 2014-2016, D. R. Commander. All Rights Reserved. + * Copyright (C) 2014-2016, 2020, D. R. Commander. All Rights Reserved. * Copyright (C) 2015-2016, 2018, Matthieu Darbois. All Rights Reserved. * Copyright (C) 2016, Siarhei Siamashka. All Rights Reserved. * @@ -33,6 +33,8 @@ #if defined(__APPLE__) .section __DATA, __const +#elif defined(_WIN32) +.section .rdata #else .section .rodata, "a", %progbits #endif @@ -84,56 +86,6 @@ Ljsimd_idct_islow_neon_consts: #undef F_2_562 #undef F_3_072 -/* Constants for jsimd_idct_ifast_neon() */ - -.balign 16 -Ljsimd_idct_ifast_neon_consts: - .short (277 * 128 - 256 * 128) /* XFIX_1_082392200 */ - .short (362 * 128 - 256 * 128) /* XFIX_1_414213562 */ - .short (473 * 128 - 256 * 128) /* XFIX_1_847759065 */ - .short (669 * 128 - 512 * 128) /* XFIX_2_613125930 */ - -/* Constants for jsimd_idct_4x4_neon() and jsimd_idct_2x2_neon() */ - -#define CONST_BITS 13 - -#define FIX_0_211164243 (1730) /* FIX(0.211164243) */ -#define FIX_0_509795579 (4176) /* FIX(0.509795579) */ -#define FIX_0_601344887 (4926) /* FIX(0.601344887) */ -#define FIX_0_720959822 (5906) /* FIX(0.720959822) */ -#define FIX_0_765366865 (6270) /* FIX(0.765366865) */ -#define FIX_0_850430095 (6967) /* FIX(0.850430095) */ -#define FIX_0_899976223 (7373) /* FIX(0.899976223) */ -#define FIX_1_061594337 (8697) /* FIX(1.061594337) */ -#define FIX_1_272758580 (10426) /* FIX(1.272758580) */ -#define FIX_1_451774981 (11893) /* FIX(1.451774981) */ -#define FIX_1_847759065 (15137) /* FIX(1.847759065) */ -#define FIX_2_172734803 (17799) /* FIX(2.172734803) */ -#define FIX_2_562915447 (20995) /* FIX(2.562915447) */ -#define FIX_3_624509785 (29692) /* FIX(3.624509785) */ - -.balign 16 -Ljsimd_idct_4x4_neon_consts: - .short FIX_1_847759065 /* v0.h[0] */ - .short -FIX_0_765366865 /* v0.h[1] */ - .short -FIX_0_211164243 /* v0.h[2] */ - .short FIX_1_451774981 /* v0.h[3] */ - .short -FIX_2_172734803 /* d1[0] */ - .short FIX_1_061594337 /* d1[1] */ - .short -FIX_0_509795579 /* d1[2] */ - .short -FIX_0_601344887 /* d1[3] */ - .short FIX_0_899976223 /* v2.h[0] */ - .short FIX_2_562915447 /* v2.h[1] */ - .short 1 << (CONST_BITS + 1) /* v2.h[2] */ - .short 0 /* v2.h[3] */ - -.balign 8 -Ljsimd_idct_2x2_neon_consts: - .short -FIX_0_720959822 /* v14[0] */ - .short FIX_0_850430095 /* v14[1] */ - .short -FIX_1_272758580 /* v14[2] */ - .short FIX_3_624509785 /* v14[3] */ - /* Constants for jsimd_ycc_*_neon() */ .balign 16 @@ -199,52 +151,6 @@ Ljsimd_fdct_islow_neon_consts: #undef F_2_562 #undef F_3_072 -/* Constants for jsimd_fdct_ifast_neon() */ - -.balign 16 -Ljsimd_fdct_ifast_neon_consts: - .short (98 * 128) /* XFIX_0_382683433 */ - .short (139 * 128) /* XFIX_0_541196100 */ - .short (181 * 128) /* XFIX_0_707106781 */ - .short (334 * 128 - 256 * 128) /* XFIX_1_306562965 */ - -/* Constants for jsimd_h2*_downsample_neon() */ - -.balign 16 -Ljsimd_h2_downsample_neon_consts: - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F /* diff 0 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0E /* diff 1 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0D, 0x0D /* diff 2 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0C, 0x0C, 0x0C /* diff 3 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x09, 0x0A, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B /* diff 4 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A /* diff 5 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09 /* diff 6 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08 /* diff 7 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, \ - 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07 /* diff 8 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x06, \ - 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06 /* diff 9 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x05, 0x05, \ - 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05 /* diff 10 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x04, 0x04, 0x04, \ - 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 /* diff 11 */ - .byte 0x00, 0x01, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, \ - 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03 /* diff 12 */ - .byte 0x00, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, \ - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02 /* diff 13 */ - .byte 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, \ - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 /* diff 14 */ - .byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* diff 15 */ - /* Constants for jsimd_huff_encode_one_block_neon() */ .balign 16 @@ -279,9 +185,6 @@ Ljsimd_huff_encode_one_block_neon_consts: .text -#define RESPECT_STRICT_ALIGNMENT 1 - - /*****************************************************************************/ /* Supplementary macro for setting function attributes */ @@ -311,45 +214,6 @@ _\fname: #endif .endm -/* Transpose elements of single 128 bit registers */ -.macro transpose_single x0, x1, xi, xilen, literal - ins \xi\xilen[0], \x0\xilen[0] - ins \x1\xilen[0], \x0\xilen[1] - trn1 \x0\literal, \x0\literal, \x1\literal - trn2 \x1\literal, \xi\literal, \x1\literal -.endm - -/* Transpose elements of 2 different registers */ -.macro transpose x0, x1, xi, xilen, literal - mov \xi\xilen, \x0\xilen - trn1 \x0\literal, \x0\literal, \x1\literal - trn2 \x1\literal, \xi\literal, \x1\literal -.endm - -/* Transpose a block of 4x4 coefficients in four 64-bit registers */ -.macro transpose_4x4_32 x0, x0len, x1, x1len, x2, x2len, x3, x3len, xi, xilen - mov \xi\xilen, \x0\xilen - trn1 \x0\x0len, \x0\x0len, \x2\x2len - trn2 \x2\x2len, \xi\x0len, \x2\x2len - mov \xi\xilen, \x1\xilen - trn1 \x1\x1len, \x1\x1len, \x3\x3len - trn2 \x3\x3len, \xi\x1len, \x3\x3len -.endm - -.macro transpose_4x4_16 x0, x0len, x1, x1len, x2, x2len, x3, x3len, xi, xilen - mov \xi\xilen, \x0\xilen - trn1 \x0\x0len, \x0\x0len, \x1\x1len - trn2 \x1\x2len, \xi\x0len, \x1\x2len - mov \xi\xilen, \x2\xilen - trn1 \x2\x2len, \x2\x2len, \x3\x3len - trn2 \x3\x2len, \xi\x1len, \x3\x3len -.endm - -.macro transpose_4x4 x0, x1, x2, x3, x5 - transpose_4x4_16 \x0, .4h, \x1, .4h, \x2, .4h, \x3, .4h, \x5, .16b - transpose_4x4_32 \x0, .2s, \x1, .2s, \x2, .2s, \x3, .2s, \x5, .16b -.endm - .macro transpose_8x8 l0, l1, l2, l3, l4, l5, l6, l7, t0, t1, t2, t3 trn1 \t0\().8h, \l0\().8h, \l1\().8h trn1 \t1\().8h, \l2\().8h, \l3\().8h @@ -609,23 +473,23 @@ asm_function jsimd_idct_islow_neon shrn2 v5.8h, v15.4s, #16 /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS+PASS1_BITS+3) */ shrn2 v6.8h, v17.4s, #16 /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS+PASS1_BITS+3) */ movi v0.16b, #(CENTERJSAMPLE) - /* Prepare pointers (dual-issue with NEON instructions) */ + /* Prepare pointers (dual-issue with Neon instructions) */ ldp TMP1, TMP2, [OUTPUT_BUF], 16 - sqrshrn v28.8b, v2.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn v28.8b, v2.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) ldp TMP3, TMP4, [OUTPUT_BUF], 16 - sqrshrn v29.8b, v3.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn v29.8b, v3.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) add TMP1, TMP1, OUTPUT_COL - sqrshrn v30.8b, v4.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn v30.8b, v4.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) add TMP2, TMP2, OUTPUT_COL - sqrshrn v31.8b, v5.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn v31.8b, v5.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) add TMP3, TMP3, OUTPUT_COL - sqrshrn2 v28.16b, v6.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn2 v28.16b, v6.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) add TMP4, TMP4, OUTPUT_COL - sqrshrn2 v29.16b, v7.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn2 v29.16b, v7.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) ldp TMP5, TMP6, [OUTPUT_BUF], 16 - sqrshrn2 v30.16b, v8.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn2 v30.16b, v8.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) ldp TMP7, TMP8, [OUTPUT_BUF], 16 - sqrshrn2 v31.16b, v9.8h, #(CONST_BITS+PASS1_BITS+3-16) + sqrshrn2 v31.16b, v9.8h, #(CONST_BITS + PASS1_BITS + 3 - 16) add TMP5, TMP5, OUTPUT_COL add v16.16b, v28.16b, v0.16b add TMP6, TMP6, OUTPUT_COL @@ -737,14 +601,14 @@ asm_function jsimd_idct_islow_neon add v14.4s, v6.4s, v10.4s /* tmp13 + tmp0 */ sub v16.4s, v6.4s, v10.4s /* tmp13 - tmp0 */ - rshrn v2.4h, v18.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ - rshrn v3.4h, v22.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ - rshrn v4.4h, v26.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ - rshrn v5.4h, v14.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ - rshrn2 v2.8h, v16.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ - rshrn2 v3.8h, v28.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ - rshrn2 v4.8h, v24.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ - rshrn2 v5.8h, v20.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ + rshrn v2.4h, v18.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ + rshrn v3.4h, v22.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ + rshrn v4.4h, v26.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ + rshrn v5.4h, v14.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ + rshrn2 v2.8h, v16.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ + rshrn2 v3.8h, v28.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ + rshrn2 v4.8h, v24.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ + rshrn2 v5.8h, v20.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ mov v6.16b, v15.16b mov v7.16b, v15.16b mov v8.16b, v15.16b @@ -821,14 +685,14 @@ asm_function jsimd_idct_islow_neon mov v3.16b, v14.16b mov v4.16b, v14.16b mov v5.16b, v14.16b - rshrn v6.4h, v19.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ - rshrn v7.4h, v23.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ - rshrn v8.4h, v27.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ - rshrn v9.4h, v15.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ - rshrn2 v6.8h, v17.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ - rshrn2 v7.8h, v29.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ - rshrn2 v8.8h, v25.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ - rshrn2 v9.8h, v21.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ + rshrn v6.4h, v19.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ + rshrn v7.4h, v23.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ + rshrn v8.4h, v27.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ + rshrn v9.4h, v15.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ + rshrn2 v6.8h, v17.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ + rshrn2 v7.8h, v29.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ + rshrn2 v8.8h, v25.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ + rshrn2 v9.8h, v21.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ b 1b .balign 16 @@ -931,22 +795,22 @@ asm_function jsimd_idct_islow_neon sub v16.4s, v6.4s, v10.4s /* tmp13 - tmp0 */ sub v17.4s, v31.4s, v11.4s /* tmp13 - tmp0 */ - rshrn v2.4h, v18.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ - rshrn v3.4h, v22.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ - rshrn v4.4h, v26.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ - rshrn v5.4h, v14.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ - rshrn v6.4h, v19.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ - rshrn v7.4h, v23.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ - rshrn v8.4h, v27.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ - rshrn v9.4h, v15.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ - rshrn2 v2.8h, v16.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ - rshrn2 v3.8h, v28.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ - rshrn2 v4.8h, v24.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ - rshrn2 v5.8h, v20.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ - rshrn2 v6.8h, v17.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ - rshrn2 v7.8h, v29.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ - rshrn2 v8.8h, v25.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ - rshrn2 v9.8h, v21.4s, #(CONST_BITS-PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ + rshrn v2.4h, v18.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ + rshrn v3.4h, v22.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ + rshrn v4.4h, v26.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ + rshrn v5.4h, v14.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ + rshrn v6.4h, v19.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*0] = (int)DESCALE(tmp10 + tmp3, CONST_BITS-PASS1_BITS) */ + rshrn v7.4h, v23.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*1] = (int)DESCALE(tmp11 + tmp2, CONST_BITS-PASS1_BITS) */ + rshrn v8.4h, v27.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*2] = (int)DESCALE(tmp12 + tmp1, CONST_BITS-PASS1_BITS) */ + rshrn v9.4h, v15.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*3] = (int)DESCALE(tmp13 + tmp0, CONST_BITS-PASS1_BITS) */ + rshrn2 v2.8h, v16.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ + rshrn2 v3.8h, v28.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ + rshrn2 v4.8h, v24.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ + rshrn2 v5.8h, v20.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ + rshrn2 v6.8h, v17.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*4] = (int)DESCALE(tmp13 - tmp0, CONST_BITS-PASS1_BITS) */ + rshrn2 v7.8h, v29.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*5] = (int)DESCALE(tmp12 - tmp1, CONST_BITS-PASS1_BITS) */ + rshrn2 v8.8h, v25.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*6] = (int)DESCALE(tmp11 - tmp2, CONST_BITS-PASS1_BITS) */ + rshrn2 v9.8h, v21.4s, #(CONST_BITS - PASS1_BITS) /* wsptr[DCTSIZE*7] = (int)DESCALE(tmp10 - tmp3, CONST_BITS-PASS1_BITS) */ b 1b .unreq DCT_TABLE @@ -979,619 +843,6 @@ asm_function jsimd_idct_islow_neon #undef XFIX_P_3_072 -/*****************************************************************************/ - -/* - * jsimd_idct_ifast_neon - * - * This function contains a fast, not so accurate integer implementation of - * the inverse DCT (Discrete Cosine Transform). It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_idct_ifast' - * function from jidctfst.c - * - * Normally 1-D AAN DCT needs 5 multiplications and 29 additions. - * But in ARM NEON case some extra additions are required because VQDMULH - * instruction can't handle the constants larger than 1. So the expressions - * like "x * 1.082392200" have to be converted to "x * 0.082392200 + x", - * which introduces an extra addition. Overall, there are 6 extra additions - * per 1-D IDCT pass, totalling to 5 VQDMULH and 35 VADD/VSUB instructions. - */ - -#define XFIX_1_082392200 v0.h[0] -#define XFIX_1_414213562 v0.h[1] -#define XFIX_1_847759065 v0.h[2] -#define XFIX_2_613125930 v0.h[3] - -asm_function jsimd_idct_ifast_neon - - DCT_TABLE .req x0 - COEF_BLOCK .req x1 - OUTPUT_BUF .req x2 - OUTPUT_COL .req x3 - TMP1 .req x0 - TMP2 .req x1 - TMP3 .req x9 - TMP4 .req x10 - TMP5 .req x11 - TMP6 .req x12 - TMP7 .req x13 - TMP8 .req x14 - - /* OUTPUT_COL is a JDIMENSION (unsigned int) argument, so the ABI doesn't - guarantee that the upper (unused) 32 bits of x3 are valid. This - instruction ensures that those bits are set to zero. */ - uxtw x3, w3 - - /* Load and dequantize coefficients into NEON registers - * with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | d16 | d17 ( v16.8h ) - * 1 | d18 | d19 ( v17.8h ) - * 2 | d20 | d21 ( v18.8h ) - * 3 | d22 | d23 ( v19.8h ) - * 4 | d24 | d25 ( v20.8h ) - * 5 | d26 | d27 ( v21.8h ) - * 6 | d28 | d29 ( v22.8h ) - * 7 | d30 | d31 ( v23.8h ) - */ - /* Save NEON registers used in fast IDCT */ - get_symbol_loc TMP5, Ljsimd_idct_ifast_neon_consts - ld1 {v16.8h, v17.8h}, [COEF_BLOCK], 32 - ld1 {v0.8h, v1.8h}, [DCT_TABLE], 32 - ld1 {v18.8h, v19.8h}, [COEF_BLOCK], 32 - mul v16.8h, v16.8h, v0.8h - ld1 {v2.8h, v3.8h}, [DCT_TABLE], 32 - mul v17.8h, v17.8h, v1.8h - ld1 {v20.8h, v21.8h}, [COEF_BLOCK], 32 - mul v18.8h, v18.8h, v2.8h - ld1 {v0.8h, v1.8h}, [DCT_TABLE], 32 - mul v19.8h, v19.8h, v3.8h - ld1 {v22.8h, v23.8h}, [COEF_BLOCK], 32 - mul v20.8h, v20.8h, v0.8h - ld1 {v2.8h, v3.8h}, [DCT_TABLE], 32 - mul v22.8h, v22.8h, v2.8h - mul v21.8h, v21.8h, v1.8h - ld1 {v0.4h}, [TMP5] /* load constants */ - mul v23.8h, v23.8h, v3.8h - - /* 1-D IDCT, pass 1 */ - sub v2.8h, v18.8h, v22.8h - add v22.8h, v18.8h, v22.8h - sub v1.8h, v19.8h, v21.8h - add v21.8h, v19.8h, v21.8h - sub v5.8h, v17.8h, v23.8h - add v23.8h, v17.8h, v23.8h - sqdmulh v4.8h, v2.8h, XFIX_1_414213562 - sqdmulh v6.8h, v1.8h, XFIX_2_613125930 - add v3.8h, v1.8h, v1.8h - sub v1.8h, v5.8h, v1.8h - add v18.8h, v2.8h, v4.8h - sqdmulh v4.8h, v1.8h, XFIX_1_847759065 - sub v2.8h, v23.8h, v21.8h - add v3.8h, v3.8h, v6.8h - sqdmulh v6.8h, v2.8h, XFIX_1_414213562 - add v1.8h, v1.8h, v4.8h - sqdmulh v4.8h, v5.8h, XFIX_1_082392200 - sub v18.8h, v18.8h, v22.8h - add v2.8h, v2.8h, v6.8h - sub v6.8h, v16.8h, v20.8h - add v20.8h, v16.8h, v20.8h - add v17.8h, v5.8h, v4.8h - add v5.8h, v6.8h, v18.8h - sub v18.8h, v6.8h, v18.8h - add v6.8h, v23.8h, v21.8h - add v16.8h, v20.8h, v22.8h - sub v3.8h, v6.8h, v3.8h - sub v20.8h, v20.8h, v22.8h - sub v3.8h, v3.8h, v1.8h - sub v1.8h, v17.8h, v1.8h - add v2.8h, v3.8h, v2.8h - sub v23.8h, v16.8h, v6.8h - add v1.8h, v1.8h, v2.8h - add v16.8h, v16.8h, v6.8h - add v22.8h, v5.8h, v3.8h - sub v17.8h, v5.8h, v3.8h - sub v21.8h, v18.8h, v2.8h - add v18.8h, v18.8h, v2.8h - sub v19.8h, v20.8h, v1.8h - add v20.8h, v20.8h, v1.8h - transpose_8x8 v16, v17, v18, v19, v20, v21, v22, v23, v28, v29, v30, v31 - /* 1-D IDCT, pass 2 */ - sub v2.8h, v18.8h, v22.8h - add v22.8h, v18.8h, v22.8h - sub v1.8h, v19.8h, v21.8h - add v21.8h, v19.8h, v21.8h - sub v5.8h, v17.8h, v23.8h - add v23.8h, v17.8h, v23.8h - sqdmulh v4.8h, v2.8h, XFIX_1_414213562 - sqdmulh v6.8h, v1.8h, XFIX_2_613125930 - add v3.8h, v1.8h, v1.8h - sub v1.8h, v5.8h, v1.8h - add v18.8h, v2.8h, v4.8h - sqdmulh v4.8h, v1.8h, XFIX_1_847759065 - sub v2.8h, v23.8h, v21.8h - add v3.8h, v3.8h, v6.8h - sqdmulh v6.8h, v2.8h, XFIX_1_414213562 - add v1.8h, v1.8h, v4.8h - sqdmulh v4.8h, v5.8h, XFIX_1_082392200 - sub v18.8h, v18.8h, v22.8h - add v2.8h, v2.8h, v6.8h - sub v6.8h, v16.8h, v20.8h - add v20.8h, v16.8h, v20.8h - add v17.8h, v5.8h, v4.8h - add v5.8h, v6.8h, v18.8h - sub v18.8h, v6.8h, v18.8h - add v6.8h, v23.8h, v21.8h - add v16.8h, v20.8h, v22.8h - sub v3.8h, v6.8h, v3.8h - sub v20.8h, v20.8h, v22.8h - sub v3.8h, v3.8h, v1.8h - sub v1.8h, v17.8h, v1.8h - add v2.8h, v3.8h, v2.8h - sub v23.8h, v16.8h, v6.8h - add v1.8h, v1.8h, v2.8h - add v16.8h, v16.8h, v6.8h - add v22.8h, v5.8h, v3.8h - sub v17.8h, v5.8h, v3.8h - sub v21.8h, v18.8h, v2.8h - add v18.8h, v18.8h, v2.8h - sub v19.8h, v20.8h, v1.8h - add v20.8h, v20.8h, v1.8h - /* Descale to 8-bit and range limit */ - movi v0.16b, #0x80 - /* Prepare pointers (dual-issue with NEON instructions) */ - ldp TMP1, TMP2, [OUTPUT_BUF], 16 - sqshrn v28.8b, v16.8h, #5 - ldp TMP3, TMP4, [OUTPUT_BUF], 16 - sqshrn v29.8b, v17.8h, #5 - add TMP1, TMP1, OUTPUT_COL - sqshrn v30.8b, v18.8h, #5 - add TMP2, TMP2, OUTPUT_COL - sqshrn v31.8b, v19.8h, #5 - add TMP3, TMP3, OUTPUT_COL - sqshrn2 v28.16b, v20.8h, #5 - add TMP4, TMP4, OUTPUT_COL - sqshrn2 v29.16b, v21.8h, #5 - ldp TMP5, TMP6, [OUTPUT_BUF], 16 - sqshrn2 v30.16b, v22.8h, #5 - ldp TMP7, TMP8, [OUTPUT_BUF], 16 - sqshrn2 v31.16b, v23.8h, #5 - add TMP5, TMP5, OUTPUT_COL - add v16.16b, v28.16b, v0.16b - add TMP6, TMP6, OUTPUT_COL - add v18.16b, v29.16b, v0.16b - add TMP7, TMP7, OUTPUT_COL - add v20.16b, v30.16b, v0.16b - add TMP8, TMP8, OUTPUT_COL - add v22.16b, v31.16b, v0.16b - - /* Transpose the final 8-bit samples */ - trn1 v28.16b, v16.16b, v18.16b - trn1 v30.16b, v20.16b, v22.16b - trn2 v29.16b, v16.16b, v18.16b - trn2 v31.16b, v20.16b, v22.16b - - trn1 v16.8h, v28.8h, v30.8h - trn2 v18.8h, v28.8h, v30.8h - trn1 v20.8h, v29.8h, v31.8h - trn2 v22.8h, v29.8h, v31.8h - - uzp1 v28.4s, v16.4s, v18.4s - uzp2 v30.4s, v16.4s, v18.4s - uzp1 v29.4s, v20.4s, v22.4s - uzp2 v31.4s, v20.4s, v22.4s - - /* Store results to the output buffer */ - st1 {v28.d}[0], [TMP1] - st1 {v29.d}[0], [TMP2] - st1 {v28.d}[1], [TMP3] - st1 {v29.d}[1], [TMP4] - st1 {v30.d}[0], [TMP5] - st1 {v31.d}[0], [TMP6] - st1 {v30.d}[1], [TMP7] - st1 {v31.d}[1], [TMP8] - blr x30 - - .unreq DCT_TABLE - .unreq COEF_BLOCK - .unreq OUTPUT_BUF - .unreq OUTPUT_COL - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMP4 - .unreq TMP5 - .unreq TMP6 - .unreq TMP7 - .unreq TMP8 - - -/*****************************************************************************/ - -/* - * jsimd_idct_4x4_neon - * - * This function contains inverse-DCT code for getting reduced-size - * 4x4 pixels output from an 8x8 DCT block. It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_idct_4x4' - * function from jpeg-6b (jidctred.c). - * - * NOTE: jpeg-8 has an improved implementation of 4x4 inverse-DCT, which - * requires much less arithmetic operations and hence should be faster. - * The primary purpose of this particular NEON optimized function is - * bit exact compatibility with jpeg-6b. - * - * TODO: a bit better instructions scheduling can be achieved by expanding - * idct_helper/transpose_4x4 macros and reordering instructions, - * but readability will suffer somewhat. - */ - -.macro idct_helper x4, x6, x8, x10, x12, x14, x16, shift, y26, y27, y28, y29 - smull v28.4s, \x4, v2.h[2] - smlal v28.4s, \x8, v0.h[0] - smlal v28.4s, \x14, v0.h[1] - - smull v26.4s, \x16, v1.h[2] - smlal v26.4s, \x12, v1.h[3] - smlal v26.4s, \x10, v2.h[0] - smlal v26.4s, \x6, v2.h[1] - - smull v30.4s, \x4, v2.h[2] - smlsl v30.4s, \x8, v0.h[0] - smlsl v30.4s, \x14, v0.h[1] - - smull v24.4s, \x16, v0.h[2] - smlal v24.4s, \x12, v0.h[3] - smlal v24.4s, \x10, v1.h[0] - smlal v24.4s, \x6, v1.h[1] - - add v20.4s, v28.4s, v26.4s - sub v28.4s, v28.4s, v26.4s - - .if \shift > 16 - srshr v20.4s, v20.4s, #\shift - srshr v28.4s, v28.4s, #\shift - xtn \y26, v20.4s - xtn \y29, v28.4s - .else - rshrn \y26, v20.4s, #\shift - rshrn \y29, v28.4s, #\shift - .endif - - add v20.4s, v30.4s, v24.4s - sub v30.4s, v30.4s, v24.4s - - .if \shift > 16 - srshr v20.4s, v20.4s, #\shift - srshr v30.4s, v30.4s, #\shift - xtn \y27, v20.4s - xtn \y28, v30.4s - .else - rshrn \y27, v20.4s, #\shift - rshrn \y28, v30.4s, #\shift - .endif -.endm - -asm_function jsimd_idct_4x4_neon - - DCT_TABLE .req x0 - COEF_BLOCK .req x1 - OUTPUT_BUF .req x2 - OUTPUT_COL .req x3 - TMP1 .req x0 - TMP2 .req x1 - TMP3 .req x2 - TMP4 .req x15 - - /* OUTPUT_COL is a JDIMENSION (unsigned int) argument, so the ABI doesn't - guarantee that the upper (unused) 32 bits of x3 are valid. This - instruction ensures that those bits are set to zero. */ - uxtw x3, w3 - - /* Save all used NEON registers */ - sub sp, sp, 64 - mov x9, sp - /* Load constants (v3.4h is just used for padding) */ - get_symbol_loc TMP4, Ljsimd_idct_4x4_neon_consts - st1 {v8.8b, v9.8b, v10.8b, v11.8b}, [x9], 32 - st1 {v12.8b, v13.8b, v14.8b, v15.8b}, [x9], 32 - ld1 {v0.4h, v1.4h, v2.4h, v3.4h}, [TMP4] - - /* Load all COEF_BLOCK into NEON registers with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | v4.4h | v5.4h - * 1 | v6.4h | v7.4h - * 2 | v8.4h | v9.4h - * 3 | v10.4h | v11.4h - * 4 | - | - - * 5 | v12.4h | v13.4h - * 6 | v14.4h | v15.4h - * 7 | v16.4h | v17.4h - */ - ld1 {v4.4h, v5.4h, v6.4h, v7.4h}, [COEF_BLOCK], 32 - ld1 {v8.4h, v9.4h, v10.4h, v11.4h}, [COEF_BLOCK], 32 - add COEF_BLOCK, COEF_BLOCK, #16 - ld1 {v12.4h, v13.4h, v14.4h, v15.4h}, [COEF_BLOCK], 32 - ld1 {v16.4h, v17.4h}, [COEF_BLOCK], 16 - /* dequantize */ - ld1 {v18.4h, v19.4h, v20.4h, v21.4h}, [DCT_TABLE], 32 - mul v4.4h, v4.4h, v18.4h - mul v5.4h, v5.4h, v19.4h - ins v4.d[1], v5.d[0] /* 128 bit q4 */ - ld1 {v22.4h, v23.4h, v24.4h, v25.4h}, [DCT_TABLE], 32 - mul v6.4h, v6.4h, v20.4h - mul v7.4h, v7.4h, v21.4h - ins v6.d[1], v7.d[0] /* 128 bit q6 */ - mul v8.4h, v8.4h, v22.4h - mul v9.4h, v9.4h, v23.4h - ins v8.d[1], v9.d[0] /* 128 bit q8 */ - add DCT_TABLE, DCT_TABLE, #16 - ld1 {v26.4h, v27.4h, v28.4h, v29.4h}, [DCT_TABLE], 32 - mul v10.4h, v10.4h, v24.4h - mul v11.4h, v11.4h, v25.4h - ins v10.d[1], v11.d[0] /* 128 bit q10 */ - mul v12.4h, v12.4h, v26.4h - mul v13.4h, v13.4h, v27.4h - ins v12.d[1], v13.d[0] /* 128 bit q12 */ - ld1 {v30.4h, v31.4h}, [DCT_TABLE], 16 - mul v14.4h, v14.4h, v28.4h - mul v15.4h, v15.4h, v29.4h - ins v14.d[1], v15.d[0] /* 128 bit q14 */ - mul v16.4h, v16.4h, v30.4h - mul v17.4h, v17.4h, v31.4h - ins v16.d[1], v17.d[0] /* 128 bit q16 */ - - /* Pass 1 */ - idct_helper v4.4h, v6.4h, v8.4h, v10.4h, v12.4h, v14.4h, v16.4h, 12, \ - v4.4h, v6.4h, v8.4h, v10.4h - transpose_4x4 v4, v6, v8, v10, v3 - ins v10.d[1], v11.d[0] - idct_helper v5.4h, v7.4h, v9.4h, v11.4h, v13.4h, v15.4h, v17.4h, 12, \ - v5.4h, v7.4h, v9.4h, v11.4h - transpose_4x4 v5, v7, v9, v11, v3 - ins v10.d[1], v11.d[0] - - /* Pass 2 */ - idct_helper v4.4h, v6.4h, v8.4h, v10.4h, v7.4h, v9.4h, v11.4h, 19, \ - v26.4h, v27.4h, v28.4h, v29.4h - transpose_4x4 v26, v27, v28, v29, v3 - - /* Range limit */ - movi v30.8h, #0x80 - ins v26.d[1], v27.d[0] - ins v28.d[1], v29.d[0] - add v26.8h, v26.8h, v30.8h - add v28.8h, v28.8h, v30.8h - sqxtun v26.8b, v26.8h - sqxtun v27.8b, v28.8h - - /* Store results to the output buffer */ - ldp TMP1, TMP2, [OUTPUT_BUF], 16 - ldp TMP3, TMP4, [OUTPUT_BUF] - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - add TMP3, TMP3, OUTPUT_COL - add TMP4, TMP4, OUTPUT_COL - -#if defined(__ARMEL__) && !RESPECT_STRICT_ALIGNMENT - /* We can use much less instructions on little endian systems if the - * OS kernel is not configured to trap unaligned memory accesses - */ - st1 {v26.s}[0], [TMP1], 4 - st1 {v27.s}[0], [TMP3], 4 - st1 {v26.s}[1], [TMP2], 4 - st1 {v27.s}[1], [TMP4], 4 -#else - st1 {v26.b}[0], [TMP1], 1 - st1 {v27.b}[0], [TMP3], 1 - st1 {v26.b}[1], [TMP1], 1 - st1 {v27.b}[1], [TMP3], 1 - st1 {v26.b}[2], [TMP1], 1 - st1 {v27.b}[2], [TMP3], 1 - st1 {v26.b}[3], [TMP1], 1 - st1 {v27.b}[3], [TMP3], 1 - - st1 {v26.b}[4], [TMP2], 1 - st1 {v27.b}[4], [TMP4], 1 - st1 {v26.b}[5], [TMP2], 1 - st1 {v27.b}[5], [TMP4], 1 - st1 {v26.b}[6], [TMP2], 1 - st1 {v27.b}[6], [TMP4], 1 - st1 {v26.b}[7], [TMP2], 1 - st1 {v27.b}[7], [TMP4], 1 -#endif - - /* vpop {v8.4h - v15.4h} ;not available */ - ld1 {v8.8b, v9.8b, v10.8b, v11.8b}, [sp], 32 - ld1 {v12.8b, v13.8b, v14.8b, v15.8b}, [sp], 32 - blr x30 - - .unreq DCT_TABLE - .unreq COEF_BLOCK - .unreq OUTPUT_BUF - .unreq OUTPUT_COL - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMP4 - -.purgem idct_helper - - -/*****************************************************************************/ - -/* - * jsimd_idct_2x2_neon - * - * This function contains inverse-DCT code for getting reduced-size - * 2x2 pixels output from an 8x8 DCT block. It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_idct_2x2' - * function from jpeg-6b (jidctred.c). - * - * NOTE: jpeg-8 has an improved implementation of 2x2 inverse-DCT, which - * requires much less arithmetic operations and hence should be faster. - * The primary purpose of this particular NEON optimized function is - * bit exact compatibility with jpeg-6b. - */ - -.macro idct_helper x4, x6, x10, x12, x16, shift, y26, y27 - sshll v15.4s, \x4, #15 - smull v26.4s, \x6, v14.h[3] - smlal v26.4s, \x10, v14.h[2] - smlal v26.4s, \x12, v14.h[1] - smlal v26.4s, \x16, v14.h[0] - - add v20.4s, v15.4s, v26.4s - sub v15.4s, v15.4s, v26.4s - - .if \shift > 16 - srshr v20.4s, v20.4s, #\shift - srshr v15.4s, v15.4s, #\shift - xtn \y26, v20.4s - xtn \y27, v15.4s - .else - rshrn \y26, v20.4s, #\shift - rshrn \y27, v15.4s, #\shift - .endif -.endm - -asm_function jsimd_idct_2x2_neon - - DCT_TABLE .req x0 - COEF_BLOCK .req x1 - OUTPUT_BUF .req x2 - OUTPUT_COL .req x3 - TMP1 .req x0 - TMP2 .req x15 - - /* OUTPUT_COL is a JDIMENSION (unsigned int) argument, so the ABI doesn't - guarantee that the upper (unused) 32 bits of x3 are valid. This - instruction ensures that those bits are set to zero. */ - uxtw x3, w3 - - /* vpush {v8.4h - v15.4h} ; not available */ - sub sp, sp, 64 - mov x9, sp - - /* Load constants */ - get_symbol_loc TMP2, Ljsimd_idct_2x2_neon_consts - st1 {v8.8b, v9.8b, v10.8b, v11.8b}, [x9], 32 - st1 {v12.8b, v13.8b, v14.8b, v15.8b}, [x9], 32 - ld1 {v14.4h}, [TMP2] - - /* Load all COEF_BLOCK into NEON registers with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | v4.4h | v5.4h - * 1 | v6.4h | v7.4h - * 2 | - | - - * 3 | v10.4h | v11.4h - * 4 | - | - - * 5 | v12.4h | v13.4h - * 6 | - | - - * 7 | v16.4h | v17.4h - */ - ld1 {v4.4h, v5.4h, v6.4h, v7.4h}, [COEF_BLOCK], 32 - add COEF_BLOCK, COEF_BLOCK, #16 - ld1 {v10.4h, v11.4h}, [COEF_BLOCK], 16 - add COEF_BLOCK, COEF_BLOCK, #16 - ld1 {v12.4h, v13.4h}, [COEF_BLOCK], 16 - add COEF_BLOCK, COEF_BLOCK, #16 - ld1 {v16.4h, v17.4h}, [COEF_BLOCK], 16 - /* Dequantize */ - ld1 {v18.4h, v19.4h, v20.4h, v21.4h}, [DCT_TABLE], 32 - mul v4.4h, v4.4h, v18.4h - mul v5.4h, v5.4h, v19.4h - ins v4.d[1], v5.d[0] - mul v6.4h, v6.4h, v20.4h - mul v7.4h, v7.4h, v21.4h - ins v6.d[1], v7.d[0] - add DCT_TABLE, DCT_TABLE, #16 - ld1 {v24.4h, v25.4h}, [DCT_TABLE], 16 - mul v10.4h, v10.4h, v24.4h - mul v11.4h, v11.4h, v25.4h - ins v10.d[1], v11.d[0] - add DCT_TABLE, DCT_TABLE, #16 - ld1 {v26.4h, v27.4h}, [DCT_TABLE], 16 - mul v12.4h, v12.4h, v26.4h - mul v13.4h, v13.4h, v27.4h - ins v12.d[1], v13.d[0] - add DCT_TABLE, DCT_TABLE, #16 - ld1 {v30.4h, v31.4h}, [DCT_TABLE], 16 - mul v16.4h, v16.4h, v30.4h - mul v17.4h, v17.4h, v31.4h - ins v16.d[1], v17.d[0] - - /* Pass 1 */ -#if 0 - idct_helper v4.4h, v6.4h, v10.4h, v12.4h, v16.4h, 13, v4.4h, v6.4h - transpose_4x4 v4.4h, v6.4h, v8.4h, v10.4h - idct_helper v5.4h, v7.4h, v11.4h, v13.4h, v17.4h, 13, v5.4h, v7.4h - transpose_4x4 v5.4h, v7.4h, v9.4h, v11.4h -#else - smull v26.4s, v6.4h, v14.h[3] - smlal v26.4s, v10.4h, v14.h[2] - smlal v26.4s, v12.4h, v14.h[1] - smlal v26.4s, v16.4h, v14.h[0] - smull v24.4s, v7.4h, v14.h[3] - smlal v24.4s, v11.4h, v14.h[2] - smlal v24.4s, v13.4h, v14.h[1] - smlal v24.4s, v17.4h, v14.h[0] - sshll v15.4s, v4.4h, #15 - sshll v30.4s, v5.4h, #15 - add v20.4s, v15.4s, v26.4s - sub v15.4s, v15.4s, v26.4s - rshrn v4.4h, v20.4s, #13 - rshrn v6.4h, v15.4s, #13 - add v20.4s, v30.4s, v24.4s - sub v15.4s, v30.4s, v24.4s - rshrn v5.4h, v20.4s, #13 - rshrn v7.4h, v15.4s, #13 - ins v4.d[1], v5.d[0] - ins v6.d[1], v7.d[0] - transpose v4, v6, v3, .16b, .8h - transpose v6, v10, v3, .16b, .4s - ins v11.d[0], v10.d[1] - ins v7.d[0], v6.d[1] -#endif - - /* Pass 2 */ - idct_helper v4.4h, v6.4h, v10.4h, v7.4h, v11.4h, 20, v26.4h, v27.4h - - /* Range limit */ - movi v30.8h, #0x80 - ins v26.d[1], v27.d[0] - add v26.8h, v26.8h, v30.8h - sqxtun v30.8b, v26.8h - ins v26.d[0], v30.d[0] - sqxtun v27.8b, v26.8h - - /* Store results to the output buffer */ - ldp TMP1, TMP2, [OUTPUT_BUF] - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - - st1 {v26.b}[0], [TMP1], 1 - st1 {v27.b}[4], [TMP1], 1 - st1 {v26.b}[1], [TMP2], 1 - st1 {v27.b}[5], [TMP2], 1 - - ld1 {v8.8b, v9.8b, v10.8b, v11.8b}, [sp], 32 - ld1 {v12.8b, v13.8b, v14.8b, v15.8b}, [sp], 32 - blr x30 - - .unreq DCT_TABLE - .unreq COEF_BLOCK - .unreq OUTPUT_BUF - .unreq OUTPUT_COL - .unreq TMP1 - .unreq TMP2 - -.purgem idct_helper - - /*****************************************************************************/ /* @@ -1855,7 +1106,7 @@ asm_function jsimd_ycc_\colorid\()_convert_neon_slowst3 /* Load constants to d1, d2, d3 (v0.4h is just used for padding) */ get_symbol_loc x15, Ljsimd_ycc_rgb_neon_consts - /* Save NEON registers */ + /* Save Neon registers */ st1 {v8.8b, v9.8b, v10.8b, v11.8b}, [x9], 32 st1 {v12.8b, v13.8b, v14.8b, v15.8b}, [x9], 32 ld1 {v0.4h, v1.4h}, [x15], 16 @@ -2140,7 +1391,7 @@ generate_jsimd_ycc_rgb_convert_neon extbgr, 24, 2, .4h, 1, .4h, 0, .4h, .8b, .endm /* TODO: expand macros and interleave instructions if some in-order - * ARM64 processor actually can dual-issue LOAD/STORE with ALU */ + * AArch64 processor actually can dual-issue LOAD/STORE with ALU */ .macro do_rgb_to_yuv_stage2_store_load_stage1 fast_ld3 do_rgb_to_yuv_stage2 do_load \bpp, 8, \fast_ld3 @@ -2180,7 +1431,7 @@ asm_function jsimd_\colorid\()_ycc_convert_neon_slowld3 ldr OUTPUT_BUF2, [OUTPUT_BUF, #16] .unreq OUTPUT_BUF - /* Save NEON registers */ + /* Save Neon registers */ sub sp, sp, #64 mov x9, sp st1 {v8.8b, v9.8b, v10.8b, v11.8b}, [x9], 32 @@ -2276,88 +1527,12 @@ generate_jsimd_rgb_ycc_convert_neon extbgr, 24, 2, 1, 0, 0 .purgem do_store -/*****************************************************************************/ - -/* - * Load data into workspace, applying unsigned->signed conversion - * - * TODO: can be combined with 'jsimd_fdct_ifast_neon' to get - * rid of VST1.16 instructions - */ - -asm_function jsimd_convsamp_neon - SAMPLE_DATA .req x0 - START_COL .req x1 - WORKSPACE .req x2 - TMP1 .req x9 - TMP2 .req x10 - TMP3 .req x11 - TMP4 .req x12 - TMP5 .req x13 - TMP6 .req x14 - TMP7 .req x15 - TMP8 .req x4 - TMPDUP .req w3 - - /* START_COL is a JDIMENSION (unsigned int) argument, so the ABI doesn't - guarantee that the upper (unused) 32 bits of x1 are valid. This - instruction ensures that those bits are set to zero. */ - uxtw x1, w1 - - mov TMPDUP, #128 - ldp TMP1, TMP2, [SAMPLE_DATA], 16 - ldp TMP3, TMP4, [SAMPLE_DATA], 16 - dup v0.8b, TMPDUP - add TMP1, TMP1, START_COL - add TMP2, TMP2, START_COL - ldp TMP5, TMP6, [SAMPLE_DATA], 16 - add TMP3, TMP3, START_COL - add TMP4, TMP4, START_COL - ldp TMP7, TMP8, [SAMPLE_DATA], 16 - add TMP5, TMP5, START_COL - add TMP6, TMP6, START_COL - ld1 {v16.8b}, [TMP1] - add TMP7, TMP7, START_COL - add TMP8, TMP8, START_COL - ld1 {v17.8b}, [TMP2] - usubl v16.8h, v16.8b, v0.8b - ld1 {v18.8b}, [TMP3] - usubl v17.8h, v17.8b, v0.8b - ld1 {v19.8b}, [TMP4] - usubl v18.8h, v18.8b, v0.8b - ld1 {v20.8b}, [TMP5] - usubl v19.8h, v19.8b, v0.8b - ld1 {v21.8b}, [TMP6] - st1 {v16.8h, v17.8h, v18.8h, v19.8h}, [WORKSPACE], 64 - usubl v20.8h, v20.8b, v0.8b - ld1 {v22.8b}, [TMP7] - usubl v21.8h, v21.8b, v0.8b - ld1 {v23.8b}, [TMP8] - usubl v22.8h, v22.8b, v0.8b - usubl v23.8h, v23.8b, v0.8b - st1 {v20.8h, v21.8h, v22.8h, v23.8h}, [WORKSPACE], 64 - - br x30 - - .unreq SAMPLE_DATA - .unreq START_COL - .unreq WORKSPACE - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMP4 - .unreq TMP5 - .unreq TMP6 - .unreq TMP7 - .unreq TMP8 - .unreq TMPDUP - /*****************************************************************************/ /* * jsimd_fdct_islow_neon * - * This file contains a slow-but-accurate integer implementation of the + * This file contains a slower but more accurate integer implementation of the * forward DCT (Discrete Cosine Transform). The following code is based * directly on the IJG''s original jfdctint.c; see the jfdctint.c for * more details. @@ -2394,13 +1569,13 @@ asm_function jsimd_fdct_islow_neon get_symbol_loc TMP, Ljsimd_fdct_islow_neon_consts ld1 {v0.8h, v1.8h}, [TMP] - /* Save NEON registers */ + /* Save Neon registers */ sub sp, sp, #64 mov x10, sp st1 {v8.8b, v9.8b, v10.8b, v11.8b}, [x10], 32 st1 {v12.8b, v13.8b, v14.8b, v15.8b}, [x10], 32 - /* Load all DATA into NEON registers with the following allocation: + /* Load all DATA into Neon registers with the following allocation: * 0 1 2 3 | 4 5 6 7 * ---------+-------- * 0 | d16 | d17 | v16.8h @@ -2627,7 +1802,7 @@ asm_function jsimd_fdct_islow_neon st1 {v16.8h, v17.8h, v18.8h, v19.8h}, [DATA], 64 st1 {v20.8h, v21.8h, v22.8h, v23.8h}, [DATA] - /* Restore NEON registers */ + /* Restore Neon registers */ ld1 {v8.8b, v9.8b, v10.8b, v11.8b}, [sp], 32 ld1 {v12.8b, v13.8b, v14.8b, v15.8b}, [sp], 32 @@ -2650,360 +1825,6 @@ asm_function jsimd_fdct_islow_neon #undef XFIX_P_3_072 -/*****************************************************************************/ - -/* - * jsimd_fdct_ifast_neon - * - * This function contains a fast, not so accurate integer implementation of - * the forward DCT (Discrete Cosine Transform). It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_fdct_ifast' - * function from jfdctfst.c - * - * TODO: can be combined with 'jsimd_convsamp_neon' to get - * rid of a bunch of VLD1.16 instructions - */ - -#undef XFIX_0_541196100 -#define XFIX_0_382683433 v0.h[0] -#define XFIX_0_541196100 v0.h[1] -#define XFIX_0_707106781 v0.h[2] -#define XFIX_1_306562965 v0.h[3] - -asm_function jsimd_fdct_ifast_neon - - DATA .req x0 - TMP .req x9 - - /* Load constants */ - get_symbol_loc TMP, Ljsimd_fdct_ifast_neon_consts - ld1 {v0.4h}, [TMP] - - /* Load all DATA into NEON registers with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | d16 | d17 | v0.8h - * 1 | d18 | d19 | q9 - * 2 | d20 | d21 | q10 - * 3 | d22 | d23 | q11 - * 4 | d24 | d25 | q12 - * 5 | d26 | d27 | q13 - * 6 | d28 | d29 | q14 - * 7 | d30 | d31 | q15 - */ - - ld1 {v16.8h, v17.8h, v18.8h, v19.8h}, [DATA], 64 - ld1 {v20.8h, v21.8h, v22.8h, v23.8h}, [DATA] - mov TMP, #2 - sub DATA, DATA, #64 -1: - /* Transpose */ - transpose_8x8 v16, v17, v18, v19, v20, v21, v22, v23, v1, v2, v3, v4 - subs TMP, TMP, #1 - /* 1-D FDCT */ - add v4.8h, v19.8h, v20.8h - sub v20.8h, v19.8h, v20.8h - sub v28.8h, v18.8h, v21.8h - add v18.8h, v18.8h, v21.8h - sub v29.8h, v17.8h, v22.8h - add v17.8h, v17.8h, v22.8h - sub v21.8h, v16.8h, v23.8h - add v16.8h, v16.8h, v23.8h - sub v6.8h, v17.8h, v18.8h - sub v7.8h, v16.8h, v4.8h - add v5.8h, v17.8h, v18.8h - add v6.8h, v6.8h, v7.8h - add v4.8h, v16.8h, v4.8h - sqdmulh v6.8h, v6.8h, XFIX_0_707106781 - add v19.8h, v20.8h, v28.8h - add v16.8h, v4.8h, v5.8h - sub v20.8h, v4.8h, v5.8h - add v5.8h, v28.8h, v29.8h - add v29.8h, v29.8h, v21.8h - sqdmulh v5.8h, v5.8h, XFIX_0_707106781 - sub v28.8h, v19.8h, v29.8h - add v18.8h, v7.8h, v6.8h - sqdmulh v28.8h, v28.8h, XFIX_0_382683433 - sub v22.8h, v7.8h, v6.8h - sqdmulh v19.8h, v19.8h, XFIX_0_541196100 - sqdmulh v7.8h, v29.8h, XFIX_1_306562965 - add v6.8h, v21.8h, v5.8h - sub v5.8h, v21.8h, v5.8h - add v29.8h, v29.8h, v28.8h - add v19.8h, v19.8h, v28.8h - add v29.8h, v29.8h, v7.8h - add v21.8h, v5.8h, v19.8h - sub v19.8h, v5.8h, v19.8h - add v17.8h, v6.8h, v29.8h - sub v23.8h, v6.8h, v29.8h - - b.ne 1b - - /* store results */ - st1 {v16.8h, v17.8h, v18.8h, v19.8h}, [DATA], 64 - st1 {v20.8h, v21.8h, v22.8h, v23.8h}, [DATA] - - br x30 - - .unreq DATA - .unreq TMP -#undef XFIX_0_382683433 -#undef XFIX_0_541196100 -#undef XFIX_0_707106781 -#undef XFIX_1_306562965 - - -/*****************************************************************************/ - -/* - * GLOBAL(void) - * jsimd_quantize_neon(JCOEFPTR coef_block, DCTELEM *divisors, - * DCTELEM *workspace); - * - */ -asm_function jsimd_quantize_neon - - COEF_BLOCK .req x0 - DIVISORS .req x1 - WORKSPACE .req x2 - - RECIPROCAL .req DIVISORS - CORRECTION .req x9 - SHIFT .req x10 - LOOP_COUNT .req x11 - - mov LOOP_COUNT, #2 - add CORRECTION, DIVISORS, #(64 * 2) - add SHIFT, DIVISORS, #(64 * 6) -1: - subs LOOP_COUNT, LOOP_COUNT, #1 - ld1 {v0.8h, v1.8h, v2.8h, v3.8h}, [WORKSPACE], 64 - ld1 {v4.8h, v5.8h, v6.8h, v7.8h}, [CORRECTION], 64 - abs v20.8h, v0.8h - abs v21.8h, v1.8h - abs v22.8h, v2.8h - abs v23.8h, v3.8h - ld1 {v28.8h, v29.8h, v30.8h, v31.8h}, [RECIPROCAL], 64 - add v20.8h, v20.8h, v4.8h /* add correction */ - add v21.8h, v21.8h, v5.8h - add v22.8h, v22.8h, v6.8h - add v23.8h, v23.8h, v7.8h - umull v4.4s, v20.4h, v28.4h /* multiply by reciprocal */ - umull2 v16.4s, v20.8h, v28.8h - umull v5.4s, v21.4h, v29.4h - umull2 v17.4s, v21.8h, v29.8h - umull v6.4s, v22.4h, v30.4h /* multiply by reciprocal */ - umull2 v18.4s, v22.8h, v30.8h - umull v7.4s, v23.4h, v31.4h - umull2 v19.4s, v23.8h, v31.8h - ld1 {v24.8h, v25.8h, v26.8h, v27.8h}, [SHIFT], 64 - shrn v4.4h, v4.4s, #16 - shrn v5.4h, v5.4s, #16 - shrn v6.4h, v6.4s, #16 - shrn v7.4h, v7.4s, #16 - shrn2 v4.8h, v16.4s, #16 - shrn2 v5.8h, v17.4s, #16 - shrn2 v6.8h, v18.4s, #16 - shrn2 v7.8h, v19.4s, #16 - neg v24.8h, v24.8h - neg v25.8h, v25.8h - neg v26.8h, v26.8h - neg v27.8h, v27.8h - sshr v0.8h, v0.8h, #15 /* extract sign */ - sshr v1.8h, v1.8h, #15 - sshr v2.8h, v2.8h, #15 - sshr v3.8h, v3.8h, #15 - ushl v4.8h, v4.8h, v24.8h /* shift */ - ushl v5.8h, v5.8h, v25.8h - ushl v6.8h, v6.8h, v26.8h - ushl v7.8h, v7.8h, v27.8h - - eor v4.16b, v4.16b, v0.16b /* restore sign */ - eor v5.16b, v5.16b, v1.16b - eor v6.16b, v6.16b, v2.16b - eor v7.16b, v7.16b, v3.16b - sub v4.8h, v4.8h, v0.8h - sub v5.8h, v5.8h, v1.8h - sub v6.8h, v6.8h, v2.8h - sub v7.8h, v7.8h, v3.8h - st1 {v4.8h, v5.8h, v6.8h, v7.8h}, [COEF_BLOCK], 64 - - b.ne 1b - - br x30 /* return */ - - .unreq COEF_BLOCK - .unreq DIVISORS - .unreq WORKSPACE - .unreq RECIPROCAL - .unreq CORRECTION - .unreq SHIFT - .unreq LOOP_COUNT - - -/*****************************************************************************/ - -/* - * Downsample pixel values of a single component. - * This version handles the common case of 2:1 horizontal and 1:1 vertical, - * without smoothing. - * - * GLOBAL(void) - * jsimd_h2v1_downsample_neon(JDIMENSION image_width, int max_v_samp_factor, - * JDIMENSION v_samp_factor, - * JDIMENSION width_in_blocks, - * JSAMPARRAY input_data, JSAMPARRAY output_data); - */ - -asm_function jsimd_h2v1_downsample_neon - IMAGE_WIDTH .req x0 - MAX_V_SAMP .req x1 - V_SAMP .req x2 - BLOCK_WIDTH .req x3 - INPUT_DATA .req x4 - OUTPUT_DATA .req x5 - OUTPTR .req x9 - INPTR .req x10 - TMP1 .req x11 - TMP2 .req x12 - TMP3 .req x13 - TMPDUP .req w15 - - mov TMPDUP, #0x10000 - lsl TMP2, BLOCK_WIDTH, #4 - sub TMP2, TMP2, IMAGE_WIDTH - get_symbol_loc TMP3, Ljsimd_h2_downsample_neon_consts - add TMP3, TMP3, TMP2, lsl #4 - dup v16.4s, TMPDUP - ld1 {v18.16b}, [TMP3] - -1: /* row loop */ - ldr INPTR, [INPUT_DATA], #8 - ldr OUTPTR, [OUTPUT_DATA], #8 - subs TMP1, BLOCK_WIDTH, #1 - b.eq 3f -2: /* columns */ - ld1 {v0.16b}, [INPTR], #16 - mov v4.16b, v16.16b - subs TMP1, TMP1, #1 - uadalp v4.8h, v0.16b - shrn v6.8b, v4.8h, #1 - st1 {v6.8b}, [OUTPTR], #8 - b.ne 2b -3: /* last columns */ - ld1 {v0.16b}, [INPTR] - mov v4.16b, v16.16b - subs V_SAMP, V_SAMP, #1 - /* expand right */ - tbl v2.16b, {v0.16b}, v18.16b - uadalp v4.8h, v2.16b - shrn v6.8b, v4.8h, #1 - st1 {v6.8b}, [OUTPTR], #8 - b.ne 1b - - br x30 - - .unreq IMAGE_WIDTH - .unreq MAX_V_SAMP - .unreq V_SAMP - .unreq BLOCK_WIDTH - .unreq INPUT_DATA - .unreq OUTPUT_DATA - .unreq OUTPTR - .unreq INPTR - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMPDUP - - -/*****************************************************************************/ - -/* - * Downsample pixel values of a single component. - * This version handles the common case of 2:1 horizontal and 2:1 vertical, - * without smoothing. - * - * GLOBAL(void) - * jsimd_h2v2_downsample_neon(JDIMENSION image_width, int max_v_samp_factor, - * JDIMENSION v_samp_factor, - * JDIMENSION width_in_blocks, - * JSAMPARRAY input_data, JSAMPARRAY output_data); - */ - -.balign 16 -asm_function jsimd_h2v2_downsample_neon - IMAGE_WIDTH .req x0 - MAX_V_SAMP .req x1 - V_SAMP .req x2 - BLOCK_WIDTH .req x3 - INPUT_DATA .req x4 - OUTPUT_DATA .req x5 - OUTPTR .req x9 - INPTR0 .req x10 - INPTR1 .req x14 - TMP1 .req x11 - TMP2 .req x12 - TMP3 .req x13 - TMPDUP .req w15 - - mov TMPDUP, #1 - lsl TMP2, BLOCK_WIDTH, #4 - lsl TMPDUP, TMPDUP, #17 - sub TMP2, TMP2, IMAGE_WIDTH - get_symbol_loc TMP3, Ljsimd_h2_downsample_neon_consts - orr TMPDUP, TMPDUP, #1 - add TMP3, TMP3, TMP2, lsl #4 - dup v16.4s, TMPDUP - ld1 {v18.16b}, [TMP3] - -1: /* row loop */ - ldr INPTR0, [INPUT_DATA], #8 - ldr OUTPTR, [OUTPUT_DATA], #8 - ldr INPTR1, [INPUT_DATA], #8 - subs TMP1, BLOCK_WIDTH, #1 - b.eq 3f -2: /* columns */ - ld1 {v0.16b}, [INPTR0], #16 - ld1 {v1.16b}, [INPTR1], #16 - mov v4.16b, v16.16b - subs TMP1, TMP1, #1 - uadalp v4.8h, v0.16b - uadalp v4.8h, v1.16b - shrn v6.8b, v4.8h, #2 - st1 {v6.8b}, [OUTPTR], #8 - b.ne 2b -3: /* last columns */ - ld1 {v0.16b}, [INPTR0], #16 - ld1 {v1.16b}, [INPTR1], #16 - mov v4.16b, v16.16b - subs V_SAMP, V_SAMP, #1 - /* expand right */ - tbl v2.16b, {v0.16b}, v18.16b - tbl v3.16b, {v1.16b}, v18.16b - uadalp v4.8h, v2.16b - uadalp v4.8h, v3.16b - shrn v6.8b, v4.8h, #2 - st1 {v6.8b}, [OUTPTR], #8 - b.ne 1b - - br x30 - - .unreq IMAGE_WIDTH - .unreq MAX_V_SAMP - .unreq V_SAMP - .unreq BLOCK_WIDTH - .unreq INPUT_DATA - .unreq OUTPUT_DATA - .unreq OUTPTR - .unreq INPTR0 - .unreq INPTR1 - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMPDUP - - /*****************************************************************************/ /* @@ -3064,7 +1885,7 @@ asm_function jsimd_huff_encode_one_block_neon_slowtbl .endif sub sp, sp, 272 sub BUFFER, BUFFER, #0x1 /* BUFFER=buffer-- */ - /* Save ARM registers */ + /* Save Arm registers */ stp x19, x20, [sp] get_symbol_loc x15, Ljsimd_huff_encode_one_block_neon_consts ldr PUT_BUFFER, [x0, #0x10] diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/align.h b/third-party/mozjpeg/mozjpeg/simd/arm/align.h new file mode 100644 index 00000000000..cff4241e843 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/align.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* How to obtain memory alignment for structures and variables */ +#if defined(_MSC_VER) +#define ALIGN(alignment) __declspec(align(alignment)) +#elif defined(__clang__) || defined(__GNUC__) +#define ALIGN(alignment) __attribute__((aligned(alignment))) +#else +#error "Unknown compiler" +#endif diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jccolor-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jccolor-neon.c new file mode 100644 index 00000000000..9fcc62dd25c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jccolor-neon.c @@ -0,0 +1,160 @@ +/* + * jccolor-neon.c - colorspace conversion (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" +#include "neon-compat.h" + +#include + + +/* RGB -> YCbCr conversion constants */ + +#define F_0_298 19595 +#define F_0_587 38470 +#define F_0_113 7471 +#define F_0_168 11059 +#define F_0_331 21709 +#define F_0_500 32768 +#define F_0_418 27439 +#define F_0_081 5329 + +ALIGN(16) static const uint16_t jsimd_rgb_ycc_neon_consts[] = { + F_0_298, F_0_587, F_0_113, F_0_168, + F_0_331, F_0_500, F_0_418, F_0_081 +}; + + +/* Include inline routines for colorspace extensions. */ + +#if defined(__aarch64__) || defined(_M_ARM64) +#include "aarch64/jccolext-neon.c" +#else +#include "aarch32/jccolext-neon.c" +#endif +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE + +#define RGB_RED EXT_RGB_RED +#define RGB_GREEN EXT_RGB_GREEN +#define RGB_BLUE EXT_RGB_BLUE +#define RGB_PIXELSIZE EXT_RGB_PIXELSIZE +#define jsimd_rgb_ycc_convert_neon jsimd_extrgb_ycc_convert_neon +#if defined(__aarch64__) || defined(_M_ARM64) +#include "aarch64/jccolext-neon.c" +#else +#include "aarch32/jccolext-neon.c" +#endif +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_ycc_convert_neon + +#define RGB_RED EXT_RGBX_RED +#define RGB_GREEN EXT_RGBX_GREEN +#define RGB_BLUE EXT_RGBX_BLUE +#define RGB_PIXELSIZE EXT_RGBX_PIXELSIZE +#define jsimd_rgb_ycc_convert_neon jsimd_extrgbx_ycc_convert_neon +#if defined(__aarch64__) || defined(_M_ARM64) +#include "aarch64/jccolext-neon.c" +#else +#include "aarch32/jccolext-neon.c" +#endif +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_ycc_convert_neon + +#define RGB_RED EXT_BGR_RED +#define RGB_GREEN EXT_BGR_GREEN +#define RGB_BLUE EXT_BGR_BLUE +#define RGB_PIXELSIZE EXT_BGR_PIXELSIZE +#define jsimd_rgb_ycc_convert_neon jsimd_extbgr_ycc_convert_neon +#if defined(__aarch64__) || defined(_M_ARM64) +#include "aarch64/jccolext-neon.c" +#else +#include "aarch32/jccolext-neon.c" +#endif +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_ycc_convert_neon + +#define RGB_RED EXT_BGRX_RED +#define RGB_GREEN EXT_BGRX_GREEN +#define RGB_BLUE EXT_BGRX_BLUE +#define RGB_PIXELSIZE EXT_BGRX_PIXELSIZE +#define jsimd_rgb_ycc_convert_neon jsimd_extbgrx_ycc_convert_neon +#if defined(__aarch64__) || defined(_M_ARM64) +#include "aarch64/jccolext-neon.c" +#else +#include "aarch32/jccolext-neon.c" +#endif +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_ycc_convert_neon + +#define RGB_RED EXT_XBGR_RED +#define RGB_GREEN EXT_XBGR_GREEN +#define RGB_BLUE EXT_XBGR_BLUE +#define RGB_PIXELSIZE EXT_XBGR_PIXELSIZE +#define jsimd_rgb_ycc_convert_neon jsimd_extxbgr_ycc_convert_neon +#if defined(__aarch64__) || defined(_M_ARM64) +#include "aarch64/jccolext-neon.c" +#else +#include "aarch32/jccolext-neon.c" +#endif +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_ycc_convert_neon + +#define RGB_RED EXT_XRGB_RED +#define RGB_GREEN EXT_XRGB_GREEN +#define RGB_BLUE EXT_XRGB_BLUE +#define RGB_PIXELSIZE EXT_XRGB_PIXELSIZE +#define jsimd_rgb_ycc_convert_neon jsimd_extxrgb_ycc_convert_neon +#if defined(__aarch64__) || defined(_M_ARM64) +#include "aarch64/jccolext-neon.c" +#else +#include "aarch32/jccolext-neon.c" +#endif +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_ycc_convert_neon diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jcgray-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jcgray-neon.c new file mode 100644 index 00000000000..71c7b2de218 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jcgray-neon.c @@ -0,0 +1,120 @@ +/* + * jcgray-neon.c - grayscale colorspace conversion (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" + +#include + + +/* RGB -> Grayscale conversion constants */ + +#define F_0_298 19595 +#define F_0_587 38470 +#define F_0_113 7471 + + +/* Include inline routines for colorspace extensions. */ + +#include "jcgryext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE + +#define RGB_RED EXT_RGB_RED +#define RGB_GREEN EXT_RGB_GREEN +#define RGB_BLUE EXT_RGB_BLUE +#define RGB_PIXELSIZE EXT_RGB_PIXELSIZE +#define jsimd_rgb_gray_convert_neon jsimd_extrgb_gray_convert_neon +#include "jcgryext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_neon + +#define RGB_RED EXT_RGBX_RED +#define RGB_GREEN EXT_RGBX_GREEN +#define RGB_BLUE EXT_RGBX_BLUE +#define RGB_PIXELSIZE EXT_RGBX_PIXELSIZE +#define jsimd_rgb_gray_convert_neon jsimd_extrgbx_gray_convert_neon +#include "jcgryext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_neon + +#define RGB_RED EXT_BGR_RED +#define RGB_GREEN EXT_BGR_GREEN +#define RGB_BLUE EXT_BGR_BLUE +#define RGB_PIXELSIZE EXT_BGR_PIXELSIZE +#define jsimd_rgb_gray_convert_neon jsimd_extbgr_gray_convert_neon +#include "jcgryext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_neon + +#define RGB_RED EXT_BGRX_RED +#define RGB_GREEN EXT_BGRX_GREEN +#define RGB_BLUE EXT_BGRX_BLUE +#define RGB_PIXELSIZE EXT_BGRX_PIXELSIZE +#define jsimd_rgb_gray_convert_neon jsimd_extbgrx_gray_convert_neon +#include "jcgryext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_neon + +#define RGB_RED EXT_XBGR_RED +#define RGB_GREEN EXT_XBGR_GREEN +#define RGB_BLUE EXT_XBGR_BLUE +#define RGB_PIXELSIZE EXT_XBGR_PIXELSIZE +#define jsimd_rgb_gray_convert_neon jsimd_extxbgr_gray_convert_neon +#include "jcgryext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_neon + +#define RGB_RED EXT_XRGB_RED +#define RGB_GREEN EXT_XRGB_GREEN +#define RGB_BLUE EXT_XRGB_BLUE +#define RGB_PIXELSIZE EXT_XRGB_PIXELSIZE +#define jsimd_rgb_gray_convert_neon jsimd_extxrgb_gray_convert_neon +#include "jcgryext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_neon diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jcgryext-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jcgryext-neon.c new file mode 100644 index 00000000000..416a7385df8 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jcgryext-neon.c @@ -0,0 +1,106 @@ +/* + * jcgryext-neon.c - grayscale colorspace conversion (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jcgray-neon.c */ + + +/* RGB -> Grayscale conversion is defined by the following equation: + * Y = 0.29900 * R + 0.58700 * G + 0.11400 * B + * + * Avoid floating point arithmetic by using shifted integer constants: + * 0.29899597 = 19595 * 2^-16 + * 0.58700561 = 38470 * 2^-16 + * 0.11399841 = 7471 * 2^-16 + * These constants are defined in jcgray-neon.c + * + * This is the same computation as the RGB -> Y portion of RGB -> YCbCr. + */ + +void jsimd_rgb_gray_convert_neon(JDIMENSION image_width, JSAMPARRAY input_buf, + JSAMPIMAGE output_buf, JDIMENSION output_row, + int num_rows) +{ + JSAMPROW inptr; + JSAMPROW outptr; + /* Allocate temporary buffer for final (image_width % 16) pixels in row. */ + ALIGN(16) uint8_t tmp_buf[16 * RGB_PIXELSIZE]; + + while (--num_rows >= 0) { + inptr = *input_buf++; + outptr = output_buf[0][output_row]; + output_row++; + + int cols_remaining = image_width; + for (; cols_remaining > 0; cols_remaining -= 16) { + + /* To prevent buffer overread by the vector load instructions, the last + * (image_width % 16) columns of data are first memcopied to a temporary + * buffer large enough to accommodate the vector load. + */ + if (cols_remaining < 16) { + memcpy(tmp_buf, inptr, cols_remaining * RGB_PIXELSIZE); + inptr = tmp_buf; + } + +#if RGB_PIXELSIZE == 4 + uint8x16x4_t input_pixels = vld4q_u8(inptr); +#else + uint8x16x3_t input_pixels = vld3q_u8(inptr); +#endif + uint16x8_t r_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_RED])); + uint16x8_t r_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_RED])); + uint16x8_t g_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_GREEN])); + uint16x8_t g_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_GREEN])); + uint16x8_t b_l = vmovl_u8(vget_low_u8(input_pixels.val[RGB_BLUE])); + uint16x8_t b_h = vmovl_u8(vget_high_u8(input_pixels.val[RGB_BLUE])); + + /* Compute Y = 0.29900 * R + 0.58700 * G + 0.11400 * B */ + uint32x4_t y_ll = vmull_n_u16(vget_low_u16(r_l), F_0_298); + uint32x4_t y_lh = vmull_n_u16(vget_high_u16(r_l), F_0_298); + uint32x4_t y_hl = vmull_n_u16(vget_low_u16(r_h), F_0_298); + uint32x4_t y_hh = vmull_n_u16(vget_high_u16(r_h), F_0_298); + y_ll = vmlal_n_u16(y_ll, vget_low_u16(g_l), F_0_587); + y_lh = vmlal_n_u16(y_lh, vget_high_u16(g_l), F_0_587); + y_hl = vmlal_n_u16(y_hl, vget_low_u16(g_h), F_0_587); + y_hh = vmlal_n_u16(y_hh, vget_high_u16(g_h), F_0_587); + y_ll = vmlal_n_u16(y_ll, vget_low_u16(b_l), F_0_113); + y_lh = vmlal_n_u16(y_lh, vget_high_u16(b_l), F_0_113); + y_hl = vmlal_n_u16(y_hl, vget_low_u16(b_h), F_0_113); + y_hh = vmlal_n_u16(y_hh, vget_high_u16(b_h), F_0_113); + + /* Descale Y values (rounding right shift) and narrow to 16-bit. */ + uint16x8_t y_l = vcombine_u16(vrshrn_n_u32(y_ll, 16), + vrshrn_n_u32(y_lh, 16)); + uint16x8_t y_h = vcombine_u16(vrshrn_n_u32(y_hl, 16), + vrshrn_n_u32(y_hh, 16)); + + /* Narrow Y values to 8-bit and store to memory. Buffer overwrite is + * permitted up to the next multiple of ALIGN_SIZE bytes. + */ + vst1q_u8(outptr, vcombine_u8(vmovn_u16(y_l), vmovn_u16(y_h))); + + /* Increment pointers. */ + inptr += (16 * RGB_PIXELSIZE); + outptr += 16; + } + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jchuff.h b/third-party/mozjpeg/mozjpeg/simd/arm/jchuff.h new file mode 100644 index 00000000000..2fbd252b9b8 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jchuff.h @@ -0,0 +1,131 @@ +/* + * jchuff.h + * + * This file was part of the Independent JPEG Group's software: + * Copyright (C) 1991-1997, Thomas G. Lane. + * libjpeg-turbo Modifications: + * Copyright (C) 2009, 2018, 2021, D. R. Commander. + * Copyright (C) 2018, Matthias Räncker. + * Copyright (C) 2020-2021, Arm Limited. + * For conditions of distribution and use, see the accompanying README.ijg + * file. + */ + +/* Expanded entropy encoder object for Huffman encoding. + * + * The savable_state subrecord contains fields that change within an MCU, + * but must not be updated permanently until we complete the MCU. + */ + +#if defined(__aarch64__) || defined(_M_ARM64) +#define BIT_BUF_SIZE 64 +#else +#define BIT_BUF_SIZE 32 +#endif + +typedef struct { + size_t put_buffer; /* current bit accumulation buffer */ + int free_bits; /* # of bits available in it */ + int last_dc_val[MAX_COMPS_IN_SCAN]; /* last DC coef for each component */ +} savable_state; + +typedef struct { + JOCTET *next_output_byte; /* => next byte to write in buffer */ + size_t free_in_buffer; /* # of byte spaces remaining in buffer */ + savable_state cur; /* Current bit buffer & DC state */ + j_compress_ptr cinfo; /* dump_buffer needs access to this */ + int simd; +} working_state; + +/* Outputting bits to the file */ + +/* Output byte b and, speculatively, an additional 0 byte. 0xFF must be encoded + * as 0xFF 0x00, so the output buffer pointer is advanced by 2 if the byte is + * 0xFF. Otherwise, the output buffer pointer is advanced by 1, and the + * speculative 0 byte will be overwritten by the next byte. + */ +#define EMIT_BYTE(b) { \ + buffer[0] = (JOCTET)(b); \ + buffer[1] = 0; \ + buffer -= -2 + ((JOCTET)(b) < 0xFF); \ +} + +/* Output the entire bit buffer. If there are no 0xFF bytes in it, then write + * directly to the output buffer. Otherwise, use the EMIT_BYTE() macro to + * encode 0xFF as 0xFF 0x00. + */ +#if defined(__aarch64__) || defined(_M_ARM64) + +#define FLUSH() { \ + if (put_buffer & 0x8080808080808080 & ~(put_buffer + 0x0101010101010101)) { \ + EMIT_BYTE(put_buffer >> 56) \ + EMIT_BYTE(put_buffer >> 48) \ + EMIT_BYTE(put_buffer >> 40) \ + EMIT_BYTE(put_buffer >> 32) \ + EMIT_BYTE(put_buffer >> 24) \ + EMIT_BYTE(put_buffer >> 16) \ + EMIT_BYTE(put_buffer >> 8) \ + EMIT_BYTE(put_buffer ) \ + } else { \ + *((uint64_t *)buffer) = BUILTIN_BSWAP64(put_buffer); \ + buffer += 8; \ + } \ +} + +#else + +#if defined(_MSC_VER) && !defined(__clang__) +#define SPLAT() { \ + buffer[0] = (JOCTET)(put_buffer >> 24); \ + buffer[1] = (JOCTET)(put_buffer >> 16); \ + buffer[2] = (JOCTET)(put_buffer >> 8); \ + buffer[3] = (JOCTET)(put_buffer ); \ + buffer += 4; \ +} +#else +#define SPLAT() { \ + put_buffer = __builtin_bswap32(put_buffer); \ + __asm__("str %1, [%0], #4" : "+r" (buffer) : "r" (put_buffer)); \ +} +#endif + +#define FLUSH() { \ + if (put_buffer & 0x80808080 & ~(put_buffer + 0x01010101)) { \ + EMIT_BYTE(put_buffer >> 24) \ + EMIT_BYTE(put_buffer >> 16) \ + EMIT_BYTE(put_buffer >> 8) \ + EMIT_BYTE(put_buffer ) \ + } else { \ + SPLAT(); \ + } \ +} + +#endif + +/* Fill the bit buffer to capacity with the leading bits from code, then output + * the bit buffer and put the remaining bits from code into the bit buffer. + */ +#define PUT_AND_FLUSH(code, size) { \ + put_buffer = (put_buffer << (size + free_bits)) | (code >> -free_bits); \ + FLUSH() \ + free_bits += BIT_BUF_SIZE; \ + put_buffer = code; \ +} + +/* Insert code into the bit buffer and output the bit buffer if needed. + * NOTE: We can't flush with free_bits == 0, since the left shift in + * PUT_AND_FLUSH() would have undefined behavior. + */ +#define PUT_BITS(code, size) { \ + free_bits -= size; \ + if (free_bits < 0) \ + PUT_AND_FLUSH(code, size) \ + else \ + put_buffer = (put_buffer << size) | code; \ +} + +#define PUT_CODE(code, size, diff) { \ + diff |= code << nbits; \ + nbits += size; \ + PUT_BITS(diff, nbits) \ +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jcphuff-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jcphuff-neon.c new file mode 100644 index 00000000000..51db3c5f393 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jcphuff-neon.c @@ -0,0 +1,623 @@ +/* + * jcphuff-neon.c - prepare data for progressive Huffman encoding (Arm Neon) + * + * Copyright (C) 2020-2021, Arm Limited. All Rights Reserved. + * Copyright (C) 2022, Matthieu Darbois. All Rights Reserved. + * Copyright (C) 2022, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "neon-compat.h" + +#include + + +/* Data preparation for encode_mcu_AC_first(). + * + * The equivalent scalar C function (encode_mcu_AC_first_prepare()) can be + * found in jcphuff.c. + */ + +void jsimd_encode_mcu_AC_first_prepare_neon + (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, + UJCOEF *values, size_t *zerobits) +{ + UJCOEF *values_ptr = values; + UJCOEF *diff_values_ptr = values + DCTSIZE2; + + /* Rows of coefficients to zero (since they haven't been processed) */ + int i, rows_to_zero = 8; + + for (i = 0; i < Sl / 16; i++) { + int16x8_t coefs1 = vld1q_dup_s16(block + jpeg_natural_order_start[0]); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[1], coefs1, 1); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[2], coefs1, 2); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[3], coefs1, 3); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[4], coefs1, 4); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[5], coefs1, 5); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[6], coefs1, 6); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[7], coefs1, 7); + int16x8_t coefs2 = vld1q_dup_s16(block + jpeg_natural_order_start[8]); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[9], coefs2, 1); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[10], coefs2, 2); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[11], coefs2, 3); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[12], coefs2, 4); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[13], coefs2, 5); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[14], coefs2, 6); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[15], coefs2, 7); + + /* Isolate sign of coefficients. */ + uint16x8_t sign_coefs1 = vreinterpretq_u16_s16(vshrq_n_s16(coefs1, 15)); + uint16x8_t sign_coefs2 = vreinterpretq_u16_s16(vshrq_n_s16(coefs2, 15)); + /* Compute absolute value of coefficients and apply point transform Al. */ + uint16x8_t abs_coefs1 = vreinterpretq_u16_s16(vabsq_s16(coefs1)); + uint16x8_t abs_coefs2 = vreinterpretq_u16_s16(vabsq_s16(coefs2)); + abs_coefs1 = vshlq_u16(abs_coefs1, vdupq_n_s16(-Al)); + abs_coefs2 = vshlq_u16(abs_coefs2, vdupq_n_s16(-Al)); + + /* Compute diff values. */ + uint16x8_t diff1 = veorq_u16(abs_coefs1, sign_coefs1); + uint16x8_t diff2 = veorq_u16(abs_coefs2, sign_coefs2); + + /* Store transformed coefficients and diff values. */ + vst1q_u16(values_ptr, abs_coefs1); + vst1q_u16(values_ptr + DCTSIZE, abs_coefs2); + vst1q_u16(diff_values_ptr, diff1); + vst1q_u16(diff_values_ptr + DCTSIZE, diff2); + values_ptr += 16; + diff_values_ptr += 16; + jpeg_natural_order_start += 16; + rows_to_zero -= 2; + } + + /* Same operation but for remaining partial vector */ + int remaining_coefs = Sl % 16; + if (remaining_coefs > 8) { + int16x8_t coefs1 = vld1q_dup_s16(block + jpeg_natural_order_start[0]); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[1], coefs1, 1); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[2], coefs1, 2); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[3], coefs1, 3); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[4], coefs1, 4); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[5], coefs1, 5); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[6], coefs1, 6); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[7], coefs1, 7); + int16x8_t coefs2 = vdupq_n_s16(0); + switch (remaining_coefs) { + case 15: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[14], coefs2, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 14: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[13], coefs2, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 13: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[12], coefs2, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 12: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[11], coefs2, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 11: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[10], coefs2, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 10: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[9], coefs2, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 9: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[8], coefs2, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } + + /* Isolate sign of coefficients. */ + uint16x8_t sign_coefs1 = vreinterpretq_u16_s16(vshrq_n_s16(coefs1, 15)); + uint16x8_t sign_coefs2 = vreinterpretq_u16_s16(vshrq_n_s16(coefs2, 15)); + /* Compute absolute value of coefficients and apply point transform Al. */ + uint16x8_t abs_coefs1 = vreinterpretq_u16_s16(vabsq_s16(coefs1)); + uint16x8_t abs_coefs2 = vreinterpretq_u16_s16(vabsq_s16(coefs2)); + abs_coefs1 = vshlq_u16(abs_coefs1, vdupq_n_s16(-Al)); + abs_coefs2 = vshlq_u16(abs_coefs2, vdupq_n_s16(-Al)); + + /* Compute diff values. */ + uint16x8_t diff1 = veorq_u16(abs_coefs1, sign_coefs1); + uint16x8_t diff2 = veorq_u16(abs_coefs2, sign_coefs2); + + /* Store transformed coefficients and diff values. */ + vst1q_u16(values_ptr, abs_coefs1); + vst1q_u16(values_ptr + DCTSIZE, abs_coefs2); + vst1q_u16(diff_values_ptr, diff1); + vst1q_u16(diff_values_ptr + DCTSIZE, diff2); + values_ptr += 16; + diff_values_ptr += 16; + rows_to_zero -= 2; + + } else if (remaining_coefs > 0) { + int16x8_t coefs = vdupq_n_s16(0); + + switch (remaining_coefs) { + case 8: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[7], coefs, 7); + FALLTHROUGH /*FALLTHROUGH*/ + case 7: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[6], coefs, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[5], coefs, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[4], coefs, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[3], coefs, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[2], coefs, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[1], coefs, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[0], coefs, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } + + /* Isolate sign of coefficients. */ + uint16x8_t sign_coefs = vreinterpretq_u16_s16(vshrq_n_s16(coefs, 15)); + /* Compute absolute value of coefficients and apply point transform Al. */ + uint16x8_t abs_coefs = vreinterpretq_u16_s16(vabsq_s16(coefs)); + abs_coefs = vshlq_u16(abs_coefs, vdupq_n_s16(-Al)); + + /* Compute diff values. */ + uint16x8_t diff = veorq_u16(abs_coefs, sign_coefs); + + /* Store transformed coefficients and diff values. */ + vst1q_u16(values_ptr, abs_coefs); + vst1q_u16(diff_values_ptr, diff); + values_ptr += 8; + diff_values_ptr += 8; + rows_to_zero--; + } + + /* Zero remaining memory in the values and diff_values blocks. */ + for (i = 0; i < rows_to_zero; i++) { + vst1q_u16(values_ptr, vdupq_n_u16(0)); + vst1q_u16(diff_values_ptr, vdupq_n_u16(0)); + values_ptr += 8; + diff_values_ptr += 8; + } + + /* Construct zerobits bitmap. A set bit means that the corresponding + * coefficient != 0. + */ + uint16x8_t row0 = vld1q_u16(values + 0 * DCTSIZE); + uint16x8_t row1 = vld1q_u16(values + 1 * DCTSIZE); + uint16x8_t row2 = vld1q_u16(values + 2 * DCTSIZE); + uint16x8_t row3 = vld1q_u16(values + 3 * DCTSIZE); + uint16x8_t row4 = vld1q_u16(values + 4 * DCTSIZE); + uint16x8_t row5 = vld1q_u16(values + 5 * DCTSIZE); + uint16x8_t row6 = vld1q_u16(values + 6 * DCTSIZE); + uint16x8_t row7 = vld1q_u16(values + 7 * DCTSIZE); + + uint8x8_t row0_eq0 = vmovn_u16(vceqq_u16(row0, vdupq_n_u16(0))); + uint8x8_t row1_eq0 = vmovn_u16(vceqq_u16(row1, vdupq_n_u16(0))); + uint8x8_t row2_eq0 = vmovn_u16(vceqq_u16(row2, vdupq_n_u16(0))); + uint8x8_t row3_eq0 = vmovn_u16(vceqq_u16(row3, vdupq_n_u16(0))); + uint8x8_t row4_eq0 = vmovn_u16(vceqq_u16(row4, vdupq_n_u16(0))); + uint8x8_t row5_eq0 = vmovn_u16(vceqq_u16(row5, vdupq_n_u16(0))); + uint8x8_t row6_eq0 = vmovn_u16(vceqq_u16(row6, vdupq_n_u16(0))); + uint8x8_t row7_eq0 = vmovn_u16(vceqq_u16(row7, vdupq_n_u16(0))); + + /* { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 } */ + const uint8x8_t bitmap_mask = + vreinterpret_u8_u64(vmov_n_u64(0x8040201008040201)); + + row0_eq0 = vand_u8(row0_eq0, bitmap_mask); + row1_eq0 = vand_u8(row1_eq0, bitmap_mask); + row2_eq0 = vand_u8(row2_eq0, bitmap_mask); + row3_eq0 = vand_u8(row3_eq0, bitmap_mask); + row4_eq0 = vand_u8(row4_eq0, bitmap_mask); + row5_eq0 = vand_u8(row5_eq0, bitmap_mask); + row6_eq0 = vand_u8(row6_eq0, bitmap_mask); + row7_eq0 = vand_u8(row7_eq0, bitmap_mask); + + uint8x8_t bitmap_rows_01 = vpadd_u8(row0_eq0, row1_eq0); + uint8x8_t bitmap_rows_23 = vpadd_u8(row2_eq0, row3_eq0); + uint8x8_t bitmap_rows_45 = vpadd_u8(row4_eq0, row5_eq0); + uint8x8_t bitmap_rows_67 = vpadd_u8(row6_eq0, row7_eq0); + uint8x8_t bitmap_rows_0123 = vpadd_u8(bitmap_rows_01, bitmap_rows_23); + uint8x8_t bitmap_rows_4567 = vpadd_u8(bitmap_rows_45, bitmap_rows_67); + uint8x8_t bitmap_all = vpadd_u8(bitmap_rows_0123, bitmap_rows_4567); + +#if defined(__aarch64__) || defined(_M_ARM64) + /* Move bitmap to a 64-bit scalar register. */ + uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(bitmap_all), 0); + /* Store zerobits bitmap. */ + *zerobits = ~bitmap; +#else + /* Move bitmap to two 32-bit scalar registers. */ + uint32_t bitmap0 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 0); + uint32_t bitmap1 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 1); + /* Store zerobits bitmap. */ + zerobits[0] = ~bitmap0; + zerobits[1] = ~bitmap1; +#endif +} + + +/* Data preparation for encode_mcu_AC_refine(). + * + * The equivalent scalar C function (encode_mcu_AC_refine_prepare()) can be + * found in jcphuff.c. + */ + +int jsimd_encode_mcu_AC_refine_prepare_neon + (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, + UJCOEF *absvalues, size_t *bits) +{ + /* Temporary storage buffers for data used to compute the signbits bitmap and + * the end-of-block (EOB) position + */ + uint8_t coef_sign_bits[64]; + uint8_t coef_eq1_bits[64]; + + UJCOEF *absvalues_ptr = absvalues; + uint8_t *coef_sign_bits_ptr = coef_sign_bits; + uint8_t *eq1_bits_ptr = coef_eq1_bits; + + /* Rows of coefficients to zero (since they haven't been processed) */ + int i, rows_to_zero = 8; + + for (i = 0; i < Sl / 16; i++) { + int16x8_t coefs1 = vld1q_dup_s16(block + jpeg_natural_order_start[0]); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[1], coefs1, 1); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[2], coefs1, 2); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[3], coefs1, 3); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[4], coefs1, 4); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[5], coefs1, 5); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[6], coefs1, 6); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[7], coefs1, 7); + int16x8_t coefs2 = vld1q_dup_s16(block + jpeg_natural_order_start[8]); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[9], coefs2, 1); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[10], coefs2, 2); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[11], coefs2, 3); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[12], coefs2, 4); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[13], coefs2, 5); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[14], coefs2, 6); + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[15], coefs2, 7); + + /* Compute and store data for signbits bitmap. */ + uint8x8_t sign_coefs1 = + vmovn_u16(vreinterpretq_u16_s16(vshrq_n_s16(coefs1, 15))); + uint8x8_t sign_coefs2 = + vmovn_u16(vreinterpretq_u16_s16(vshrq_n_s16(coefs2, 15))); + vst1_u8(coef_sign_bits_ptr, sign_coefs1); + vst1_u8(coef_sign_bits_ptr + DCTSIZE, sign_coefs2); + + /* Compute absolute value of coefficients and apply point transform Al. */ + uint16x8_t abs_coefs1 = vreinterpretq_u16_s16(vabsq_s16(coefs1)); + uint16x8_t abs_coefs2 = vreinterpretq_u16_s16(vabsq_s16(coefs2)); + abs_coefs1 = vshlq_u16(abs_coefs1, vdupq_n_s16(-Al)); + abs_coefs2 = vshlq_u16(abs_coefs2, vdupq_n_s16(-Al)); + vst1q_u16(absvalues_ptr, abs_coefs1); + vst1q_u16(absvalues_ptr + DCTSIZE, abs_coefs2); + + /* Test whether transformed coefficient values == 1 (used to find EOB + * position.) + */ + uint8x8_t coefs_eq11 = vmovn_u16(vceqq_u16(abs_coefs1, vdupq_n_u16(1))); + uint8x8_t coefs_eq12 = vmovn_u16(vceqq_u16(abs_coefs2, vdupq_n_u16(1))); + vst1_u8(eq1_bits_ptr, coefs_eq11); + vst1_u8(eq1_bits_ptr + DCTSIZE, coefs_eq12); + + absvalues_ptr += 16; + coef_sign_bits_ptr += 16; + eq1_bits_ptr += 16; + jpeg_natural_order_start += 16; + rows_to_zero -= 2; + } + + /* Same operation but for remaining partial vector */ + int remaining_coefs = Sl % 16; + if (remaining_coefs > 8) { + int16x8_t coefs1 = vld1q_dup_s16(block + jpeg_natural_order_start[0]); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[1], coefs1, 1); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[2], coefs1, 2); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[3], coefs1, 3); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[4], coefs1, 4); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[5], coefs1, 5); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[6], coefs1, 6); + coefs1 = vld1q_lane_s16(block + jpeg_natural_order_start[7], coefs1, 7); + int16x8_t coefs2 = vdupq_n_s16(0); + switch (remaining_coefs) { + case 15: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[14], coefs2, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 14: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[13], coefs2, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 13: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[12], coefs2, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 12: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[11], coefs2, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 11: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[10], coefs2, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 10: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[9], coefs2, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 9: + coefs2 = vld1q_lane_s16(block + jpeg_natural_order_start[8], coefs2, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } + + /* Compute and store data for signbits bitmap. */ + uint8x8_t sign_coefs1 = + vmovn_u16(vreinterpretq_u16_s16(vshrq_n_s16(coefs1, 15))); + uint8x8_t sign_coefs2 = + vmovn_u16(vreinterpretq_u16_s16(vshrq_n_s16(coefs2, 15))); + vst1_u8(coef_sign_bits_ptr, sign_coefs1); + vst1_u8(coef_sign_bits_ptr + DCTSIZE, sign_coefs2); + + /* Compute absolute value of coefficients and apply point transform Al. */ + uint16x8_t abs_coefs1 = vreinterpretq_u16_s16(vabsq_s16(coefs1)); + uint16x8_t abs_coefs2 = vreinterpretq_u16_s16(vabsq_s16(coefs2)); + abs_coefs1 = vshlq_u16(abs_coefs1, vdupq_n_s16(-Al)); + abs_coefs2 = vshlq_u16(abs_coefs2, vdupq_n_s16(-Al)); + vst1q_u16(absvalues_ptr, abs_coefs1); + vst1q_u16(absvalues_ptr + DCTSIZE, abs_coefs2); + + /* Test whether transformed coefficient values == 1 (used to find EOB + * position.) + */ + uint8x8_t coefs_eq11 = vmovn_u16(vceqq_u16(abs_coefs1, vdupq_n_u16(1))); + uint8x8_t coefs_eq12 = vmovn_u16(vceqq_u16(abs_coefs2, vdupq_n_u16(1))); + vst1_u8(eq1_bits_ptr, coefs_eq11); + vst1_u8(eq1_bits_ptr + DCTSIZE, coefs_eq12); + + absvalues_ptr += 16; + coef_sign_bits_ptr += 16; + eq1_bits_ptr += 16; + jpeg_natural_order_start += 16; + rows_to_zero -= 2; + + } else if (remaining_coefs > 0) { + int16x8_t coefs = vdupq_n_s16(0); + + switch (remaining_coefs) { + case 8: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[7], coefs, 7); + FALLTHROUGH /*FALLTHROUGH*/ + case 7: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[6], coefs, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[5], coefs, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[4], coefs, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[3], coefs, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[2], coefs, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[1], coefs, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + coefs = vld1q_lane_s16(block + jpeg_natural_order_start[0], coefs, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } + + /* Compute and store data for signbits bitmap. */ + uint8x8_t sign_coefs = + vmovn_u16(vreinterpretq_u16_s16(vshrq_n_s16(coefs, 15))); + vst1_u8(coef_sign_bits_ptr, sign_coefs); + + /* Compute absolute value of coefficients and apply point transform Al. */ + uint16x8_t abs_coefs = vreinterpretq_u16_s16(vabsq_s16(coefs)); + abs_coefs = vshlq_u16(abs_coefs, vdupq_n_s16(-Al)); + vst1q_u16(absvalues_ptr, abs_coefs); + + /* Test whether transformed coefficient values == 1 (used to find EOB + * position.) + */ + uint8x8_t coefs_eq1 = vmovn_u16(vceqq_u16(abs_coefs, vdupq_n_u16(1))); + vst1_u8(eq1_bits_ptr, coefs_eq1); + + absvalues_ptr += 8; + coef_sign_bits_ptr += 8; + eq1_bits_ptr += 8; + rows_to_zero--; + } + + /* Zero remaining memory in blocks. */ + for (i = 0; i < rows_to_zero; i++) { + vst1q_u16(absvalues_ptr, vdupq_n_u16(0)); + vst1_u8(coef_sign_bits_ptr, vdup_n_u8(0)); + vst1_u8(eq1_bits_ptr, vdup_n_u8(0)); + absvalues_ptr += 8; + coef_sign_bits_ptr += 8; + eq1_bits_ptr += 8; + } + + /* Construct zerobits bitmap. */ + uint16x8_t abs_row0 = vld1q_u16(absvalues + 0 * DCTSIZE); + uint16x8_t abs_row1 = vld1q_u16(absvalues + 1 * DCTSIZE); + uint16x8_t abs_row2 = vld1q_u16(absvalues + 2 * DCTSIZE); + uint16x8_t abs_row3 = vld1q_u16(absvalues + 3 * DCTSIZE); + uint16x8_t abs_row4 = vld1q_u16(absvalues + 4 * DCTSIZE); + uint16x8_t abs_row5 = vld1q_u16(absvalues + 5 * DCTSIZE); + uint16x8_t abs_row6 = vld1q_u16(absvalues + 6 * DCTSIZE); + uint16x8_t abs_row7 = vld1q_u16(absvalues + 7 * DCTSIZE); + + uint8x8_t abs_row0_eq0 = vmovn_u16(vceqq_u16(abs_row0, vdupq_n_u16(0))); + uint8x8_t abs_row1_eq0 = vmovn_u16(vceqq_u16(abs_row1, vdupq_n_u16(0))); + uint8x8_t abs_row2_eq0 = vmovn_u16(vceqq_u16(abs_row2, vdupq_n_u16(0))); + uint8x8_t abs_row3_eq0 = vmovn_u16(vceqq_u16(abs_row3, vdupq_n_u16(0))); + uint8x8_t abs_row4_eq0 = vmovn_u16(vceqq_u16(abs_row4, vdupq_n_u16(0))); + uint8x8_t abs_row5_eq0 = vmovn_u16(vceqq_u16(abs_row5, vdupq_n_u16(0))); + uint8x8_t abs_row6_eq0 = vmovn_u16(vceqq_u16(abs_row6, vdupq_n_u16(0))); + uint8x8_t abs_row7_eq0 = vmovn_u16(vceqq_u16(abs_row7, vdupq_n_u16(0))); + + /* { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 } */ + const uint8x8_t bitmap_mask = + vreinterpret_u8_u64(vmov_n_u64(0x8040201008040201)); + + abs_row0_eq0 = vand_u8(abs_row0_eq0, bitmap_mask); + abs_row1_eq0 = vand_u8(abs_row1_eq0, bitmap_mask); + abs_row2_eq0 = vand_u8(abs_row2_eq0, bitmap_mask); + abs_row3_eq0 = vand_u8(abs_row3_eq0, bitmap_mask); + abs_row4_eq0 = vand_u8(abs_row4_eq0, bitmap_mask); + abs_row5_eq0 = vand_u8(abs_row5_eq0, bitmap_mask); + abs_row6_eq0 = vand_u8(abs_row6_eq0, bitmap_mask); + abs_row7_eq0 = vand_u8(abs_row7_eq0, bitmap_mask); + + uint8x8_t bitmap_rows_01 = vpadd_u8(abs_row0_eq0, abs_row1_eq0); + uint8x8_t bitmap_rows_23 = vpadd_u8(abs_row2_eq0, abs_row3_eq0); + uint8x8_t bitmap_rows_45 = vpadd_u8(abs_row4_eq0, abs_row5_eq0); + uint8x8_t bitmap_rows_67 = vpadd_u8(abs_row6_eq0, abs_row7_eq0); + uint8x8_t bitmap_rows_0123 = vpadd_u8(bitmap_rows_01, bitmap_rows_23); + uint8x8_t bitmap_rows_4567 = vpadd_u8(bitmap_rows_45, bitmap_rows_67); + uint8x8_t bitmap_all = vpadd_u8(bitmap_rows_0123, bitmap_rows_4567); + +#if defined(__aarch64__) || defined(_M_ARM64) + /* Move bitmap to a 64-bit scalar register. */ + uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(bitmap_all), 0); + /* Store zerobits bitmap. */ + bits[0] = ~bitmap; +#else + /* Move bitmap to two 32-bit scalar registers. */ + uint32_t bitmap0 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 0); + uint32_t bitmap1 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 1); + /* Store zerobits bitmap. */ + bits[0] = ~bitmap0; + bits[1] = ~bitmap1; +#endif + + /* Construct signbits bitmap. */ + uint8x8_t signbits_row0 = vld1_u8(coef_sign_bits + 0 * DCTSIZE); + uint8x8_t signbits_row1 = vld1_u8(coef_sign_bits + 1 * DCTSIZE); + uint8x8_t signbits_row2 = vld1_u8(coef_sign_bits + 2 * DCTSIZE); + uint8x8_t signbits_row3 = vld1_u8(coef_sign_bits + 3 * DCTSIZE); + uint8x8_t signbits_row4 = vld1_u8(coef_sign_bits + 4 * DCTSIZE); + uint8x8_t signbits_row5 = vld1_u8(coef_sign_bits + 5 * DCTSIZE); + uint8x8_t signbits_row6 = vld1_u8(coef_sign_bits + 6 * DCTSIZE); + uint8x8_t signbits_row7 = vld1_u8(coef_sign_bits + 7 * DCTSIZE); + + signbits_row0 = vand_u8(signbits_row0, bitmap_mask); + signbits_row1 = vand_u8(signbits_row1, bitmap_mask); + signbits_row2 = vand_u8(signbits_row2, bitmap_mask); + signbits_row3 = vand_u8(signbits_row3, bitmap_mask); + signbits_row4 = vand_u8(signbits_row4, bitmap_mask); + signbits_row5 = vand_u8(signbits_row5, bitmap_mask); + signbits_row6 = vand_u8(signbits_row6, bitmap_mask); + signbits_row7 = vand_u8(signbits_row7, bitmap_mask); + + bitmap_rows_01 = vpadd_u8(signbits_row0, signbits_row1); + bitmap_rows_23 = vpadd_u8(signbits_row2, signbits_row3); + bitmap_rows_45 = vpadd_u8(signbits_row4, signbits_row5); + bitmap_rows_67 = vpadd_u8(signbits_row6, signbits_row7); + bitmap_rows_0123 = vpadd_u8(bitmap_rows_01, bitmap_rows_23); + bitmap_rows_4567 = vpadd_u8(bitmap_rows_45, bitmap_rows_67); + bitmap_all = vpadd_u8(bitmap_rows_0123, bitmap_rows_4567); + +#if defined(__aarch64__) || defined(_M_ARM64) + /* Move bitmap to a 64-bit scalar register. */ + bitmap = vget_lane_u64(vreinterpret_u64_u8(bitmap_all), 0); + /* Store signbits bitmap. */ + bits[1] = ~bitmap; +#else + /* Move bitmap to two 32-bit scalar registers. */ + bitmap0 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 0); + bitmap1 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 1); + /* Store signbits bitmap. */ + bits[2] = ~bitmap0; + bits[3] = ~bitmap1; +#endif + + /* Construct bitmap to find EOB position (the index of the last coefficient + * equal to 1.) + */ + uint8x8_t row0_eq1 = vld1_u8(coef_eq1_bits + 0 * DCTSIZE); + uint8x8_t row1_eq1 = vld1_u8(coef_eq1_bits + 1 * DCTSIZE); + uint8x8_t row2_eq1 = vld1_u8(coef_eq1_bits + 2 * DCTSIZE); + uint8x8_t row3_eq1 = vld1_u8(coef_eq1_bits + 3 * DCTSIZE); + uint8x8_t row4_eq1 = vld1_u8(coef_eq1_bits + 4 * DCTSIZE); + uint8x8_t row5_eq1 = vld1_u8(coef_eq1_bits + 5 * DCTSIZE); + uint8x8_t row6_eq1 = vld1_u8(coef_eq1_bits + 6 * DCTSIZE); + uint8x8_t row7_eq1 = vld1_u8(coef_eq1_bits + 7 * DCTSIZE); + + row0_eq1 = vand_u8(row0_eq1, bitmap_mask); + row1_eq1 = vand_u8(row1_eq1, bitmap_mask); + row2_eq1 = vand_u8(row2_eq1, bitmap_mask); + row3_eq1 = vand_u8(row3_eq1, bitmap_mask); + row4_eq1 = vand_u8(row4_eq1, bitmap_mask); + row5_eq1 = vand_u8(row5_eq1, bitmap_mask); + row6_eq1 = vand_u8(row6_eq1, bitmap_mask); + row7_eq1 = vand_u8(row7_eq1, bitmap_mask); + + bitmap_rows_01 = vpadd_u8(row0_eq1, row1_eq1); + bitmap_rows_23 = vpadd_u8(row2_eq1, row3_eq1); + bitmap_rows_45 = vpadd_u8(row4_eq1, row5_eq1); + bitmap_rows_67 = vpadd_u8(row6_eq1, row7_eq1); + bitmap_rows_0123 = vpadd_u8(bitmap_rows_01, bitmap_rows_23); + bitmap_rows_4567 = vpadd_u8(bitmap_rows_45, bitmap_rows_67); + bitmap_all = vpadd_u8(bitmap_rows_0123, bitmap_rows_4567); + +#if defined(__aarch64__) || defined(_M_ARM64) + /* Move bitmap to a 64-bit scalar register. */ + bitmap = vget_lane_u64(vreinterpret_u64_u8(bitmap_all), 0); + + /* Return EOB position. */ + if (bitmap == 0) { + /* EOB position is defined to be 0 if all coefficients != 1. */ + return 0; + } else { + return 63 - BUILTIN_CLZLL(bitmap); + } +#else + /* Move bitmap to two 32-bit scalar registers. */ + bitmap0 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 0); + bitmap1 = vget_lane_u32(vreinterpret_u32_u8(bitmap_all), 1); + + /* Return EOB position. */ + if (bitmap0 == 0 && bitmap1 == 0) { + return 0; + } else if (bitmap1 != 0) { + return 63 - BUILTIN_CLZ(bitmap1); + } else { + return 31 - BUILTIN_CLZ(bitmap0); + } +#endif +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jcsample-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jcsample-neon.c new file mode 100644 index 00000000000..8a3e237838e --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jcsample-neon.c @@ -0,0 +1,192 @@ +/* + * jcsample-neon.c - downsampling (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" + +#include + + +ALIGN(16) static const uint8_t jsimd_h2_downsample_consts[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 0 */ + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 1 */ + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0E, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 2 */ + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0D, 0x0D, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 3 */ + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 4 */ + 0x08, 0x09, 0x0A, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 5 */ + 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 6 */ + 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 7 */ + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* Pad 8 */ + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x06, /* Pad 9 */ + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x05, 0x05, /* Pad 10 */ + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x04, 0x04, 0x04, /* Pad 11 */ + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x01, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, /* Pad 12 */ + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x00, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* Pad 13 */ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, /* Pad 14 */ + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Pad 15 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + + +/* Downsample pixel values of a single component. + * This version handles the common case of 2:1 horizontal and 1:1 vertical, + * without smoothing. + */ + +void jsimd_h2v1_downsample_neon(JDIMENSION image_width, int max_v_samp_factor, + JDIMENSION v_samp_factor, + JDIMENSION width_in_blocks, + JSAMPARRAY input_data, JSAMPARRAY output_data) +{ + JSAMPROW inptr, outptr; + /* Load expansion mask to pad remaining elements of last DCT block. */ + const int mask_offset = 16 * ((width_in_blocks * 2 * DCTSIZE) - image_width); + const uint8x16_t expand_mask = + vld1q_u8(&jsimd_h2_downsample_consts[mask_offset]); + /* Load bias pattern (alternating every pixel.) */ + /* { 0, 1, 0, 1, 0, 1, 0, 1 } */ + const uint16x8_t bias = vreinterpretq_u16_u32(vdupq_n_u32(0x00010000)); + unsigned i, outrow; + + for (outrow = 0; outrow < v_samp_factor; outrow++) { + outptr = output_data[outrow]; + inptr = input_data[outrow]; + + /* Downsample all but the last DCT block of pixels. */ + for (i = 0; i < width_in_blocks - 1; i++) { + uint8x16_t pixels = vld1q_u8(inptr + i * 2 * DCTSIZE); + /* Add adjacent pixel values, widen to 16-bit, and add bias. */ + uint16x8_t samples_u16 = vpadalq_u8(bias, pixels); + /* Divide total by 2 and narrow to 8-bit. */ + uint8x8_t samples_u8 = vshrn_n_u16(samples_u16, 1); + /* Store samples to memory. */ + vst1_u8(outptr + i * DCTSIZE, samples_u8); + } + + /* Load pixels in last DCT block into a table. */ + uint8x16_t pixels = vld1q_u8(inptr + (width_in_blocks - 1) * 2 * DCTSIZE); +#if defined(__aarch64__) || defined(_M_ARM64) + /* Pad the empty elements with the value of the last pixel. */ + pixels = vqtbl1q_u8(pixels, expand_mask); +#else + uint8x8x2_t table = { { vget_low_u8(pixels), vget_high_u8(pixels) } }; + pixels = vcombine_u8(vtbl2_u8(table, vget_low_u8(expand_mask)), + vtbl2_u8(table, vget_high_u8(expand_mask))); +#endif + /* Add adjacent pixel values, widen to 16-bit, and add bias. */ + uint16x8_t samples_u16 = vpadalq_u8(bias, pixels); + /* Divide total by 2, narrow to 8-bit, and store. */ + uint8x8_t samples_u8 = vshrn_n_u16(samples_u16, 1); + vst1_u8(outptr + (width_in_blocks - 1) * DCTSIZE, samples_u8); + } +} + + +/* Downsample pixel values of a single component. + * This version handles the standard case of 2:1 horizontal and 2:1 vertical, + * without smoothing. + */ + +void jsimd_h2v2_downsample_neon(JDIMENSION image_width, int max_v_samp_factor, + JDIMENSION v_samp_factor, + JDIMENSION width_in_blocks, + JSAMPARRAY input_data, JSAMPARRAY output_data) +{ + JSAMPROW inptr0, inptr1, outptr; + /* Load expansion mask to pad remaining elements of last DCT block. */ + const int mask_offset = 16 * ((width_in_blocks * 2 * DCTSIZE) - image_width); + const uint8x16_t expand_mask = + vld1q_u8(&jsimd_h2_downsample_consts[mask_offset]); + /* Load bias pattern (alternating every pixel.) */ + /* { 1, 2, 1, 2, 1, 2, 1, 2 } */ + const uint16x8_t bias = vreinterpretq_u16_u32(vdupq_n_u32(0x00020001)); + unsigned i, outrow; + + for (outrow = 0; outrow < v_samp_factor; outrow++) { + outptr = output_data[outrow]; + inptr0 = input_data[outrow]; + inptr1 = input_data[outrow + 1]; + + /* Downsample all but the last DCT block of pixels. */ + for (i = 0; i < width_in_blocks - 1; i++) { + uint8x16_t pixels_r0 = vld1q_u8(inptr0 + i * 2 * DCTSIZE); + uint8x16_t pixels_r1 = vld1q_u8(inptr1 + i * 2 * DCTSIZE); + /* Add adjacent pixel values in row 0, widen to 16-bit, and add bias. */ + uint16x8_t samples_u16 = vpadalq_u8(bias, pixels_r0); + /* Add adjacent pixel values in row 1, widen to 16-bit, and accumulate. + */ + samples_u16 = vpadalq_u8(samples_u16, pixels_r1); + /* Divide total by 4 and narrow to 8-bit. */ + uint8x8_t samples_u8 = vshrn_n_u16(samples_u16, 2); + /* Store samples to memory and increment pointers. */ + vst1_u8(outptr + i * DCTSIZE, samples_u8); + } + + /* Load pixels in last DCT block into a table. */ + uint8x16_t pixels_r0 = + vld1q_u8(inptr0 + (width_in_blocks - 1) * 2 * DCTSIZE); + uint8x16_t pixels_r1 = + vld1q_u8(inptr1 + (width_in_blocks - 1) * 2 * DCTSIZE); +#if defined(__aarch64__) || defined(_M_ARM64) + /* Pad the empty elements with the value of the last pixel. */ + pixels_r0 = vqtbl1q_u8(pixels_r0, expand_mask); + pixels_r1 = vqtbl1q_u8(pixels_r1, expand_mask); +#else + uint8x8x2_t table_r0 = + { { vget_low_u8(pixels_r0), vget_high_u8(pixels_r0) } }; + uint8x8x2_t table_r1 = + { { vget_low_u8(pixels_r1), vget_high_u8(pixels_r1) } }; + pixels_r0 = vcombine_u8(vtbl2_u8(table_r0, vget_low_u8(expand_mask)), + vtbl2_u8(table_r0, vget_high_u8(expand_mask))); + pixels_r1 = vcombine_u8(vtbl2_u8(table_r1, vget_low_u8(expand_mask)), + vtbl2_u8(table_r1, vget_high_u8(expand_mask))); +#endif + /* Add adjacent pixel values in row 0, widen to 16-bit, and add bias. */ + uint16x8_t samples_u16 = vpadalq_u8(bias, pixels_r0); + /* Add adjacent pixel values in row 1, widen to 16-bit, and accumulate. */ + samples_u16 = vpadalq_u8(samples_u16, pixels_r1); + /* Divide total by 4, narrow to 8-bit, and store. */ + uint8x8_t samples_u8 = vshrn_n_u16(samples_u16, 2); + vst1_u8(outptr + (width_in_blocks - 1) * DCTSIZE, samples_u8); + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jdcolext-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jdcolext-neon.c new file mode 100644 index 00000000000..c3c07a19645 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jdcolext-neon.c @@ -0,0 +1,374 @@ +/* + * jdcolext-neon.c - colorspace conversion (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jdcolor-neon.c. */ + + +/* YCbCr -> RGB conversion is defined by the following equations: + * R = Y + 1.40200 * (Cr - 128) + * G = Y - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) + * B = Y + 1.77200 * (Cb - 128) + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.3441467 = 11277 * 2^-15 + * 0.7141418 = 23401 * 2^-15 + * 1.4020386 = 22971 * 2^-14 + * 1.7720337 = 29033 * 2^-14 + * These constants are defined in jdcolor-neon.c. + * + * To ensure correct results, rounding is used when descaling. + */ + +/* Notes on safe memory access for YCbCr -> RGB conversion routines: + * + * Input memory buffers can be safely overread up to the next multiple of + * ALIGN_SIZE bytes, since they are always allocated by alloc_sarray() in + * jmemmgr.c. + * + * The output buffer cannot safely be written beyond output_width, since + * output_buf points to a possibly unpadded row in the decompressed image + * buffer allocated by the calling program. + */ + +void jsimd_ycc_rgb_convert_neon(JDIMENSION output_width, JSAMPIMAGE input_buf, + JDIMENSION input_row, JSAMPARRAY output_buf, + int num_rows) +{ + JSAMPROW outptr; + /* Pointers to Y, Cb, and Cr data */ + JSAMPROW inptr0, inptr1, inptr2; + + const int16x4_t consts = vld1_s16(jsimd_ycc_rgb_convert_neon_consts); + const int16x8_t neg_128 = vdupq_n_s16(-128); + + while (--num_rows >= 0) { + inptr0 = input_buf[0][input_row]; + inptr1 = input_buf[1][input_row]; + inptr2 = input_buf[2][input_row]; + input_row++; + outptr = *output_buf++; + int cols_remaining = output_width; + for (; cols_remaining >= 16; cols_remaining -= 16) { + uint8x16_t y = vld1q_u8(inptr0); + uint8x16_t cb = vld1q_u8(inptr1); + uint8x16_t cr = vld1q_u8(inptr2); + /* Subtract 128 from Cb and Cr. */ + int16x8_t cr_128_l = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), + vget_low_u8(cr))); + int16x8_t cr_128_h = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), + vget_high_u8(cr))); + int16x8_t cb_128_l = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), + vget_low_u8(cb))); + int16x8_t cb_128_h = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), + vget_high_u8(cb))); + /* Compute G-Y: - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) */ + int32x4_t g_sub_y_ll = vmull_lane_s16(vget_low_s16(cb_128_l), consts, 0); + int32x4_t g_sub_y_lh = vmull_lane_s16(vget_high_s16(cb_128_l), + consts, 0); + int32x4_t g_sub_y_hl = vmull_lane_s16(vget_low_s16(cb_128_h), consts, 0); + int32x4_t g_sub_y_hh = vmull_lane_s16(vget_high_s16(cb_128_h), + consts, 0); + g_sub_y_ll = vmlsl_lane_s16(g_sub_y_ll, vget_low_s16(cr_128_l), + consts, 1); + g_sub_y_lh = vmlsl_lane_s16(g_sub_y_lh, vget_high_s16(cr_128_l), + consts, 1); + g_sub_y_hl = vmlsl_lane_s16(g_sub_y_hl, vget_low_s16(cr_128_h), + consts, 1); + g_sub_y_hh = vmlsl_lane_s16(g_sub_y_hh, vget_high_s16(cr_128_h), + consts, 1); + /* Descale G components: shift right 15, round, and narrow to 16-bit. */ + int16x8_t g_sub_y_l = vcombine_s16(vrshrn_n_s32(g_sub_y_ll, 15), + vrshrn_n_s32(g_sub_y_lh, 15)); + int16x8_t g_sub_y_h = vcombine_s16(vrshrn_n_s32(g_sub_y_hl, 15), + vrshrn_n_s32(g_sub_y_hh, 15)); + /* Compute R-Y: 1.40200 * (Cr - 128) */ + int16x8_t r_sub_y_l = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128_l, 1), + consts, 2); + int16x8_t r_sub_y_h = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128_h, 1), + consts, 2); + /* Compute B-Y: 1.77200 * (Cb - 128) */ + int16x8_t b_sub_y_l = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128_l, 1), + consts, 3); + int16x8_t b_sub_y_h = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128_h, 1), + consts, 3); + /* Add Y. */ + int16x8_t r_l = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y_l), + vget_low_u8(y))); + int16x8_t r_h = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y_h), + vget_high_u8(y))); + int16x8_t b_l = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y_l), + vget_low_u8(y))); + int16x8_t b_h = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y_h), + vget_high_u8(y))); + int16x8_t g_l = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y_l), + vget_low_u8(y))); + int16x8_t g_h = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y_h), + vget_high_u8(y))); + +#if RGB_PIXELSIZE == 4 + uint8x16x4_t rgba; + /* Convert each component to unsigned and narrow, clamping to [0-255]. */ + rgba.val[RGB_RED] = vcombine_u8(vqmovun_s16(r_l), vqmovun_s16(r_h)); + rgba.val[RGB_GREEN] = vcombine_u8(vqmovun_s16(g_l), vqmovun_s16(g_h)); + rgba.val[RGB_BLUE] = vcombine_u8(vqmovun_s16(b_l), vqmovun_s16(b_h)); + /* Set alpha channel to opaque (0xFF). */ + rgba.val[RGB_ALPHA] = vdupq_n_u8(0xFF); + /* Store RGBA pixel data to memory. */ + vst4q_u8(outptr, rgba); +#elif RGB_PIXELSIZE == 3 + uint8x16x3_t rgb; + /* Convert each component to unsigned and narrow, clamping to [0-255]. */ + rgb.val[RGB_RED] = vcombine_u8(vqmovun_s16(r_l), vqmovun_s16(r_h)); + rgb.val[RGB_GREEN] = vcombine_u8(vqmovun_s16(g_l), vqmovun_s16(g_h)); + rgb.val[RGB_BLUE] = vcombine_u8(vqmovun_s16(b_l), vqmovun_s16(b_h)); + /* Store RGB pixel data to memory. */ + vst3q_u8(outptr, rgb); +#else + /* Pack R, G, and B values in ratio 5:6:5. */ + uint16x8_t rgb565_l = vqshluq_n_s16(r_l, 8); + rgb565_l = vsriq_n_u16(rgb565_l, vqshluq_n_s16(g_l, 8), 5); + rgb565_l = vsriq_n_u16(rgb565_l, vqshluq_n_s16(b_l, 8), 11); + uint16x8_t rgb565_h = vqshluq_n_s16(r_h, 8); + rgb565_h = vsriq_n_u16(rgb565_h, vqshluq_n_s16(g_h, 8), 5); + rgb565_h = vsriq_n_u16(rgb565_h, vqshluq_n_s16(b_h, 8), 11); + /* Store RGB pixel data to memory. */ + vst1q_u16((uint16_t *)outptr, rgb565_l); + vst1q_u16(((uint16_t *)outptr) + 8, rgb565_h); +#endif + + /* Increment pointers. */ + inptr0 += 16; + inptr1 += 16; + inptr2 += 16; + outptr += (RGB_PIXELSIZE * 16); + } + + if (cols_remaining >= 8) { + uint8x8_t y = vld1_u8(inptr0); + uint8x8_t cb = vld1_u8(inptr1); + uint8x8_t cr = vld1_u8(inptr2); + /* Subtract 128 from Cb and Cr. */ + int16x8_t cr_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cr)); + int16x8_t cb_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cb)); + /* Compute G-Y: - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) */ + int32x4_t g_sub_y_l = vmull_lane_s16(vget_low_s16(cb_128), consts, 0); + int32x4_t g_sub_y_h = vmull_lane_s16(vget_high_s16(cb_128), consts, 0); + g_sub_y_l = vmlsl_lane_s16(g_sub_y_l, vget_low_s16(cr_128), consts, 1); + g_sub_y_h = vmlsl_lane_s16(g_sub_y_h, vget_high_s16(cr_128), consts, 1); + /* Descale G components: shift right 15, round, and narrow to 16-bit. */ + int16x8_t g_sub_y = vcombine_s16(vrshrn_n_s32(g_sub_y_l, 15), + vrshrn_n_s32(g_sub_y_h, 15)); + /* Compute R-Y: 1.40200 * (Cr - 128) */ + int16x8_t r_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128, 1), + consts, 2); + /* Compute B-Y: 1.77200 * (Cb - 128) */ + int16x8_t b_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128, 1), + consts, 3); + /* Add Y. */ + int16x8_t r = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), y)); + int16x8_t b = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), y)); + int16x8_t g = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), y)); + +#if RGB_PIXELSIZE == 4 + uint8x8x4_t rgba; + /* Convert each component to unsigned and narrow, clamping to [0-255]. */ + rgba.val[RGB_RED] = vqmovun_s16(r); + rgba.val[RGB_GREEN] = vqmovun_s16(g); + rgba.val[RGB_BLUE] = vqmovun_s16(b); + /* Set alpha channel to opaque (0xFF). */ + rgba.val[RGB_ALPHA] = vdup_n_u8(0xFF); + /* Store RGBA pixel data to memory. */ + vst4_u8(outptr, rgba); +#elif RGB_PIXELSIZE == 3 + uint8x8x3_t rgb; + /* Convert each component to unsigned and narrow, clamping to [0-255]. */ + rgb.val[RGB_RED] = vqmovun_s16(r); + rgb.val[RGB_GREEN] = vqmovun_s16(g); + rgb.val[RGB_BLUE] = vqmovun_s16(b); + /* Store RGB pixel data to memory. */ + vst3_u8(outptr, rgb); +#else + /* Pack R, G, and B values in ratio 5:6:5. */ + uint16x8_t rgb565 = vqshluq_n_s16(r, 8); + rgb565 = vsriq_n_u16(rgb565, vqshluq_n_s16(g, 8), 5); + rgb565 = vsriq_n_u16(rgb565, vqshluq_n_s16(b, 8), 11); + /* Store RGB pixel data to memory. */ + vst1q_u16((uint16_t *)outptr, rgb565); +#endif + + /* Increment pointers. */ + inptr0 += 8; + inptr1 += 8; + inptr2 += 8; + outptr += (RGB_PIXELSIZE * 8); + cols_remaining -= 8; + } + + /* Handle the tail elements. */ + if (cols_remaining > 0) { + uint8x8_t y = vld1_u8(inptr0); + uint8x8_t cb = vld1_u8(inptr1); + uint8x8_t cr = vld1_u8(inptr2); + /* Subtract 128 from Cb and Cr. */ + int16x8_t cr_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cr)); + int16x8_t cb_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cb)); + /* Compute G-Y: - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) */ + int32x4_t g_sub_y_l = vmull_lane_s16(vget_low_s16(cb_128), consts, 0); + int32x4_t g_sub_y_h = vmull_lane_s16(vget_high_s16(cb_128), consts, 0); + g_sub_y_l = vmlsl_lane_s16(g_sub_y_l, vget_low_s16(cr_128), consts, 1); + g_sub_y_h = vmlsl_lane_s16(g_sub_y_h, vget_high_s16(cr_128), consts, 1); + /* Descale G components: shift right 15, round, and narrow to 16-bit. */ + int16x8_t g_sub_y = vcombine_s16(vrshrn_n_s32(g_sub_y_l, 15), + vrshrn_n_s32(g_sub_y_h, 15)); + /* Compute R-Y: 1.40200 * (Cr - 128) */ + int16x8_t r_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128, 1), + consts, 2); + /* Compute B-Y: 1.77200 * (Cb - 128) */ + int16x8_t b_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128, 1), + consts, 3); + /* Add Y. */ + int16x8_t r = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), y)); + int16x8_t b = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), y)); + int16x8_t g = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), y)); + +#if RGB_PIXELSIZE == 4 + uint8x8x4_t rgba; + /* Convert each component to unsigned and narrow, clamping to [0-255]. */ + rgba.val[RGB_RED] = vqmovun_s16(r); + rgba.val[RGB_GREEN] = vqmovun_s16(g); + rgba.val[RGB_BLUE] = vqmovun_s16(b); + /* Set alpha channel to opaque (0xFF). */ + rgba.val[RGB_ALPHA] = vdup_n_u8(0xFF); + /* Store RGBA pixel data to memory. */ + switch (cols_remaining) { + case 7: + vst4_lane_u8(outptr + 6 * RGB_PIXELSIZE, rgba, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + vst4_lane_u8(outptr + 5 * RGB_PIXELSIZE, rgba, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + vst4_lane_u8(outptr + 4 * RGB_PIXELSIZE, rgba, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + vst4_lane_u8(outptr + 3 * RGB_PIXELSIZE, rgba, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + vst4_lane_u8(outptr + 2 * RGB_PIXELSIZE, rgba, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + vst4_lane_u8(outptr + RGB_PIXELSIZE, rgba, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + vst4_lane_u8(outptr, rgba, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } +#elif RGB_PIXELSIZE == 3 + uint8x8x3_t rgb; + /* Convert each component to unsigned and narrow, clamping to [0-255]. */ + rgb.val[RGB_RED] = vqmovun_s16(r); + rgb.val[RGB_GREEN] = vqmovun_s16(g); + rgb.val[RGB_BLUE] = vqmovun_s16(b); + /* Store RGB pixel data to memory. */ + switch (cols_remaining) { + case 7: + vst3_lane_u8(outptr + 6 * RGB_PIXELSIZE, rgb, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + vst3_lane_u8(outptr + 5 * RGB_PIXELSIZE, rgb, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + vst3_lane_u8(outptr + 4 * RGB_PIXELSIZE, rgb, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + vst3_lane_u8(outptr + 3 * RGB_PIXELSIZE, rgb, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + vst3_lane_u8(outptr + 2 * RGB_PIXELSIZE, rgb, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + vst3_lane_u8(outptr + RGB_PIXELSIZE, rgb, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + vst3_lane_u8(outptr, rgb, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } +#else + /* Pack R, G, and B values in ratio 5:6:5. */ + uint16x8_t rgb565 = vqshluq_n_s16(r, 8); + rgb565 = vsriq_n_u16(rgb565, vqshluq_n_s16(g, 8), 5); + rgb565 = vsriq_n_u16(rgb565, vqshluq_n_s16(b, 8), 11); + /* Store RGB565 pixel data to memory. */ + switch (cols_remaining) { + case 7: + vst1q_lane_u16((uint16_t *)(outptr + 6 * RGB_PIXELSIZE), rgb565, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + vst1q_lane_u16((uint16_t *)(outptr + 5 * RGB_PIXELSIZE), rgb565, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + vst1q_lane_u16((uint16_t *)(outptr + 4 * RGB_PIXELSIZE), rgb565, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + vst1q_lane_u16((uint16_t *)(outptr + 3 * RGB_PIXELSIZE), rgb565, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + vst1q_lane_u16((uint16_t *)(outptr + 2 * RGB_PIXELSIZE), rgb565, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + vst1q_lane_u16((uint16_t *)(outptr + RGB_PIXELSIZE), rgb565, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + vst1q_lane_u16((uint16_t *)outptr, rgb565, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } +#endif + } + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jdcolor-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jdcolor-neon.c new file mode 100644 index 00000000000..28dbc57243c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jdcolor-neon.c @@ -0,0 +1,141 @@ +/* + * jdcolor-neon.c - colorspace conversion (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" + +#include + + +/* YCbCr -> RGB conversion constants */ + +#define F_0_344 11277 /* 0.3441467 = 11277 * 2^-15 */ +#define F_0_714 23401 /* 0.7141418 = 23401 * 2^-15 */ +#define F_1_402 22971 /* 1.4020386 = 22971 * 2^-14 */ +#define F_1_772 29033 /* 1.7720337 = 29033 * 2^-14 */ + +ALIGN(16) static const int16_t jsimd_ycc_rgb_convert_neon_consts[] = { + -F_0_344, F_0_714, F_1_402, F_1_772 +}; + + +/* Include inline routines for colorspace extensions. */ + +#include "jdcolext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE + +#define RGB_RED EXT_RGB_RED +#define RGB_GREEN EXT_RGB_GREEN +#define RGB_BLUE EXT_RGB_BLUE +#define RGB_PIXELSIZE EXT_RGB_PIXELSIZE +#define jsimd_ycc_rgb_convert_neon jsimd_ycc_extrgb_convert_neon +#include "jdcolext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_ycc_rgb_convert_neon + +#define RGB_RED EXT_RGBX_RED +#define RGB_GREEN EXT_RGBX_GREEN +#define RGB_BLUE EXT_RGBX_BLUE +#define RGB_ALPHA 3 +#define RGB_PIXELSIZE EXT_RGBX_PIXELSIZE +#define jsimd_ycc_rgb_convert_neon jsimd_ycc_extrgbx_convert_neon +#include "jdcolext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_ycc_rgb_convert_neon + +#define RGB_RED EXT_BGR_RED +#define RGB_GREEN EXT_BGR_GREEN +#define RGB_BLUE EXT_BGR_BLUE +#define RGB_PIXELSIZE EXT_BGR_PIXELSIZE +#define jsimd_ycc_rgb_convert_neon jsimd_ycc_extbgr_convert_neon +#include "jdcolext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_ycc_rgb_convert_neon + +#define RGB_RED EXT_BGRX_RED +#define RGB_GREEN EXT_BGRX_GREEN +#define RGB_BLUE EXT_BGRX_BLUE +#define RGB_ALPHA 3 +#define RGB_PIXELSIZE EXT_BGRX_PIXELSIZE +#define jsimd_ycc_rgb_convert_neon jsimd_ycc_extbgrx_convert_neon +#include "jdcolext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_ycc_rgb_convert_neon + +#define RGB_RED EXT_XBGR_RED +#define RGB_GREEN EXT_XBGR_GREEN +#define RGB_BLUE EXT_XBGR_BLUE +#define RGB_ALPHA 0 +#define RGB_PIXELSIZE EXT_XBGR_PIXELSIZE +#define jsimd_ycc_rgb_convert_neon jsimd_ycc_extxbgr_convert_neon +#include "jdcolext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_ycc_rgb_convert_neon + +#define RGB_RED EXT_XRGB_RED +#define RGB_GREEN EXT_XRGB_GREEN +#define RGB_BLUE EXT_XRGB_BLUE +#define RGB_ALPHA 0 +#define RGB_PIXELSIZE EXT_XRGB_PIXELSIZE +#define jsimd_ycc_rgb_convert_neon jsimd_ycc_extxrgb_convert_neon +#include "jdcolext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_ycc_rgb_convert_neon + +/* YCbCr -> RGB565 Conversion */ + +#define RGB_PIXELSIZE 2 +#define jsimd_ycc_rgb_convert_neon jsimd_ycc_rgb565_convert_neon +#include "jdcolext-neon.c" +#undef RGB_PIXELSIZE +#undef jsimd_ycc_rgb_convert_neon diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jdmerge-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jdmerge-neon.c new file mode 100644 index 00000000000..18fb9d8a55a --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jdmerge-neon.c @@ -0,0 +1,144 @@ +/* + * jdmerge-neon.c - merged upsampling/color conversion (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" + +#include + + +/* YCbCr -> RGB conversion constants */ + +#define F_0_344 11277 /* 0.3441467 = 11277 * 2^-15 */ +#define F_0_714 23401 /* 0.7141418 = 23401 * 2^-15 */ +#define F_1_402 22971 /* 1.4020386 = 22971 * 2^-14 */ +#define F_1_772 29033 /* 1.7720337 = 29033 * 2^-14 */ + +ALIGN(16) static const int16_t jsimd_ycc_rgb_convert_neon_consts[] = { + -F_0_344, F_0_714, F_1_402, F_1_772 +}; + + +/* Include inline routines for colorspace extensions. */ + +#include "jdmrgext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE + +#define RGB_RED EXT_RGB_RED +#define RGB_GREEN EXT_RGB_GREEN +#define RGB_BLUE EXT_RGB_BLUE +#define RGB_PIXELSIZE EXT_RGB_PIXELSIZE +#define jsimd_h2v1_merged_upsample_neon jsimd_h2v1_extrgb_merged_upsample_neon +#define jsimd_h2v2_merged_upsample_neon jsimd_h2v2_extrgb_merged_upsample_neon +#include "jdmrgext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_neon +#undef jsimd_h2v2_merged_upsample_neon + +#define RGB_RED EXT_RGBX_RED +#define RGB_GREEN EXT_RGBX_GREEN +#define RGB_BLUE EXT_RGBX_BLUE +#define RGB_ALPHA 3 +#define RGB_PIXELSIZE EXT_RGBX_PIXELSIZE +#define jsimd_h2v1_merged_upsample_neon jsimd_h2v1_extrgbx_merged_upsample_neon +#define jsimd_h2v2_merged_upsample_neon jsimd_h2v2_extrgbx_merged_upsample_neon +#include "jdmrgext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_neon +#undef jsimd_h2v2_merged_upsample_neon + +#define RGB_RED EXT_BGR_RED +#define RGB_GREEN EXT_BGR_GREEN +#define RGB_BLUE EXT_BGR_BLUE +#define RGB_PIXELSIZE EXT_BGR_PIXELSIZE +#define jsimd_h2v1_merged_upsample_neon jsimd_h2v1_extbgr_merged_upsample_neon +#define jsimd_h2v2_merged_upsample_neon jsimd_h2v2_extbgr_merged_upsample_neon +#include "jdmrgext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_neon +#undef jsimd_h2v2_merged_upsample_neon + +#define RGB_RED EXT_BGRX_RED +#define RGB_GREEN EXT_BGRX_GREEN +#define RGB_BLUE EXT_BGRX_BLUE +#define RGB_ALPHA 3 +#define RGB_PIXELSIZE EXT_BGRX_PIXELSIZE +#define jsimd_h2v1_merged_upsample_neon jsimd_h2v1_extbgrx_merged_upsample_neon +#define jsimd_h2v2_merged_upsample_neon jsimd_h2v2_extbgrx_merged_upsample_neon +#include "jdmrgext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_neon +#undef jsimd_h2v2_merged_upsample_neon + +#define RGB_RED EXT_XBGR_RED +#define RGB_GREEN EXT_XBGR_GREEN +#define RGB_BLUE EXT_XBGR_BLUE +#define RGB_ALPHA 0 +#define RGB_PIXELSIZE EXT_XBGR_PIXELSIZE +#define jsimd_h2v1_merged_upsample_neon jsimd_h2v1_extxbgr_merged_upsample_neon +#define jsimd_h2v2_merged_upsample_neon jsimd_h2v2_extxbgr_merged_upsample_neon +#include "jdmrgext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_neon +#undef jsimd_h2v2_merged_upsample_neon + +#define RGB_RED EXT_XRGB_RED +#define RGB_GREEN EXT_XRGB_GREEN +#define RGB_BLUE EXT_XRGB_BLUE +#define RGB_ALPHA 0 +#define RGB_PIXELSIZE EXT_XRGB_PIXELSIZE +#define jsimd_h2v1_merged_upsample_neon jsimd_h2v1_extxrgb_merged_upsample_neon +#define jsimd_h2v2_merged_upsample_neon jsimd_h2v2_extxrgb_merged_upsample_neon +#include "jdmrgext-neon.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_ALPHA +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_neon diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jdmrgext-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jdmrgext-neon.c new file mode 100644 index 00000000000..5b89bdb3394 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jdmrgext-neon.c @@ -0,0 +1,723 @@ +/* + * jdmrgext-neon.c - merged upsampling/color conversion (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jdmerge-neon.c. */ + + +/* These routines combine simple (non-fancy, i.e. non-smooth) h2v1 or h2v2 + * chroma upsampling and YCbCr -> RGB color conversion into a single function. + * + * As with the standalone functions, YCbCr -> RGB conversion is defined by the + * following equations: + * R = Y + 1.40200 * (Cr - 128) + * G = Y - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) + * B = Y + 1.77200 * (Cb - 128) + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.3441467 = 11277 * 2^-15 + * 0.7141418 = 23401 * 2^-15 + * 1.4020386 = 22971 * 2^-14 + * 1.7720337 = 29033 * 2^-14 + * These constants are defined in jdmerge-neon.c. + * + * To ensure correct results, rounding is used when descaling. + */ + +/* Notes on safe memory access for merged upsampling/YCbCr -> RGB conversion + * routines: + * + * Input memory buffers can be safely overread up to the next multiple of + * ALIGN_SIZE bytes, since they are always allocated by alloc_sarray() in + * jmemmgr.c. + * + * The output buffer cannot safely be written beyond output_width, since + * output_buf points to a possibly unpadded row in the decompressed image + * buffer allocated by the calling program. + */ + +/* Upsample and color convert for the case of 2:1 horizontal and 1:1 vertical. + */ + +void jsimd_h2v1_merged_upsample_neon(JDIMENSION output_width, + JSAMPIMAGE input_buf, + JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf) +{ + JSAMPROW outptr; + /* Pointers to Y, Cb, and Cr data */ + JSAMPROW inptr0, inptr1, inptr2; + + const int16x4_t consts = vld1_s16(jsimd_ycc_rgb_convert_neon_consts); + const int16x8_t neg_128 = vdupq_n_s16(-128); + + inptr0 = input_buf[0][in_row_group_ctr]; + inptr1 = input_buf[1][in_row_group_ctr]; + inptr2 = input_buf[2][in_row_group_ctr]; + outptr = output_buf[0]; + + int cols_remaining = output_width; + for (; cols_remaining >= 16; cols_remaining -= 16) { + /* De-interleave Y component values into two separate vectors, one + * containing the component values with even-numbered indices and one + * containing the component values with odd-numbered indices. + */ + uint8x8x2_t y = vld2_u8(inptr0); + uint8x8_t cb = vld1_u8(inptr1); + uint8x8_t cr = vld1_u8(inptr2); + /* Subtract 128 from Cb and Cr. */ + int16x8_t cr_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cr)); + int16x8_t cb_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cb)); + /* Compute G-Y: - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) */ + int32x4_t g_sub_y_l = vmull_lane_s16(vget_low_s16(cb_128), consts, 0); + int32x4_t g_sub_y_h = vmull_lane_s16(vget_high_s16(cb_128), consts, 0); + g_sub_y_l = vmlsl_lane_s16(g_sub_y_l, vget_low_s16(cr_128), consts, 1); + g_sub_y_h = vmlsl_lane_s16(g_sub_y_h, vget_high_s16(cr_128), consts, 1); + /* Descale G components: shift right 15, round, and narrow to 16-bit. */ + int16x8_t g_sub_y = vcombine_s16(vrshrn_n_s32(g_sub_y_l, 15), + vrshrn_n_s32(g_sub_y_h, 15)); + /* Compute R-Y: 1.40200 * (Cr - 128) */ + int16x8_t r_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128, 1), consts, 2); + /* Compute B-Y: 1.77200 * (Cb - 128) */ + int16x8_t b_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128, 1), consts, 3); + /* Add the chroma-derived values (G-Y, R-Y, and B-Y) to both the "even" and + * "odd" Y component values. This effectively upsamples the chroma + * components horizontally. + */ + int16x8_t g_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y.val[0])); + int16x8_t r_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y.val[0])); + int16x8_t b_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y.val[0])); + int16x8_t g_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y.val[1])); + int16x8_t r_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y.val[1])); + int16x8_t b_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y.val[1])); + /* Convert each component to unsigned and narrow, clamping to [0-255]. + * Re-interleave the "even" and "odd" component values. + */ + uint8x8x2_t r = vzip_u8(vqmovun_s16(r_even), vqmovun_s16(r_odd)); + uint8x8x2_t g = vzip_u8(vqmovun_s16(g_even), vqmovun_s16(g_odd)); + uint8x8x2_t b = vzip_u8(vqmovun_s16(b_even), vqmovun_s16(b_odd)); + +#ifdef RGB_ALPHA + uint8x16x4_t rgba; + rgba.val[RGB_RED] = vcombine_u8(r.val[0], r.val[1]); + rgba.val[RGB_GREEN] = vcombine_u8(g.val[0], g.val[1]); + rgba.val[RGB_BLUE] = vcombine_u8(b.val[0], b.val[1]); + /* Set alpha channel to opaque (0xFF). */ + rgba.val[RGB_ALPHA] = vdupq_n_u8(0xFF); + /* Store RGBA pixel data to memory. */ + vst4q_u8(outptr, rgba); +#else + uint8x16x3_t rgb; + rgb.val[RGB_RED] = vcombine_u8(r.val[0], r.val[1]); + rgb.val[RGB_GREEN] = vcombine_u8(g.val[0], g.val[1]); + rgb.val[RGB_BLUE] = vcombine_u8(b.val[0], b.val[1]); + /* Store RGB pixel data to memory. */ + vst3q_u8(outptr, rgb); +#endif + + /* Increment pointers. */ + inptr0 += 16; + inptr1 += 8; + inptr2 += 8; + outptr += (RGB_PIXELSIZE * 16); + } + + if (cols_remaining > 0) { + /* De-interleave Y component values into two separate vectors, one + * containing the component values with even-numbered indices and one + * containing the component values with odd-numbered indices. + */ + uint8x8x2_t y = vld2_u8(inptr0); + uint8x8_t cb = vld1_u8(inptr1); + uint8x8_t cr = vld1_u8(inptr2); + /* Subtract 128 from Cb and Cr. */ + int16x8_t cr_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cr)); + int16x8_t cb_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cb)); + /* Compute G-Y: - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) */ + int32x4_t g_sub_y_l = vmull_lane_s16(vget_low_s16(cb_128), consts, 0); + int32x4_t g_sub_y_h = vmull_lane_s16(vget_high_s16(cb_128), consts, 0); + g_sub_y_l = vmlsl_lane_s16(g_sub_y_l, vget_low_s16(cr_128), consts, 1); + g_sub_y_h = vmlsl_lane_s16(g_sub_y_h, vget_high_s16(cr_128), consts, 1); + /* Descale G components: shift right 15, round, and narrow to 16-bit. */ + int16x8_t g_sub_y = vcombine_s16(vrshrn_n_s32(g_sub_y_l, 15), + vrshrn_n_s32(g_sub_y_h, 15)); + /* Compute R-Y: 1.40200 * (Cr - 128) */ + int16x8_t r_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128, 1), consts, 2); + /* Compute B-Y: 1.77200 * (Cb - 128) */ + int16x8_t b_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128, 1), consts, 3); + /* Add the chroma-derived values (G-Y, R-Y, and B-Y) to both the "even" and + * "odd" Y component values. This effectively upsamples the chroma + * components horizontally. + */ + int16x8_t g_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y.val[0])); + int16x8_t r_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y.val[0])); + int16x8_t b_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y.val[0])); + int16x8_t g_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y.val[1])); + int16x8_t r_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y.val[1])); + int16x8_t b_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y.val[1])); + /* Convert each component to unsigned and narrow, clamping to [0-255]. + * Re-interleave the "even" and "odd" component values. + */ + uint8x8x2_t r = vzip_u8(vqmovun_s16(r_even), vqmovun_s16(r_odd)); + uint8x8x2_t g = vzip_u8(vqmovun_s16(g_even), vqmovun_s16(g_odd)); + uint8x8x2_t b = vzip_u8(vqmovun_s16(b_even), vqmovun_s16(b_odd)); + +#ifdef RGB_ALPHA + uint8x8x4_t rgba_h; + rgba_h.val[RGB_RED] = r.val[1]; + rgba_h.val[RGB_GREEN] = g.val[1]; + rgba_h.val[RGB_BLUE] = b.val[1]; + /* Set alpha channel to opaque (0xFF). */ + rgba_h.val[RGB_ALPHA] = vdup_n_u8(0xFF); + uint8x8x4_t rgba_l; + rgba_l.val[RGB_RED] = r.val[0]; + rgba_l.val[RGB_GREEN] = g.val[0]; + rgba_l.val[RGB_BLUE] = b.val[0]; + /* Set alpha channel to opaque (0xFF). */ + rgba_l.val[RGB_ALPHA] = vdup_n_u8(0xFF); + /* Store RGBA pixel data to memory. */ + switch (cols_remaining) { + case 15: + vst4_lane_u8(outptr + 14 * RGB_PIXELSIZE, rgba_h, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 14: + vst4_lane_u8(outptr + 13 * RGB_PIXELSIZE, rgba_h, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 13: + vst4_lane_u8(outptr + 12 * RGB_PIXELSIZE, rgba_h, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 12: + vst4_lane_u8(outptr + 11 * RGB_PIXELSIZE, rgba_h, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 11: + vst4_lane_u8(outptr + 10 * RGB_PIXELSIZE, rgba_h, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 10: + vst4_lane_u8(outptr + 9 * RGB_PIXELSIZE, rgba_h, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 9: + vst4_lane_u8(outptr + 8 * RGB_PIXELSIZE, rgba_h, 0); + FALLTHROUGH /*FALLTHROUGH*/ + case 8: + vst4_u8(outptr, rgba_l); + break; + case 7: + vst4_lane_u8(outptr + 6 * RGB_PIXELSIZE, rgba_l, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + vst4_lane_u8(outptr + 5 * RGB_PIXELSIZE, rgba_l, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + vst4_lane_u8(outptr + 4 * RGB_PIXELSIZE, rgba_l, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + vst4_lane_u8(outptr + 3 * RGB_PIXELSIZE, rgba_l, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + vst4_lane_u8(outptr + 2 * RGB_PIXELSIZE, rgba_l, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + vst4_lane_u8(outptr + RGB_PIXELSIZE, rgba_l, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + vst4_lane_u8(outptr, rgba_l, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } +#else + uint8x8x3_t rgb_h; + rgb_h.val[RGB_RED] = r.val[1]; + rgb_h.val[RGB_GREEN] = g.val[1]; + rgb_h.val[RGB_BLUE] = b.val[1]; + uint8x8x3_t rgb_l; + rgb_l.val[RGB_RED] = r.val[0]; + rgb_l.val[RGB_GREEN] = g.val[0]; + rgb_l.val[RGB_BLUE] = b.val[0]; + /* Store RGB pixel data to memory. */ + switch (cols_remaining) { + case 15: + vst3_lane_u8(outptr + 14 * RGB_PIXELSIZE, rgb_h, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 14: + vst3_lane_u8(outptr + 13 * RGB_PIXELSIZE, rgb_h, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 13: + vst3_lane_u8(outptr + 12 * RGB_PIXELSIZE, rgb_h, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 12: + vst3_lane_u8(outptr + 11 * RGB_PIXELSIZE, rgb_h, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 11: + vst3_lane_u8(outptr + 10 * RGB_PIXELSIZE, rgb_h, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 10: + vst3_lane_u8(outptr + 9 * RGB_PIXELSIZE, rgb_h, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 9: + vst3_lane_u8(outptr + 8 * RGB_PIXELSIZE, rgb_h, 0); + FALLTHROUGH /*FALLTHROUGH*/ + case 8: + vst3_u8(outptr, rgb_l); + break; + case 7: + vst3_lane_u8(outptr + 6 * RGB_PIXELSIZE, rgb_l, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + vst3_lane_u8(outptr + 5 * RGB_PIXELSIZE, rgb_l, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + vst3_lane_u8(outptr + 4 * RGB_PIXELSIZE, rgb_l, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + vst3_lane_u8(outptr + 3 * RGB_PIXELSIZE, rgb_l, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + vst3_lane_u8(outptr + 2 * RGB_PIXELSIZE, rgb_l, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + vst3_lane_u8(outptr + RGB_PIXELSIZE, rgb_l, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + vst3_lane_u8(outptr, rgb_l, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } +#endif + } +} + + +/* Upsample and color convert for the case of 2:1 horizontal and 2:1 vertical. + * + * See comments above for details regarding color conversion and safe memory + * access. + */ + +void jsimd_h2v2_merged_upsample_neon(JDIMENSION output_width, + JSAMPIMAGE input_buf, + JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf) +{ + JSAMPROW outptr0, outptr1; + /* Pointers to Y (both rows), Cb, and Cr data */ + JSAMPROW inptr0_0, inptr0_1, inptr1, inptr2; + + const int16x4_t consts = vld1_s16(jsimd_ycc_rgb_convert_neon_consts); + const int16x8_t neg_128 = vdupq_n_s16(-128); + + inptr0_0 = input_buf[0][in_row_group_ctr * 2]; + inptr0_1 = input_buf[0][in_row_group_ctr * 2 + 1]; + inptr1 = input_buf[1][in_row_group_ctr]; + inptr2 = input_buf[2][in_row_group_ctr]; + outptr0 = output_buf[0]; + outptr1 = output_buf[1]; + + int cols_remaining = output_width; + for (; cols_remaining >= 16; cols_remaining -= 16) { + /* For each row, de-interleave Y component values into two separate + * vectors, one containing the component values with even-numbered indices + * and one containing the component values with odd-numbered indices. + */ + uint8x8x2_t y0 = vld2_u8(inptr0_0); + uint8x8x2_t y1 = vld2_u8(inptr0_1); + uint8x8_t cb = vld1_u8(inptr1); + uint8x8_t cr = vld1_u8(inptr2); + /* Subtract 128 from Cb and Cr. */ + int16x8_t cr_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cr)); + int16x8_t cb_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cb)); + /* Compute G-Y: - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) */ + int32x4_t g_sub_y_l = vmull_lane_s16(vget_low_s16(cb_128), consts, 0); + int32x4_t g_sub_y_h = vmull_lane_s16(vget_high_s16(cb_128), consts, 0); + g_sub_y_l = vmlsl_lane_s16(g_sub_y_l, vget_low_s16(cr_128), consts, 1); + g_sub_y_h = vmlsl_lane_s16(g_sub_y_h, vget_high_s16(cr_128), consts, 1); + /* Descale G components: shift right 15, round, and narrow to 16-bit. */ + int16x8_t g_sub_y = vcombine_s16(vrshrn_n_s32(g_sub_y_l, 15), + vrshrn_n_s32(g_sub_y_h, 15)); + /* Compute R-Y: 1.40200 * (Cr - 128) */ + int16x8_t r_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128, 1), consts, 2); + /* Compute B-Y: 1.77200 * (Cb - 128) */ + int16x8_t b_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128, 1), consts, 3); + /* For each row, add the chroma-derived values (G-Y, R-Y, and B-Y) to both + * the "even" and "odd" Y component values. This effectively upsamples the + * chroma components both horizontally and vertically. + */ + int16x8_t g0_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y0.val[0])); + int16x8_t r0_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y0.val[0])); + int16x8_t b0_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y0.val[0])); + int16x8_t g0_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y0.val[1])); + int16x8_t r0_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y0.val[1])); + int16x8_t b0_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y0.val[1])); + int16x8_t g1_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y1.val[0])); + int16x8_t r1_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y1.val[0])); + int16x8_t b1_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y1.val[0])); + int16x8_t g1_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y1.val[1])); + int16x8_t r1_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y1.val[1])); + int16x8_t b1_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y1.val[1])); + /* Convert each component to unsigned and narrow, clamping to [0-255]. + * Re-interleave the "even" and "odd" component values. + */ + uint8x8x2_t r0 = vzip_u8(vqmovun_s16(r0_even), vqmovun_s16(r0_odd)); + uint8x8x2_t r1 = vzip_u8(vqmovun_s16(r1_even), vqmovun_s16(r1_odd)); + uint8x8x2_t g0 = vzip_u8(vqmovun_s16(g0_even), vqmovun_s16(g0_odd)); + uint8x8x2_t g1 = vzip_u8(vqmovun_s16(g1_even), vqmovun_s16(g1_odd)); + uint8x8x2_t b0 = vzip_u8(vqmovun_s16(b0_even), vqmovun_s16(b0_odd)); + uint8x8x2_t b1 = vzip_u8(vqmovun_s16(b1_even), vqmovun_s16(b1_odd)); + +#ifdef RGB_ALPHA + uint8x16x4_t rgba0, rgba1; + rgba0.val[RGB_RED] = vcombine_u8(r0.val[0], r0.val[1]); + rgba1.val[RGB_RED] = vcombine_u8(r1.val[0], r1.val[1]); + rgba0.val[RGB_GREEN] = vcombine_u8(g0.val[0], g0.val[1]); + rgba1.val[RGB_GREEN] = vcombine_u8(g1.val[0], g1.val[1]); + rgba0.val[RGB_BLUE] = vcombine_u8(b0.val[0], b0.val[1]); + rgba1.val[RGB_BLUE] = vcombine_u8(b1.val[0], b1.val[1]); + /* Set alpha channel to opaque (0xFF). */ + rgba0.val[RGB_ALPHA] = vdupq_n_u8(0xFF); + rgba1.val[RGB_ALPHA] = vdupq_n_u8(0xFF); + /* Store RGBA pixel data to memory. */ + vst4q_u8(outptr0, rgba0); + vst4q_u8(outptr1, rgba1); +#else + uint8x16x3_t rgb0, rgb1; + rgb0.val[RGB_RED] = vcombine_u8(r0.val[0], r0.val[1]); + rgb1.val[RGB_RED] = vcombine_u8(r1.val[0], r1.val[1]); + rgb0.val[RGB_GREEN] = vcombine_u8(g0.val[0], g0.val[1]); + rgb1.val[RGB_GREEN] = vcombine_u8(g1.val[0], g1.val[1]); + rgb0.val[RGB_BLUE] = vcombine_u8(b0.val[0], b0.val[1]); + rgb1.val[RGB_BLUE] = vcombine_u8(b1.val[0], b1.val[1]); + /* Store RGB pixel data to memory. */ + vst3q_u8(outptr0, rgb0); + vst3q_u8(outptr1, rgb1); +#endif + + /* Increment pointers. */ + inptr0_0 += 16; + inptr0_1 += 16; + inptr1 += 8; + inptr2 += 8; + outptr0 += (RGB_PIXELSIZE * 16); + outptr1 += (RGB_PIXELSIZE * 16); + } + + if (cols_remaining > 0) { + /* For each row, de-interleave Y component values into two separate + * vectors, one containing the component values with even-numbered indices + * and one containing the component values with odd-numbered indices. + */ + uint8x8x2_t y0 = vld2_u8(inptr0_0); + uint8x8x2_t y1 = vld2_u8(inptr0_1); + uint8x8_t cb = vld1_u8(inptr1); + uint8x8_t cr = vld1_u8(inptr2); + /* Subtract 128 from Cb and Cr. */ + int16x8_t cr_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cr)); + int16x8_t cb_128 = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(neg_128), cb)); + /* Compute G-Y: - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128) */ + int32x4_t g_sub_y_l = vmull_lane_s16(vget_low_s16(cb_128), consts, 0); + int32x4_t g_sub_y_h = vmull_lane_s16(vget_high_s16(cb_128), consts, 0); + g_sub_y_l = vmlsl_lane_s16(g_sub_y_l, vget_low_s16(cr_128), consts, 1); + g_sub_y_h = vmlsl_lane_s16(g_sub_y_h, vget_high_s16(cr_128), consts, 1); + /* Descale G components: shift right 15, round, and narrow to 16-bit. */ + int16x8_t g_sub_y = vcombine_s16(vrshrn_n_s32(g_sub_y_l, 15), + vrshrn_n_s32(g_sub_y_h, 15)); + /* Compute R-Y: 1.40200 * (Cr - 128) */ + int16x8_t r_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cr_128, 1), consts, 2); + /* Compute B-Y: 1.77200 * (Cb - 128) */ + int16x8_t b_sub_y = vqrdmulhq_lane_s16(vshlq_n_s16(cb_128, 1), consts, 3); + /* For each row, add the chroma-derived values (G-Y, R-Y, and B-Y) to both + * the "even" and "odd" Y component values. This effectively upsamples the + * chroma components both horizontally and vertically. + */ + int16x8_t g0_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y0.val[0])); + int16x8_t r0_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y0.val[0])); + int16x8_t b0_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y0.val[0])); + int16x8_t g0_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y0.val[1])); + int16x8_t r0_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y0.val[1])); + int16x8_t b0_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y0.val[1])); + int16x8_t g1_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y1.val[0])); + int16x8_t r1_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y1.val[0])); + int16x8_t b1_even = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y1.val[0])); + int16x8_t g1_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(g_sub_y), + y1.val[1])); + int16x8_t r1_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(r_sub_y), + y1.val[1])); + int16x8_t b1_odd = + vreinterpretq_s16_u16(vaddw_u8(vreinterpretq_u16_s16(b_sub_y), + y1.val[1])); + /* Convert each component to unsigned and narrow, clamping to [0-255]. + * Re-interleave the "even" and "odd" component values. + */ + uint8x8x2_t r0 = vzip_u8(vqmovun_s16(r0_even), vqmovun_s16(r0_odd)); + uint8x8x2_t r1 = vzip_u8(vqmovun_s16(r1_even), vqmovun_s16(r1_odd)); + uint8x8x2_t g0 = vzip_u8(vqmovun_s16(g0_even), vqmovun_s16(g0_odd)); + uint8x8x2_t g1 = vzip_u8(vqmovun_s16(g1_even), vqmovun_s16(g1_odd)); + uint8x8x2_t b0 = vzip_u8(vqmovun_s16(b0_even), vqmovun_s16(b0_odd)); + uint8x8x2_t b1 = vzip_u8(vqmovun_s16(b1_even), vqmovun_s16(b1_odd)); + +#ifdef RGB_ALPHA + uint8x8x4_t rgba0_h, rgba1_h; + rgba0_h.val[RGB_RED] = r0.val[1]; + rgba1_h.val[RGB_RED] = r1.val[1]; + rgba0_h.val[RGB_GREEN] = g0.val[1]; + rgba1_h.val[RGB_GREEN] = g1.val[1]; + rgba0_h.val[RGB_BLUE] = b0.val[1]; + rgba1_h.val[RGB_BLUE] = b1.val[1]; + /* Set alpha channel to opaque (0xFF). */ + rgba0_h.val[RGB_ALPHA] = vdup_n_u8(0xFF); + rgba1_h.val[RGB_ALPHA] = vdup_n_u8(0xFF); + + uint8x8x4_t rgba0_l, rgba1_l; + rgba0_l.val[RGB_RED] = r0.val[0]; + rgba1_l.val[RGB_RED] = r1.val[0]; + rgba0_l.val[RGB_GREEN] = g0.val[0]; + rgba1_l.val[RGB_GREEN] = g1.val[0]; + rgba0_l.val[RGB_BLUE] = b0.val[0]; + rgba1_l.val[RGB_BLUE] = b1.val[0]; + /* Set alpha channel to opaque (0xFF). */ + rgba0_l.val[RGB_ALPHA] = vdup_n_u8(0xFF); + rgba1_l.val[RGB_ALPHA] = vdup_n_u8(0xFF); + /* Store RGBA pixel data to memory. */ + switch (cols_remaining) { + case 15: + vst4_lane_u8(outptr0 + 14 * RGB_PIXELSIZE, rgba0_h, 6); + vst4_lane_u8(outptr1 + 14 * RGB_PIXELSIZE, rgba1_h, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 14: + vst4_lane_u8(outptr0 + 13 * RGB_PIXELSIZE, rgba0_h, 5); + vst4_lane_u8(outptr1 + 13 * RGB_PIXELSIZE, rgba1_h, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 13: + vst4_lane_u8(outptr0 + 12 * RGB_PIXELSIZE, rgba0_h, 4); + vst4_lane_u8(outptr1 + 12 * RGB_PIXELSIZE, rgba1_h, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 12: + vst4_lane_u8(outptr0 + 11 * RGB_PIXELSIZE, rgba0_h, 3); + vst4_lane_u8(outptr1 + 11 * RGB_PIXELSIZE, rgba1_h, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 11: + vst4_lane_u8(outptr0 + 10 * RGB_PIXELSIZE, rgba0_h, 2); + vst4_lane_u8(outptr1 + 10 * RGB_PIXELSIZE, rgba1_h, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 10: + vst4_lane_u8(outptr0 + 9 * RGB_PIXELSIZE, rgba0_h, 1); + vst4_lane_u8(outptr1 + 9 * RGB_PIXELSIZE, rgba1_h, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 9: + vst4_lane_u8(outptr0 + 8 * RGB_PIXELSIZE, rgba0_h, 0); + vst4_lane_u8(outptr1 + 8 * RGB_PIXELSIZE, rgba1_h, 0); + FALLTHROUGH /*FALLTHROUGH*/ + case 8: + vst4_u8(outptr0, rgba0_l); + vst4_u8(outptr1, rgba1_l); + break; + case 7: + vst4_lane_u8(outptr0 + 6 * RGB_PIXELSIZE, rgba0_l, 6); + vst4_lane_u8(outptr1 + 6 * RGB_PIXELSIZE, rgba1_l, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + vst4_lane_u8(outptr0 + 5 * RGB_PIXELSIZE, rgba0_l, 5); + vst4_lane_u8(outptr1 + 5 * RGB_PIXELSIZE, rgba1_l, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + vst4_lane_u8(outptr0 + 4 * RGB_PIXELSIZE, rgba0_l, 4); + vst4_lane_u8(outptr1 + 4 * RGB_PIXELSIZE, rgba1_l, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + vst4_lane_u8(outptr0 + 3 * RGB_PIXELSIZE, rgba0_l, 3); + vst4_lane_u8(outptr1 + 3 * RGB_PIXELSIZE, rgba1_l, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + vst4_lane_u8(outptr0 + 2 * RGB_PIXELSIZE, rgba0_l, 2); + vst4_lane_u8(outptr1 + 2 * RGB_PIXELSIZE, rgba1_l, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + vst4_lane_u8(outptr0 + 1 * RGB_PIXELSIZE, rgba0_l, 1); + vst4_lane_u8(outptr1 + 1 * RGB_PIXELSIZE, rgba1_l, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + vst4_lane_u8(outptr0, rgba0_l, 0); + vst4_lane_u8(outptr1, rgba1_l, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } +#else + uint8x8x3_t rgb0_h, rgb1_h; + rgb0_h.val[RGB_RED] = r0.val[1]; + rgb1_h.val[RGB_RED] = r1.val[1]; + rgb0_h.val[RGB_GREEN] = g0.val[1]; + rgb1_h.val[RGB_GREEN] = g1.val[1]; + rgb0_h.val[RGB_BLUE] = b0.val[1]; + rgb1_h.val[RGB_BLUE] = b1.val[1]; + + uint8x8x3_t rgb0_l, rgb1_l; + rgb0_l.val[RGB_RED] = r0.val[0]; + rgb1_l.val[RGB_RED] = r1.val[0]; + rgb0_l.val[RGB_GREEN] = g0.val[0]; + rgb1_l.val[RGB_GREEN] = g1.val[0]; + rgb0_l.val[RGB_BLUE] = b0.val[0]; + rgb1_l.val[RGB_BLUE] = b1.val[0]; + /* Store RGB pixel data to memory. */ + switch (cols_remaining) { + case 15: + vst3_lane_u8(outptr0 + 14 * RGB_PIXELSIZE, rgb0_h, 6); + vst3_lane_u8(outptr1 + 14 * RGB_PIXELSIZE, rgb1_h, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 14: + vst3_lane_u8(outptr0 + 13 * RGB_PIXELSIZE, rgb0_h, 5); + vst3_lane_u8(outptr1 + 13 * RGB_PIXELSIZE, rgb1_h, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 13: + vst3_lane_u8(outptr0 + 12 * RGB_PIXELSIZE, rgb0_h, 4); + vst3_lane_u8(outptr1 + 12 * RGB_PIXELSIZE, rgb1_h, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 12: + vst3_lane_u8(outptr0 + 11 * RGB_PIXELSIZE, rgb0_h, 3); + vst3_lane_u8(outptr1 + 11 * RGB_PIXELSIZE, rgb1_h, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 11: + vst3_lane_u8(outptr0 + 10 * RGB_PIXELSIZE, rgb0_h, 2); + vst3_lane_u8(outptr1 + 10 * RGB_PIXELSIZE, rgb1_h, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 10: + vst3_lane_u8(outptr0 + 9 * RGB_PIXELSIZE, rgb0_h, 1); + vst3_lane_u8(outptr1 + 9 * RGB_PIXELSIZE, rgb1_h, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 9: + vst3_lane_u8(outptr0 + 8 * RGB_PIXELSIZE, rgb0_h, 0); + vst3_lane_u8(outptr1 + 8 * RGB_PIXELSIZE, rgb1_h, 0); + FALLTHROUGH /*FALLTHROUGH*/ + case 8: + vst3_u8(outptr0, rgb0_l); + vst3_u8(outptr1, rgb1_l); + break; + case 7: + vst3_lane_u8(outptr0 + 6 * RGB_PIXELSIZE, rgb0_l, 6); + vst3_lane_u8(outptr1 + 6 * RGB_PIXELSIZE, rgb1_l, 6); + FALLTHROUGH /*FALLTHROUGH*/ + case 6: + vst3_lane_u8(outptr0 + 5 * RGB_PIXELSIZE, rgb0_l, 5); + vst3_lane_u8(outptr1 + 5 * RGB_PIXELSIZE, rgb1_l, 5); + FALLTHROUGH /*FALLTHROUGH*/ + case 5: + vst3_lane_u8(outptr0 + 4 * RGB_PIXELSIZE, rgb0_l, 4); + vst3_lane_u8(outptr1 + 4 * RGB_PIXELSIZE, rgb1_l, 4); + FALLTHROUGH /*FALLTHROUGH*/ + case 4: + vst3_lane_u8(outptr0 + 3 * RGB_PIXELSIZE, rgb0_l, 3); + vst3_lane_u8(outptr1 + 3 * RGB_PIXELSIZE, rgb1_l, 3); + FALLTHROUGH /*FALLTHROUGH*/ + case 3: + vst3_lane_u8(outptr0 + 2 * RGB_PIXELSIZE, rgb0_l, 2); + vst3_lane_u8(outptr1 + 2 * RGB_PIXELSIZE, rgb1_l, 2); + FALLTHROUGH /*FALLTHROUGH*/ + case 2: + vst3_lane_u8(outptr0 + 1 * RGB_PIXELSIZE, rgb0_l, 1); + vst3_lane_u8(outptr1 + 1 * RGB_PIXELSIZE, rgb1_l, 1); + FALLTHROUGH /*FALLTHROUGH*/ + case 1: + vst3_lane_u8(outptr0, rgb0_l, 0); + vst3_lane_u8(outptr1, rgb1_l, 0); + FALLTHROUGH /*FALLTHROUGH*/ + default: + break; + } +#endif + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jdsample-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jdsample-neon.c new file mode 100644 index 00000000000..90ec6782c47 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jdsample-neon.c @@ -0,0 +1,569 @@ +/* + * jdsample-neon.c - upsampling (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" + +#include + + +/* The diagram below shows a row of samples produced by h2v1 downsampling. + * + * s0 s1 s2 + * +---------+---------+---------+ + * | | | | + * | p0 p1 | p2 p3 | p4 p5 | + * | | | | + * +---------+---------+---------+ + * + * Samples s0-s2 were created by averaging the original pixel component values + * centered at positions p0-p5 above. To approximate those original pixel + * component values, we proportionally blend the adjacent samples in each row. + * + * An upsampled pixel component value is computed by blending the sample + * containing the pixel center with the nearest neighboring sample, in the + * ratio 3:1. For example: + * p1(upsampled) = 3/4 * s0 + 1/4 * s1 + * p2(upsampled) = 3/4 * s1 + 1/4 * s0 + * When computing the first and last pixel component values in the row, there + * is no adjacent sample to blend, so: + * p0(upsampled) = s0 + * p5(upsampled) = s2 + */ + +void jsimd_h2v1_fancy_upsample_neon(int max_v_samp_factor, + JDIMENSION downsampled_width, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr) +{ + JSAMPARRAY output_data = *output_data_ptr; + JSAMPROW inptr, outptr; + int inrow; + unsigned colctr; + /* Set up constants. */ + const uint16x8_t one_u16 = vdupq_n_u16(1); + const uint8x8_t three_u8 = vdup_n_u8(3); + + for (inrow = 0; inrow < max_v_samp_factor; inrow++) { + inptr = input_data[inrow]; + outptr = output_data[inrow]; + /* First pixel component value in this row of the original image */ + *outptr = (JSAMPLE)GETJSAMPLE(*inptr); + + /* 3/4 * containing sample + 1/4 * nearest neighboring sample + * For p1: containing sample = s0, nearest neighboring sample = s1 + * For p2: containing sample = s1, nearest neighboring sample = s0 + */ + uint8x16_t s0 = vld1q_u8(inptr); + uint8x16_t s1 = vld1q_u8(inptr + 1); + /* Multiplication makes vectors twice as wide. '_l' and '_h' suffixes + * denote low half and high half respectively. + */ + uint16x8_t s1_add_3s0_l = + vmlal_u8(vmovl_u8(vget_low_u8(s1)), vget_low_u8(s0), three_u8); + uint16x8_t s1_add_3s0_h = + vmlal_u8(vmovl_u8(vget_high_u8(s1)), vget_high_u8(s0), three_u8); + uint16x8_t s0_add_3s1_l = + vmlal_u8(vmovl_u8(vget_low_u8(s0)), vget_low_u8(s1), three_u8); + uint16x8_t s0_add_3s1_h = + vmlal_u8(vmovl_u8(vget_high_u8(s0)), vget_high_u8(s1), three_u8); + /* Add ordered dithering bias to odd pixel values. */ + s0_add_3s1_l = vaddq_u16(s0_add_3s1_l, one_u16); + s0_add_3s1_h = vaddq_u16(s0_add_3s1_h, one_u16); + + /* The offset is initially 1, because the first pixel component has already + * been stored. However, in subsequent iterations of the SIMD loop, this + * offset is (2 * colctr - 1) to stay within the bounds of the sample + * buffers without having to resort to a slow scalar tail case for the last + * (downsampled_width % 16) samples. See "Creation of 2-D sample arrays" + * in jmemmgr.c for more details. + */ + unsigned outptr_offset = 1; + uint8x16x2_t output_pixels; + + /* We use software pipelining to maximise performance. The code indented + * an extra two spaces begins the next iteration of the loop. + */ + for (colctr = 16; colctr < downsampled_width; colctr += 16) { + + s0 = vld1q_u8(inptr + colctr - 1); + s1 = vld1q_u8(inptr + colctr); + + /* Right-shift by 2 (divide by 4), narrow to 8-bit, and combine. */ + output_pixels.val[0] = vcombine_u8(vrshrn_n_u16(s1_add_3s0_l, 2), + vrshrn_n_u16(s1_add_3s0_h, 2)); + output_pixels.val[1] = vcombine_u8(vshrn_n_u16(s0_add_3s1_l, 2), + vshrn_n_u16(s0_add_3s1_h, 2)); + + /* Multiplication makes vectors twice as wide. '_l' and '_h' suffixes + * denote low half and high half respectively. + */ + s1_add_3s0_l = + vmlal_u8(vmovl_u8(vget_low_u8(s1)), vget_low_u8(s0), three_u8); + s1_add_3s0_h = + vmlal_u8(vmovl_u8(vget_high_u8(s1)), vget_high_u8(s0), three_u8); + s0_add_3s1_l = + vmlal_u8(vmovl_u8(vget_low_u8(s0)), vget_low_u8(s1), three_u8); + s0_add_3s1_h = + vmlal_u8(vmovl_u8(vget_high_u8(s0)), vget_high_u8(s1), three_u8); + /* Add ordered dithering bias to odd pixel values. */ + s0_add_3s1_l = vaddq_u16(s0_add_3s1_l, one_u16); + s0_add_3s1_h = vaddq_u16(s0_add_3s1_h, one_u16); + + /* Store pixel component values to memory. */ + vst2q_u8(outptr + outptr_offset, output_pixels); + outptr_offset = 2 * colctr - 1; + } + + /* Complete the last iteration of the loop. */ + + /* Right-shift by 2 (divide by 4), narrow to 8-bit, and combine. */ + output_pixels.val[0] = vcombine_u8(vrshrn_n_u16(s1_add_3s0_l, 2), + vrshrn_n_u16(s1_add_3s0_h, 2)); + output_pixels.val[1] = vcombine_u8(vshrn_n_u16(s0_add_3s1_l, 2), + vshrn_n_u16(s0_add_3s1_h, 2)); + /* Store pixel component values to memory. */ + vst2q_u8(outptr + outptr_offset, output_pixels); + + /* Last pixel component value in this row of the original image */ + outptr[2 * downsampled_width - 1] = + GETJSAMPLE(inptr[downsampled_width - 1]); + } +} + + +/* The diagram below shows an array of samples produced by h2v2 downsampling. + * + * s0 s1 s2 + * +---------+---------+---------+ + * | p0 p1 | p2 p3 | p4 p5 | + * sA | | | | + * | p6 p7 | p8 p9 | p10 p11| + * +---------+---------+---------+ + * | p12 p13| p14 p15| p16 p17| + * sB | | | | + * | p18 p19| p20 p21| p22 p23| + * +---------+---------+---------+ + * | p24 p25| p26 p27| p28 p29| + * sC | | | | + * | p30 p31| p32 p33| p34 p35| + * +---------+---------+---------+ + * + * Samples s0A-s2C were created by averaging the original pixel component + * values centered at positions p0-p35 above. To approximate one of those + * original pixel component values, we proportionally blend the sample + * containing the pixel center with the nearest neighboring samples in each + * row, column, and diagonal. + * + * An upsampled pixel component value is computed by first blending the sample + * containing the pixel center with the nearest neighboring samples in the + * same column, in the ratio 3:1, and then blending each column sum with the + * nearest neighboring column sum, in the ratio 3:1. For example: + * p14(upsampled) = 3/4 * (3/4 * s1B + 1/4 * s1A) + + * 1/4 * (3/4 * s0B + 1/4 * s0A) + * = 9/16 * s1B + 3/16 * s1A + 3/16 * s0B + 1/16 * s0A + * When computing the first and last pixel component values in the row, there + * is no horizontally adjacent sample to blend, so: + * p12(upsampled) = 3/4 * s0B + 1/4 * s0A + * p23(upsampled) = 3/4 * s2B + 1/4 * s2C + * When computing the first and last pixel component values in the column, + * there is no vertically adjacent sample to blend, so: + * p2(upsampled) = 3/4 * s1A + 1/4 * s0A + * p33(upsampled) = 3/4 * s1C + 1/4 * s2C + * When computing the corner pixel component values, there is no adjacent + * sample to blend, so: + * p0(upsampled) = s0A + * p35(upsampled) = s2C + */ + +void jsimd_h2v2_fancy_upsample_neon(int max_v_samp_factor, + JDIMENSION downsampled_width, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr) +{ + JSAMPARRAY output_data = *output_data_ptr; + JSAMPROW inptr0, inptr1, inptr2, outptr0, outptr1; + int inrow, outrow; + unsigned colctr; + /* Set up constants. */ + const uint16x8_t seven_u16 = vdupq_n_u16(7); + const uint8x8_t three_u8 = vdup_n_u8(3); + const uint16x8_t three_u16 = vdupq_n_u16(3); + + inrow = outrow = 0; + while (outrow < max_v_samp_factor) { + inptr0 = input_data[inrow - 1]; + inptr1 = input_data[inrow]; + inptr2 = input_data[inrow + 1]; + /* Suffixes 0 and 1 denote the upper and lower rows of output pixels, + * respectively. + */ + outptr0 = output_data[outrow++]; + outptr1 = output_data[outrow++]; + + /* First pixel component value in this row of the original image */ + int s0colsum0 = GETJSAMPLE(*inptr1) * 3 + GETJSAMPLE(*inptr0); + *outptr0 = (JSAMPLE)((s0colsum0 * 4 + 8) >> 4); + int s0colsum1 = GETJSAMPLE(*inptr1) * 3 + GETJSAMPLE(*inptr2); + *outptr1 = (JSAMPLE)((s0colsum1 * 4 + 8) >> 4); + + /* Step 1: Blend samples vertically in columns s0 and s1. + * Leave the divide by 4 until the end, when it can be done for both + * dimensions at once, right-shifting by 4. + */ + + /* Load and compute s0colsum0 and s0colsum1. */ + uint8x16_t s0A = vld1q_u8(inptr0); + uint8x16_t s0B = vld1q_u8(inptr1); + uint8x16_t s0C = vld1q_u8(inptr2); + /* Multiplication makes vectors twice as wide. '_l' and '_h' suffixes + * denote low half and high half respectively. + */ + uint16x8_t s0colsum0_l = vmlal_u8(vmovl_u8(vget_low_u8(s0A)), + vget_low_u8(s0B), three_u8); + uint16x8_t s0colsum0_h = vmlal_u8(vmovl_u8(vget_high_u8(s0A)), + vget_high_u8(s0B), three_u8); + uint16x8_t s0colsum1_l = vmlal_u8(vmovl_u8(vget_low_u8(s0C)), + vget_low_u8(s0B), three_u8); + uint16x8_t s0colsum1_h = vmlal_u8(vmovl_u8(vget_high_u8(s0C)), + vget_high_u8(s0B), three_u8); + /* Load and compute s1colsum0 and s1colsum1. */ + uint8x16_t s1A = vld1q_u8(inptr0 + 1); + uint8x16_t s1B = vld1q_u8(inptr1 + 1); + uint8x16_t s1C = vld1q_u8(inptr2 + 1); + uint16x8_t s1colsum0_l = vmlal_u8(vmovl_u8(vget_low_u8(s1A)), + vget_low_u8(s1B), three_u8); + uint16x8_t s1colsum0_h = vmlal_u8(vmovl_u8(vget_high_u8(s1A)), + vget_high_u8(s1B), three_u8); + uint16x8_t s1colsum1_l = vmlal_u8(vmovl_u8(vget_low_u8(s1C)), + vget_low_u8(s1B), three_u8); + uint16x8_t s1colsum1_h = vmlal_u8(vmovl_u8(vget_high_u8(s1C)), + vget_high_u8(s1B), three_u8); + + /* Step 2: Blend the already-blended columns. */ + + uint16x8_t output0_p1_l = vmlaq_u16(s1colsum0_l, s0colsum0_l, three_u16); + uint16x8_t output0_p1_h = vmlaq_u16(s1colsum0_h, s0colsum0_h, three_u16); + uint16x8_t output0_p2_l = vmlaq_u16(s0colsum0_l, s1colsum0_l, three_u16); + uint16x8_t output0_p2_h = vmlaq_u16(s0colsum0_h, s1colsum0_h, three_u16); + uint16x8_t output1_p1_l = vmlaq_u16(s1colsum1_l, s0colsum1_l, three_u16); + uint16x8_t output1_p1_h = vmlaq_u16(s1colsum1_h, s0colsum1_h, three_u16); + uint16x8_t output1_p2_l = vmlaq_u16(s0colsum1_l, s1colsum1_l, three_u16); + uint16x8_t output1_p2_h = vmlaq_u16(s0colsum1_h, s1colsum1_h, three_u16); + /* Add ordered dithering bias to odd pixel values. */ + output0_p1_l = vaddq_u16(output0_p1_l, seven_u16); + output0_p1_h = vaddq_u16(output0_p1_h, seven_u16); + output1_p1_l = vaddq_u16(output1_p1_l, seven_u16); + output1_p1_h = vaddq_u16(output1_p1_h, seven_u16); + /* Right-shift by 4 (divide by 16), narrow to 8-bit, and combine. */ + uint8x16x2_t output_pixels0 = { { + vcombine_u8(vshrn_n_u16(output0_p1_l, 4), vshrn_n_u16(output0_p1_h, 4)), + vcombine_u8(vrshrn_n_u16(output0_p2_l, 4), vrshrn_n_u16(output0_p2_h, 4)) + } }; + uint8x16x2_t output_pixels1 = { { + vcombine_u8(vshrn_n_u16(output1_p1_l, 4), vshrn_n_u16(output1_p1_h, 4)), + vcombine_u8(vrshrn_n_u16(output1_p2_l, 4), vrshrn_n_u16(output1_p2_h, 4)) + } }; + + /* Store pixel component values to memory. + * The minimum size of the output buffer for each row is 64 bytes => no + * need to worry about buffer overflow here. See "Creation of 2-D sample + * arrays" in jmemmgr.c for more details. + */ + vst2q_u8(outptr0 + 1, output_pixels0); + vst2q_u8(outptr1 + 1, output_pixels1); + + /* The first pixel of the image shifted our loads and stores by one byte. + * We have to re-align on a 32-byte boundary at some point before the end + * of the row (we do it now on the 32/33 pixel boundary) to stay within the + * bounds of the sample buffers without having to resort to a slow scalar + * tail case for the last (downsampled_width % 16) samples. See "Creation + * of 2-D sample arrays" in jmemmgr.c for more details. + */ + for (colctr = 16; colctr < downsampled_width; colctr += 16) { + /* Step 1: Blend samples vertically in columns s0 and s1. */ + + /* Load and compute s0colsum0 and s0colsum1. */ + s0A = vld1q_u8(inptr0 + colctr - 1); + s0B = vld1q_u8(inptr1 + colctr - 1); + s0C = vld1q_u8(inptr2 + colctr - 1); + s0colsum0_l = vmlal_u8(vmovl_u8(vget_low_u8(s0A)), vget_low_u8(s0B), + three_u8); + s0colsum0_h = vmlal_u8(vmovl_u8(vget_high_u8(s0A)), vget_high_u8(s0B), + three_u8); + s0colsum1_l = vmlal_u8(vmovl_u8(vget_low_u8(s0C)), vget_low_u8(s0B), + three_u8); + s0colsum1_h = vmlal_u8(vmovl_u8(vget_high_u8(s0C)), vget_high_u8(s0B), + three_u8); + /* Load and compute s1colsum0 and s1colsum1. */ + s1A = vld1q_u8(inptr0 + colctr); + s1B = vld1q_u8(inptr1 + colctr); + s1C = vld1q_u8(inptr2 + colctr); + s1colsum0_l = vmlal_u8(vmovl_u8(vget_low_u8(s1A)), vget_low_u8(s1B), + three_u8); + s1colsum0_h = vmlal_u8(vmovl_u8(vget_high_u8(s1A)), vget_high_u8(s1B), + three_u8); + s1colsum1_l = vmlal_u8(vmovl_u8(vget_low_u8(s1C)), vget_low_u8(s1B), + three_u8); + s1colsum1_h = vmlal_u8(vmovl_u8(vget_high_u8(s1C)), vget_high_u8(s1B), + three_u8); + + /* Step 2: Blend the already-blended columns. */ + + output0_p1_l = vmlaq_u16(s1colsum0_l, s0colsum0_l, three_u16); + output0_p1_h = vmlaq_u16(s1colsum0_h, s0colsum0_h, three_u16); + output0_p2_l = vmlaq_u16(s0colsum0_l, s1colsum0_l, three_u16); + output0_p2_h = vmlaq_u16(s0colsum0_h, s1colsum0_h, three_u16); + output1_p1_l = vmlaq_u16(s1colsum1_l, s0colsum1_l, three_u16); + output1_p1_h = vmlaq_u16(s1colsum1_h, s0colsum1_h, three_u16); + output1_p2_l = vmlaq_u16(s0colsum1_l, s1colsum1_l, three_u16); + output1_p2_h = vmlaq_u16(s0colsum1_h, s1colsum1_h, three_u16); + /* Add ordered dithering bias to odd pixel values. */ + output0_p1_l = vaddq_u16(output0_p1_l, seven_u16); + output0_p1_h = vaddq_u16(output0_p1_h, seven_u16); + output1_p1_l = vaddq_u16(output1_p1_l, seven_u16); + output1_p1_h = vaddq_u16(output1_p1_h, seven_u16); + /* Right-shift by 4 (divide by 16), narrow to 8-bit, and combine. */ + output_pixels0.val[0] = vcombine_u8(vshrn_n_u16(output0_p1_l, 4), + vshrn_n_u16(output0_p1_h, 4)); + output_pixels0.val[1] = vcombine_u8(vrshrn_n_u16(output0_p2_l, 4), + vrshrn_n_u16(output0_p2_h, 4)); + output_pixels1.val[0] = vcombine_u8(vshrn_n_u16(output1_p1_l, 4), + vshrn_n_u16(output1_p1_h, 4)); + output_pixels1.val[1] = vcombine_u8(vrshrn_n_u16(output1_p2_l, 4), + vrshrn_n_u16(output1_p2_h, 4)); + /* Store pixel component values to memory. */ + vst2q_u8(outptr0 + 2 * colctr - 1, output_pixels0); + vst2q_u8(outptr1 + 2 * colctr - 1, output_pixels1); + } + + /* Last pixel component value in this row of the original image */ + int s1colsum0 = GETJSAMPLE(inptr1[downsampled_width - 1]) * 3 + + GETJSAMPLE(inptr0[downsampled_width - 1]); + outptr0[2 * downsampled_width - 1] = (JSAMPLE)((s1colsum0 * 4 + 7) >> 4); + int s1colsum1 = GETJSAMPLE(inptr1[downsampled_width - 1]) * 3 + + GETJSAMPLE(inptr2[downsampled_width - 1]); + outptr1[2 * downsampled_width - 1] = (JSAMPLE)((s1colsum1 * 4 + 7) >> 4); + inrow++; + } +} + + +/* The diagram below shows a column of samples produced by h1v2 downsampling + * (or by losslessly rotating or transposing an h2v1-downsampled image.) + * + * +---------+ + * | p0 | + * sA | | + * | p1 | + * +---------+ + * | p2 | + * sB | | + * | p3 | + * +---------+ + * | p4 | + * sC | | + * | p5 | + * +---------+ + * + * Samples sA-sC were created by averaging the original pixel component values + * centered at positions p0-p5 above. To approximate those original pixel + * component values, we proportionally blend the adjacent samples in each + * column. + * + * An upsampled pixel component value is computed by blending the sample + * containing the pixel center with the nearest neighboring sample, in the + * ratio 3:1. For example: + * p1(upsampled) = 3/4 * sA + 1/4 * sB + * p2(upsampled) = 3/4 * sB + 1/4 * sA + * When computing the first and last pixel component values in the column, + * there is no adjacent sample to blend, so: + * p0(upsampled) = sA + * p5(upsampled) = sC + */ + +void jsimd_h1v2_fancy_upsample_neon(int max_v_samp_factor, + JDIMENSION downsampled_width, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr) +{ + JSAMPARRAY output_data = *output_data_ptr; + JSAMPROW inptr0, inptr1, inptr2, outptr0, outptr1; + int inrow, outrow; + unsigned colctr; + /* Set up constants. */ + const uint16x8_t one_u16 = vdupq_n_u16(1); + const uint8x8_t three_u8 = vdup_n_u8(3); + + inrow = outrow = 0; + while (outrow < max_v_samp_factor) { + inptr0 = input_data[inrow - 1]; + inptr1 = input_data[inrow]; + inptr2 = input_data[inrow + 1]; + /* Suffixes 0 and 1 denote the upper and lower rows of output pixels, + * respectively. + */ + outptr0 = output_data[outrow++]; + outptr1 = output_data[outrow++]; + inrow++; + + /* The size of the input and output buffers is always a multiple of 32 + * bytes => no need to worry about buffer overflow when reading/writing + * memory. See "Creation of 2-D sample arrays" in jmemmgr.c for more + * details. + */ + for (colctr = 0; colctr < downsampled_width; colctr += 16) { + /* Load samples. */ + uint8x16_t sA = vld1q_u8(inptr0 + colctr); + uint8x16_t sB = vld1q_u8(inptr1 + colctr); + uint8x16_t sC = vld1q_u8(inptr2 + colctr); + /* Blend samples vertically. */ + uint16x8_t colsum0_l = vmlal_u8(vmovl_u8(vget_low_u8(sA)), + vget_low_u8(sB), three_u8); + uint16x8_t colsum0_h = vmlal_u8(vmovl_u8(vget_high_u8(sA)), + vget_high_u8(sB), three_u8); + uint16x8_t colsum1_l = vmlal_u8(vmovl_u8(vget_low_u8(sC)), + vget_low_u8(sB), three_u8); + uint16x8_t colsum1_h = vmlal_u8(vmovl_u8(vget_high_u8(sC)), + vget_high_u8(sB), three_u8); + /* Add ordered dithering bias to pixel values in even output rows. */ + colsum0_l = vaddq_u16(colsum0_l, one_u16); + colsum0_h = vaddq_u16(colsum0_h, one_u16); + /* Right-shift by 2 (divide by 4), narrow to 8-bit, and combine. */ + uint8x16_t output_pixels0 = vcombine_u8(vshrn_n_u16(colsum0_l, 2), + vshrn_n_u16(colsum0_h, 2)); + uint8x16_t output_pixels1 = vcombine_u8(vrshrn_n_u16(colsum1_l, 2), + vrshrn_n_u16(colsum1_h, 2)); + /* Store pixel component values to memory. */ + vst1q_u8(outptr0 + colctr, output_pixels0); + vst1q_u8(outptr1 + colctr, output_pixels1); + } + } +} + + +/* The diagram below shows a row of samples produced by h2v1 downsampling. + * + * s0 s1 + * +---------+---------+ + * | | | + * | p0 p1 | p2 p3 | + * | | | + * +---------+---------+ + * + * Samples s0 and s1 were created by averaging the original pixel component + * values centered at positions p0-p3 above. To approximate those original + * pixel component values, we duplicate the samples horizontally: + * p0(upsampled) = p1(upsampled) = s0 + * p2(upsampled) = p3(upsampled) = s1 + */ + +void jsimd_h2v1_upsample_neon(int max_v_samp_factor, JDIMENSION output_width, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr) +{ + JSAMPARRAY output_data = *output_data_ptr; + JSAMPROW inptr, outptr; + int inrow; + unsigned colctr; + + for (inrow = 0; inrow < max_v_samp_factor; inrow++) { + inptr = input_data[inrow]; + outptr = output_data[inrow]; + for (colctr = 0; 2 * colctr < output_width; colctr += 16) { + uint8x16_t samples = vld1q_u8(inptr + colctr); + /* Duplicate the samples. The store operation below interleaves them so + * that adjacent pixel component values take on the same sample value, + * per above. + */ + uint8x16x2_t output_pixels = { { samples, samples } }; + /* Store pixel component values to memory. + * Due to the way sample buffers are allocated, we don't need to worry + * about tail cases when output_width is not a multiple of 32. See + * "Creation of 2-D sample arrays" in jmemmgr.c for details. + */ + vst2q_u8(outptr + 2 * colctr, output_pixels); + } + } +} + + +/* The diagram below shows an array of samples produced by h2v2 downsampling. + * + * s0 s1 + * +---------+---------+ + * | p0 p1 | p2 p3 | + * sA | | | + * | p4 p5 | p6 p7 | + * +---------+---------+ + * | p8 p9 | p10 p11| + * sB | | | + * | p12 p13| p14 p15| + * +---------+---------+ + * + * Samples s0A-s1B were created by averaging the original pixel component + * values centered at positions p0-p15 above. To approximate those original + * pixel component values, we duplicate the samples both horizontally and + * vertically: + * p0(upsampled) = p1(upsampled) = p4(upsampled) = p5(upsampled) = s0A + * p2(upsampled) = p3(upsampled) = p6(upsampled) = p7(upsampled) = s1A + * p8(upsampled) = p9(upsampled) = p12(upsampled) = p13(upsampled) = s0B + * p10(upsampled) = p11(upsampled) = p14(upsampled) = p15(upsampled) = s1B + */ + +void jsimd_h2v2_upsample_neon(int max_v_samp_factor, JDIMENSION output_width, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr) +{ + JSAMPARRAY output_data = *output_data_ptr; + JSAMPROW inptr, outptr0, outptr1; + int inrow, outrow; + unsigned colctr; + + for (inrow = 0, outrow = 0; outrow < max_v_samp_factor; inrow++) { + inptr = input_data[inrow]; + outptr0 = output_data[outrow++]; + outptr1 = output_data[outrow++]; + + for (colctr = 0; 2 * colctr < output_width; colctr += 16) { + uint8x16_t samples = vld1q_u8(inptr + colctr); + /* Duplicate the samples. The store operation below interleaves them so + * that adjacent pixel component values take on the same sample value, + * per above. + */ + uint8x16x2_t output_pixels = { { samples, samples } }; + /* Store pixel component values for both output rows to memory. + * Due to the way sample buffers are allocated, we don't need to worry + * about tail cases when output_width is not a multiple of 32. See + * "Creation of 2-D sample arrays" in jmemmgr.c for details. + */ + vst2q_u8(outptr0 + 2 * colctr, output_pixels); + vst2q_u8(outptr1 + 2 * colctr, output_pixels); + } + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jfdctfst-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jfdctfst-neon.c new file mode 100644 index 00000000000..bb371be3999 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jfdctfst-neon.c @@ -0,0 +1,214 @@ +/* + * jfdctfst-neon.c - fast integer FDCT (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" + +#include + + +/* jsimd_fdct_ifast_neon() performs a fast, not so accurate forward DCT + * (Discrete Cosine Transform) on one block of samples. It uses the same + * calculations and produces exactly the same output as IJG's original + * jpeg_fdct_ifast() function, which can be found in jfdctfst.c. + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.382683433 = 12544 * 2^-15 + * 0.541196100 = 17795 * 2^-15 + * 0.707106781 = 23168 * 2^-15 + * 0.306562965 = 9984 * 2^-15 + * + * See jfdctfst.c for further details of the DCT algorithm. Where possible, + * the variable names and comments here in jsimd_fdct_ifast_neon() match up + * with those in jpeg_fdct_ifast(). + */ + +#define F_0_382 12544 +#define F_0_541 17792 +#define F_0_707 23168 +#define F_0_306 9984 + + +ALIGN(16) static const int16_t jsimd_fdct_ifast_neon_consts[] = { + F_0_382, F_0_541, F_0_707, F_0_306 +}; + +void jsimd_fdct_ifast_neon(DCTELEM *data) +{ + /* Load an 8x8 block of samples into Neon registers. De-interleaving loads + * are used, followed by vuzp to transpose the block such that we have a + * column of samples per vector - allowing all rows to be processed at once. + */ + int16x8x4_t data1 = vld4q_s16(data); + int16x8x4_t data2 = vld4q_s16(data + 4 * DCTSIZE); + + int16x8x2_t cols_04 = vuzpq_s16(data1.val[0], data2.val[0]); + int16x8x2_t cols_15 = vuzpq_s16(data1.val[1], data2.val[1]); + int16x8x2_t cols_26 = vuzpq_s16(data1.val[2], data2.val[2]); + int16x8x2_t cols_37 = vuzpq_s16(data1.val[3], data2.val[3]); + + int16x8_t col0 = cols_04.val[0]; + int16x8_t col1 = cols_15.val[0]; + int16x8_t col2 = cols_26.val[0]; + int16x8_t col3 = cols_37.val[0]; + int16x8_t col4 = cols_04.val[1]; + int16x8_t col5 = cols_15.val[1]; + int16x8_t col6 = cols_26.val[1]; + int16x8_t col7 = cols_37.val[1]; + + /* Pass 1: process rows. */ + + /* Load DCT conversion constants. */ + const int16x4_t consts = vld1_s16(jsimd_fdct_ifast_neon_consts); + + int16x8_t tmp0 = vaddq_s16(col0, col7); + int16x8_t tmp7 = vsubq_s16(col0, col7); + int16x8_t tmp1 = vaddq_s16(col1, col6); + int16x8_t tmp6 = vsubq_s16(col1, col6); + int16x8_t tmp2 = vaddq_s16(col2, col5); + int16x8_t tmp5 = vsubq_s16(col2, col5); + int16x8_t tmp3 = vaddq_s16(col3, col4); + int16x8_t tmp4 = vsubq_s16(col3, col4); + + /* Even part */ + int16x8_t tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */ + int16x8_t tmp13 = vsubq_s16(tmp0, tmp3); + int16x8_t tmp11 = vaddq_s16(tmp1, tmp2); + int16x8_t tmp12 = vsubq_s16(tmp1, tmp2); + + col0 = vaddq_s16(tmp10, tmp11); /* phase 3 */ + col4 = vsubq_s16(tmp10, tmp11); + + int16x8_t z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2); + col2 = vaddq_s16(tmp13, z1); /* phase 5 */ + col6 = vsubq_s16(tmp13, z1); + + /* Odd part */ + tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */ + tmp11 = vaddq_s16(tmp5, tmp6); + tmp12 = vaddq_s16(tmp6, tmp7); + + int16x8_t z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0); + int16x8_t z2 = vqdmulhq_lane_s16(tmp10, consts, 1); + z2 = vaddq_s16(z2, z5); + int16x8_t z4 = vqdmulhq_lane_s16(tmp12, consts, 3); + z5 = vaddq_s16(tmp12, z5); + z4 = vaddq_s16(z4, z5); + int16x8_t z3 = vqdmulhq_lane_s16(tmp11, consts, 2); + + int16x8_t z11 = vaddq_s16(tmp7, z3); /* phase 5 */ + int16x8_t z13 = vsubq_s16(tmp7, z3); + + col5 = vaddq_s16(z13, z2); /* phase 6 */ + col3 = vsubq_s16(z13, z2); + col1 = vaddq_s16(z11, z4); + col7 = vsubq_s16(z11, z4); + + /* Transpose to work on columns in pass 2. */ + int16x8x2_t cols_01 = vtrnq_s16(col0, col1); + int16x8x2_t cols_23 = vtrnq_s16(col2, col3); + int16x8x2_t cols_45 = vtrnq_s16(col4, col5); + int16x8x2_t cols_67 = vtrnq_s16(col6, col7); + + int32x4x2_t cols_0145_l = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[0]), + vreinterpretq_s32_s16(cols_45.val[0])); + int32x4x2_t cols_0145_h = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[1]), + vreinterpretq_s32_s16(cols_45.val[1])); + int32x4x2_t cols_2367_l = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[0]), + vreinterpretq_s32_s16(cols_67.val[0])); + int32x4x2_t cols_2367_h = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[1]), + vreinterpretq_s32_s16(cols_67.val[1])); + + int32x4x2_t rows_04 = vzipq_s32(cols_0145_l.val[0], cols_2367_l.val[0]); + int32x4x2_t rows_15 = vzipq_s32(cols_0145_h.val[0], cols_2367_h.val[0]); + int32x4x2_t rows_26 = vzipq_s32(cols_0145_l.val[1], cols_2367_l.val[1]); + int32x4x2_t rows_37 = vzipq_s32(cols_0145_h.val[1], cols_2367_h.val[1]); + + int16x8_t row0 = vreinterpretq_s16_s32(rows_04.val[0]); + int16x8_t row1 = vreinterpretq_s16_s32(rows_15.val[0]); + int16x8_t row2 = vreinterpretq_s16_s32(rows_26.val[0]); + int16x8_t row3 = vreinterpretq_s16_s32(rows_37.val[0]); + int16x8_t row4 = vreinterpretq_s16_s32(rows_04.val[1]); + int16x8_t row5 = vreinterpretq_s16_s32(rows_15.val[1]); + int16x8_t row6 = vreinterpretq_s16_s32(rows_26.val[1]); + int16x8_t row7 = vreinterpretq_s16_s32(rows_37.val[1]); + + /* Pass 2: process columns. */ + + tmp0 = vaddq_s16(row0, row7); + tmp7 = vsubq_s16(row0, row7); + tmp1 = vaddq_s16(row1, row6); + tmp6 = vsubq_s16(row1, row6); + tmp2 = vaddq_s16(row2, row5); + tmp5 = vsubq_s16(row2, row5); + tmp3 = vaddq_s16(row3, row4); + tmp4 = vsubq_s16(row3, row4); + + /* Even part */ + tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */ + tmp13 = vsubq_s16(tmp0, tmp3); + tmp11 = vaddq_s16(tmp1, tmp2); + tmp12 = vsubq_s16(tmp1, tmp2); + + row0 = vaddq_s16(tmp10, tmp11); /* phase 3 */ + row4 = vsubq_s16(tmp10, tmp11); + + z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2); + row2 = vaddq_s16(tmp13, z1); /* phase 5 */ + row6 = vsubq_s16(tmp13, z1); + + /* Odd part */ + tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */ + tmp11 = vaddq_s16(tmp5, tmp6); + tmp12 = vaddq_s16(tmp6, tmp7); + + z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0); + z2 = vqdmulhq_lane_s16(tmp10, consts, 1); + z2 = vaddq_s16(z2, z5); + z4 = vqdmulhq_lane_s16(tmp12, consts, 3); + z5 = vaddq_s16(tmp12, z5); + z4 = vaddq_s16(z4, z5); + z3 = vqdmulhq_lane_s16(tmp11, consts, 2); + + z11 = vaddq_s16(tmp7, z3); /* phase 5 */ + z13 = vsubq_s16(tmp7, z3); + + row5 = vaddq_s16(z13, z2); /* phase 6 */ + row3 = vsubq_s16(z13, z2); + row1 = vaddq_s16(z11, z4); + row7 = vsubq_s16(z11, z4); + + vst1q_s16(data + 0 * DCTSIZE, row0); + vst1q_s16(data + 1 * DCTSIZE, row1); + vst1q_s16(data + 2 * DCTSIZE, row2); + vst1q_s16(data + 3 * DCTSIZE, row3); + vst1q_s16(data + 4 * DCTSIZE, row4); + vst1q_s16(data + 5 * DCTSIZE, row5); + vst1q_s16(data + 6 * DCTSIZE, row6); + vst1q_s16(data + 7 * DCTSIZE, row7); +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jfdctint-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jfdctint-neon.c new file mode 100644 index 00000000000..ccfc07b15d9 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jfdctint-neon.c @@ -0,0 +1,376 @@ +/* + * jfdctint-neon.c - accurate integer FDCT (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" +#include "neon-compat.h" + +#include + + +/* jsimd_fdct_islow_neon() performs a slower but more accurate forward DCT + * (Discrete Cosine Transform) on one block of samples. It uses the same + * calculations and produces exactly the same output as IJG's original + * jpeg_fdct_islow() function, which can be found in jfdctint.c. + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.298631336 = 2446 * 2^-13 + * 0.390180644 = 3196 * 2^-13 + * 0.541196100 = 4433 * 2^-13 + * 0.765366865 = 6270 * 2^-13 + * 0.899976223 = 7373 * 2^-13 + * 1.175875602 = 9633 * 2^-13 + * 1.501321110 = 12299 * 2^-13 + * 1.847759065 = 15137 * 2^-13 + * 1.961570560 = 16069 * 2^-13 + * 2.053119869 = 16819 * 2^-13 + * 2.562915447 = 20995 * 2^-13 + * 3.072711026 = 25172 * 2^-13 + * + * See jfdctint.c for further details of the DCT algorithm. Where possible, + * the variable names and comments here in jsimd_fdct_islow_neon() match up + * with those in jpeg_fdct_islow(). + */ + +#define CONST_BITS 13 +#define PASS1_BITS 2 + +#define DESCALE_P1 (CONST_BITS - PASS1_BITS) +#define DESCALE_P2 (CONST_BITS + PASS1_BITS) + +#define F_0_298 2446 +#define F_0_390 3196 +#define F_0_541 4433 +#define F_0_765 6270 +#define F_0_899 7373 +#define F_1_175 9633 +#define F_1_501 12299 +#define F_1_847 15137 +#define F_1_961 16069 +#define F_2_053 16819 +#define F_2_562 20995 +#define F_3_072 25172 + + +ALIGN(16) static const int16_t jsimd_fdct_islow_neon_consts[] = { + F_0_298, -F_0_390, F_0_541, F_0_765, + -F_0_899, F_1_175, F_1_501, -F_1_847, + -F_1_961, F_2_053, -F_2_562, F_3_072 +}; + +void jsimd_fdct_islow_neon(DCTELEM *data) +{ + /* Load DCT constants. */ +#ifdef HAVE_VLD1_S16_X3 + const int16x4x3_t consts = vld1_s16_x3(jsimd_fdct_islow_neon_consts); +#else + /* GCC does not currently support the intrinsic vld1__x3(). */ + const int16x4_t consts1 = vld1_s16(jsimd_fdct_islow_neon_consts); + const int16x4_t consts2 = vld1_s16(jsimd_fdct_islow_neon_consts + 4); + const int16x4_t consts3 = vld1_s16(jsimd_fdct_islow_neon_consts + 8); + const int16x4x3_t consts = { { consts1, consts2, consts3 } }; +#endif + + /* Load an 8x8 block of samples into Neon registers. De-interleaving loads + * are used, followed by vuzp to transpose the block such that we have a + * column of samples per vector - allowing all rows to be processed at once. + */ + int16x8x4_t s_rows_0123 = vld4q_s16(data); + int16x8x4_t s_rows_4567 = vld4q_s16(data + 4 * DCTSIZE); + + int16x8x2_t cols_04 = vuzpq_s16(s_rows_0123.val[0], s_rows_4567.val[0]); + int16x8x2_t cols_15 = vuzpq_s16(s_rows_0123.val[1], s_rows_4567.val[1]); + int16x8x2_t cols_26 = vuzpq_s16(s_rows_0123.val[2], s_rows_4567.val[2]); + int16x8x2_t cols_37 = vuzpq_s16(s_rows_0123.val[3], s_rows_4567.val[3]); + + int16x8_t col0 = cols_04.val[0]; + int16x8_t col1 = cols_15.val[0]; + int16x8_t col2 = cols_26.val[0]; + int16x8_t col3 = cols_37.val[0]; + int16x8_t col4 = cols_04.val[1]; + int16x8_t col5 = cols_15.val[1]; + int16x8_t col6 = cols_26.val[1]; + int16x8_t col7 = cols_37.val[1]; + + /* Pass 1: process rows. */ + + int16x8_t tmp0 = vaddq_s16(col0, col7); + int16x8_t tmp7 = vsubq_s16(col0, col7); + int16x8_t tmp1 = vaddq_s16(col1, col6); + int16x8_t tmp6 = vsubq_s16(col1, col6); + int16x8_t tmp2 = vaddq_s16(col2, col5); + int16x8_t tmp5 = vsubq_s16(col2, col5); + int16x8_t tmp3 = vaddq_s16(col3, col4); + int16x8_t tmp4 = vsubq_s16(col3, col4); + + /* Even part */ + int16x8_t tmp10 = vaddq_s16(tmp0, tmp3); + int16x8_t tmp13 = vsubq_s16(tmp0, tmp3); + int16x8_t tmp11 = vaddq_s16(tmp1, tmp2); + int16x8_t tmp12 = vsubq_s16(tmp1, tmp2); + + col0 = vshlq_n_s16(vaddq_s16(tmp10, tmp11), PASS1_BITS); + col4 = vshlq_n_s16(vsubq_s16(tmp10, tmp11), PASS1_BITS); + + int16x8_t tmp12_add_tmp13 = vaddq_s16(tmp12, tmp13); + int32x4_t z1_l = + vmull_lane_s16(vget_low_s16(tmp12_add_tmp13), consts.val[0], 2); + int32x4_t z1_h = + vmull_lane_s16(vget_high_s16(tmp12_add_tmp13), consts.val[0], 2); + + int32x4_t col2_scaled_l = + vmlal_lane_s16(z1_l, vget_low_s16(tmp13), consts.val[0], 3); + int32x4_t col2_scaled_h = + vmlal_lane_s16(z1_h, vget_high_s16(tmp13), consts.val[0], 3); + col2 = vcombine_s16(vrshrn_n_s32(col2_scaled_l, DESCALE_P1), + vrshrn_n_s32(col2_scaled_h, DESCALE_P1)); + + int32x4_t col6_scaled_l = + vmlal_lane_s16(z1_l, vget_low_s16(tmp12), consts.val[1], 3); + int32x4_t col6_scaled_h = + vmlal_lane_s16(z1_h, vget_high_s16(tmp12), consts.val[1], 3); + col6 = vcombine_s16(vrshrn_n_s32(col6_scaled_l, DESCALE_P1), + vrshrn_n_s32(col6_scaled_h, DESCALE_P1)); + + /* Odd part */ + int16x8_t z1 = vaddq_s16(tmp4, tmp7); + int16x8_t z2 = vaddq_s16(tmp5, tmp6); + int16x8_t z3 = vaddq_s16(tmp4, tmp6); + int16x8_t z4 = vaddq_s16(tmp5, tmp7); + /* sqrt(2) * c3 */ + int32x4_t z5_l = vmull_lane_s16(vget_low_s16(z3), consts.val[1], 1); + int32x4_t z5_h = vmull_lane_s16(vget_high_s16(z3), consts.val[1], 1); + z5_l = vmlal_lane_s16(z5_l, vget_low_s16(z4), consts.val[1], 1); + z5_h = vmlal_lane_s16(z5_h, vget_high_s16(z4), consts.val[1], 1); + + /* sqrt(2) * (-c1+c3+c5-c7) */ + int32x4_t tmp4_l = vmull_lane_s16(vget_low_s16(tmp4), consts.val[0], 0); + int32x4_t tmp4_h = vmull_lane_s16(vget_high_s16(tmp4), consts.val[0], 0); + /* sqrt(2) * ( c1+c3-c5+c7) */ + int32x4_t tmp5_l = vmull_lane_s16(vget_low_s16(tmp5), consts.val[2], 1); + int32x4_t tmp5_h = vmull_lane_s16(vget_high_s16(tmp5), consts.val[2], 1); + /* sqrt(2) * ( c1+c3+c5-c7) */ + int32x4_t tmp6_l = vmull_lane_s16(vget_low_s16(tmp6), consts.val[2], 3); + int32x4_t tmp6_h = vmull_lane_s16(vget_high_s16(tmp6), consts.val[2], 3); + /* sqrt(2) * ( c1+c3-c5-c7) */ + int32x4_t tmp7_l = vmull_lane_s16(vget_low_s16(tmp7), consts.val[1], 2); + int32x4_t tmp7_h = vmull_lane_s16(vget_high_s16(tmp7), consts.val[1], 2); + + /* sqrt(2) * (c7-c3) */ + z1_l = vmull_lane_s16(vget_low_s16(z1), consts.val[1], 0); + z1_h = vmull_lane_s16(vget_high_s16(z1), consts.val[1], 0); + /* sqrt(2) * (-c1-c3) */ + int32x4_t z2_l = vmull_lane_s16(vget_low_s16(z2), consts.val[2], 2); + int32x4_t z2_h = vmull_lane_s16(vget_high_s16(z2), consts.val[2], 2); + /* sqrt(2) * (-c3-c5) */ + int32x4_t z3_l = vmull_lane_s16(vget_low_s16(z3), consts.val[2], 0); + int32x4_t z3_h = vmull_lane_s16(vget_high_s16(z3), consts.val[2], 0); + /* sqrt(2) * (c5-c3) */ + int32x4_t z4_l = vmull_lane_s16(vget_low_s16(z4), consts.val[0], 1); + int32x4_t z4_h = vmull_lane_s16(vget_high_s16(z4), consts.val[0], 1); + + z3_l = vaddq_s32(z3_l, z5_l); + z3_h = vaddq_s32(z3_h, z5_h); + z4_l = vaddq_s32(z4_l, z5_l); + z4_h = vaddq_s32(z4_h, z5_h); + + tmp4_l = vaddq_s32(tmp4_l, z1_l); + tmp4_h = vaddq_s32(tmp4_h, z1_h); + tmp4_l = vaddq_s32(tmp4_l, z3_l); + tmp4_h = vaddq_s32(tmp4_h, z3_h); + col7 = vcombine_s16(vrshrn_n_s32(tmp4_l, DESCALE_P1), + vrshrn_n_s32(tmp4_h, DESCALE_P1)); + + tmp5_l = vaddq_s32(tmp5_l, z2_l); + tmp5_h = vaddq_s32(tmp5_h, z2_h); + tmp5_l = vaddq_s32(tmp5_l, z4_l); + tmp5_h = vaddq_s32(tmp5_h, z4_h); + col5 = vcombine_s16(vrshrn_n_s32(tmp5_l, DESCALE_P1), + vrshrn_n_s32(tmp5_h, DESCALE_P1)); + + tmp6_l = vaddq_s32(tmp6_l, z2_l); + tmp6_h = vaddq_s32(tmp6_h, z2_h); + tmp6_l = vaddq_s32(tmp6_l, z3_l); + tmp6_h = vaddq_s32(tmp6_h, z3_h); + col3 = vcombine_s16(vrshrn_n_s32(tmp6_l, DESCALE_P1), + vrshrn_n_s32(tmp6_h, DESCALE_P1)); + + tmp7_l = vaddq_s32(tmp7_l, z1_l); + tmp7_h = vaddq_s32(tmp7_h, z1_h); + tmp7_l = vaddq_s32(tmp7_l, z4_l); + tmp7_h = vaddq_s32(tmp7_h, z4_h); + col1 = vcombine_s16(vrshrn_n_s32(tmp7_l, DESCALE_P1), + vrshrn_n_s32(tmp7_h, DESCALE_P1)); + + /* Transpose to work on columns in pass 2. */ + int16x8x2_t cols_01 = vtrnq_s16(col0, col1); + int16x8x2_t cols_23 = vtrnq_s16(col2, col3); + int16x8x2_t cols_45 = vtrnq_s16(col4, col5); + int16x8x2_t cols_67 = vtrnq_s16(col6, col7); + + int32x4x2_t cols_0145_l = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[0]), + vreinterpretq_s32_s16(cols_45.val[0])); + int32x4x2_t cols_0145_h = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[1]), + vreinterpretq_s32_s16(cols_45.val[1])); + int32x4x2_t cols_2367_l = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[0]), + vreinterpretq_s32_s16(cols_67.val[0])); + int32x4x2_t cols_2367_h = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[1]), + vreinterpretq_s32_s16(cols_67.val[1])); + + int32x4x2_t rows_04 = vzipq_s32(cols_0145_l.val[0], cols_2367_l.val[0]); + int32x4x2_t rows_15 = vzipq_s32(cols_0145_h.val[0], cols_2367_h.val[0]); + int32x4x2_t rows_26 = vzipq_s32(cols_0145_l.val[1], cols_2367_l.val[1]); + int32x4x2_t rows_37 = vzipq_s32(cols_0145_h.val[1], cols_2367_h.val[1]); + + int16x8_t row0 = vreinterpretq_s16_s32(rows_04.val[0]); + int16x8_t row1 = vreinterpretq_s16_s32(rows_15.val[0]); + int16x8_t row2 = vreinterpretq_s16_s32(rows_26.val[0]); + int16x8_t row3 = vreinterpretq_s16_s32(rows_37.val[0]); + int16x8_t row4 = vreinterpretq_s16_s32(rows_04.val[1]); + int16x8_t row5 = vreinterpretq_s16_s32(rows_15.val[1]); + int16x8_t row6 = vreinterpretq_s16_s32(rows_26.val[1]); + int16x8_t row7 = vreinterpretq_s16_s32(rows_37.val[1]); + + /* Pass 2: process columns. */ + + tmp0 = vaddq_s16(row0, row7); + tmp7 = vsubq_s16(row0, row7); + tmp1 = vaddq_s16(row1, row6); + tmp6 = vsubq_s16(row1, row6); + tmp2 = vaddq_s16(row2, row5); + tmp5 = vsubq_s16(row2, row5); + tmp3 = vaddq_s16(row3, row4); + tmp4 = vsubq_s16(row3, row4); + + /* Even part */ + tmp10 = vaddq_s16(tmp0, tmp3); + tmp13 = vsubq_s16(tmp0, tmp3); + tmp11 = vaddq_s16(tmp1, tmp2); + tmp12 = vsubq_s16(tmp1, tmp2); + + row0 = vrshrq_n_s16(vaddq_s16(tmp10, tmp11), PASS1_BITS); + row4 = vrshrq_n_s16(vsubq_s16(tmp10, tmp11), PASS1_BITS); + + tmp12_add_tmp13 = vaddq_s16(tmp12, tmp13); + z1_l = vmull_lane_s16(vget_low_s16(tmp12_add_tmp13), consts.val[0], 2); + z1_h = vmull_lane_s16(vget_high_s16(tmp12_add_tmp13), consts.val[0], 2); + + int32x4_t row2_scaled_l = + vmlal_lane_s16(z1_l, vget_low_s16(tmp13), consts.val[0], 3); + int32x4_t row2_scaled_h = + vmlal_lane_s16(z1_h, vget_high_s16(tmp13), consts.val[0], 3); + row2 = vcombine_s16(vrshrn_n_s32(row2_scaled_l, DESCALE_P2), + vrshrn_n_s32(row2_scaled_h, DESCALE_P2)); + + int32x4_t row6_scaled_l = + vmlal_lane_s16(z1_l, vget_low_s16(tmp12), consts.val[1], 3); + int32x4_t row6_scaled_h = + vmlal_lane_s16(z1_h, vget_high_s16(tmp12), consts.val[1], 3); + row6 = vcombine_s16(vrshrn_n_s32(row6_scaled_l, DESCALE_P2), + vrshrn_n_s32(row6_scaled_h, DESCALE_P2)); + + /* Odd part */ + z1 = vaddq_s16(tmp4, tmp7); + z2 = vaddq_s16(tmp5, tmp6); + z3 = vaddq_s16(tmp4, tmp6); + z4 = vaddq_s16(tmp5, tmp7); + /* sqrt(2) * c3 */ + z5_l = vmull_lane_s16(vget_low_s16(z3), consts.val[1], 1); + z5_h = vmull_lane_s16(vget_high_s16(z3), consts.val[1], 1); + z5_l = vmlal_lane_s16(z5_l, vget_low_s16(z4), consts.val[1], 1); + z5_h = vmlal_lane_s16(z5_h, vget_high_s16(z4), consts.val[1], 1); + + /* sqrt(2) * (-c1+c3+c5-c7) */ + tmp4_l = vmull_lane_s16(vget_low_s16(tmp4), consts.val[0], 0); + tmp4_h = vmull_lane_s16(vget_high_s16(tmp4), consts.val[0], 0); + /* sqrt(2) * ( c1+c3-c5+c7) */ + tmp5_l = vmull_lane_s16(vget_low_s16(tmp5), consts.val[2], 1); + tmp5_h = vmull_lane_s16(vget_high_s16(tmp5), consts.val[2], 1); + /* sqrt(2) * ( c1+c3+c5-c7) */ + tmp6_l = vmull_lane_s16(vget_low_s16(tmp6), consts.val[2], 3); + tmp6_h = vmull_lane_s16(vget_high_s16(tmp6), consts.val[2], 3); + /* sqrt(2) * ( c1+c3-c5-c7) */ + tmp7_l = vmull_lane_s16(vget_low_s16(tmp7), consts.val[1], 2); + tmp7_h = vmull_lane_s16(vget_high_s16(tmp7), consts.val[1], 2); + + /* sqrt(2) * (c7-c3) */ + z1_l = vmull_lane_s16(vget_low_s16(z1), consts.val[1], 0); + z1_h = vmull_lane_s16(vget_high_s16(z1), consts.val[1], 0); + /* sqrt(2) * (-c1-c3) */ + z2_l = vmull_lane_s16(vget_low_s16(z2), consts.val[2], 2); + z2_h = vmull_lane_s16(vget_high_s16(z2), consts.val[2], 2); + /* sqrt(2) * (-c3-c5) */ + z3_l = vmull_lane_s16(vget_low_s16(z3), consts.val[2], 0); + z3_h = vmull_lane_s16(vget_high_s16(z3), consts.val[2], 0); + /* sqrt(2) * (c5-c3) */ + z4_l = vmull_lane_s16(vget_low_s16(z4), consts.val[0], 1); + z4_h = vmull_lane_s16(vget_high_s16(z4), consts.val[0], 1); + + z3_l = vaddq_s32(z3_l, z5_l); + z3_h = vaddq_s32(z3_h, z5_h); + z4_l = vaddq_s32(z4_l, z5_l); + z4_h = vaddq_s32(z4_h, z5_h); + + tmp4_l = vaddq_s32(tmp4_l, z1_l); + tmp4_h = vaddq_s32(tmp4_h, z1_h); + tmp4_l = vaddq_s32(tmp4_l, z3_l); + tmp4_h = vaddq_s32(tmp4_h, z3_h); + row7 = vcombine_s16(vrshrn_n_s32(tmp4_l, DESCALE_P2), + vrshrn_n_s32(tmp4_h, DESCALE_P2)); + + tmp5_l = vaddq_s32(tmp5_l, z2_l); + tmp5_h = vaddq_s32(tmp5_h, z2_h); + tmp5_l = vaddq_s32(tmp5_l, z4_l); + tmp5_h = vaddq_s32(tmp5_h, z4_h); + row5 = vcombine_s16(vrshrn_n_s32(tmp5_l, DESCALE_P2), + vrshrn_n_s32(tmp5_h, DESCALE_P2)); + + tmp6_l = vaddq_s32(tmp6_l, z2_l); + tmp6_h = vaddq_s32(tmp6_h, z2_h); + tmp6_l = vaddq_s32(tmp6_l, z3_l); + tmp6_h = vaddq_s32(tmp6_h, z3_h); + row3 = vcombine_s16(vrshrn_n_s32(tmp6_l, DESCALE_P2), + vrshrn_n_s32(tmp6_h, DESCALE_P2)); + + tmp7_l = vaddq_s32(tmp7_l, z1_l); + tmp7_h = vaddq_s32(tmp7_h, z1_h); + tmp7_l = vaddq_s32(tmp7_l, z4_l); + tmp7_h = vaddq_s32(tmp7_h, z4_h); + row1 = vcombine_s16(vrshrn_n_s32(tmp7_l, DESCALE_P2), + vrshrn_n_s32(tmp7_h, DESCALE_P2)); + + vst1q_s16(data + 0 * DCTSIZE, row0); + vst1q_s16(data + 1 * DCTSIZE, row1); + vst1q_s16(data + 2 * DCTSIZE, row2); + vst1q_s16(data + 3 * DCTSIZE, row3); + vst1q_s16(data + 4 * DCTSIZE, row4); + vst1q_s16(data + 5 * DCTSIZE, row5); + vst1q_s16(data + 6 * DCTSIZE, row6); + vst1q_s16(data + 7 * DCTSIZE, row7); +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jidctfst-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jidctfst-neon.c new file mode 100644 index 00000000000..a91be5362eb --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jidctfst-neon.c @@ -0,0 +1,472 @@ +/* + * jidctfst-neon.c - fast integer IDCT (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" + +#include + + +/* jsimd_idct_ifast_neon() performs dequantization and a fast, not so accurate + * inverse DCT (Discrete Cosine Transform) on one block of coefficients. It + * uses the same calculations and produces exactly the same output as IJG's + * original jpeg_idct_ifast() function, which can be found in jidctfst.c. + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.082392200 = 2688 * 2^-15 + * 0.414213562 = 13568 * 2^-15 + * 0.847759065 = 27776 * 2^-15 + * 0.613125930 = 20096 * 2^-15 + * + * See jidctfst.c for further details of the IDCT algorithm. Where possible, + * the variable names and comments here in jsimd_idct_ifast_neon() match up + * with those in jpeg_idct_ifast(). + */ + +#define PASS1_BITS 2 + +#define F_0_082 2688 +#define F_0_414 13568 +#define F_0_847 27776 +#define F_0_613 20096 + + +ALIGN(16) static const int16_t jsimd_idct_ifast_neon_consts[] = { + F_0_082, F_0_414, F_0_847, F_0_613 +}; + +void jsimd_idct_ifast_neon(void *dct_table, JCOEFPTR coef_block, + JSAMPARRAY output_buf, JDIMENSION output_col) +{ + IFAST_MULT_TYPE *quantptr = dct_table; + + /* Load DCT coefficients. */ + int16x8_t row0 = vld1q_s16(coef_block + 0 * DCTSIZE); + int16x8_t row1 = vld1q_s16(coef_block + 1 * DCTSIZE); + int16x8_t row2 = vld1q_s16(coef_block + 2 * DCTSIZE); + int16x8_t row3 = vld1q_s16(coef_block + 3 * DCTSIZE); + int16x8_t row4 = vld1q_s16(coef_block + 4 * DCTSIZE); + int16x8_t row5 = vld1q_s16(coef_block + 5 * DCTSIZE); + int16x8_t row6 = vld1q_s16(coef_block + 6 * DCTSIZE); + int16x8_t row7 = vld1q_s16(coef_block + 7 * DCTSIZE); + + /* Load quantization table values for DC coefficients. */ + int16x8_t quant_row0 = vld1q_s16(quantptr + 0 * DCTSIZE); + /* Dequantize DC coefficients. */ + row0 = vmulq_s16(row0, quant_row0); + + /* Construct bitmap to test if all AC coefficients are 0. */ + int16x8_t bitmap = vorrq_s16(row1, row2); + bitmap = vorrq_s16(bitmap, row3); + bitmap = vorrq_s16(bitmap, row4); + bitmap = vorrq_s16(bitmap, row5); + bitmap = vorrq_s16(bitmap, row6); + bitmap = vorrq_s16(bitmap, row7); + + int64_t left_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 0); + int64_t right_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 1); + + /* Load IDCT conversion constants. */ + const int16x4_t consts = vld1_s16(jsimd_idct_ifast_neon_consts); + + if (left_ac_bitmap == 0 && right_ac_bitmap == 0) { + /* All AC coefficients are zero. + * Compute DC values and duplicate into vectors. + */ + int16x8_t dcval = row0; + row1 = dcval; + row2 = dcval; + row3 = dcval; + row4 = dcval; + row5 = dcval; + row6 = dcval; + row7 = dcval; + } else if (left_ac_bitmap == 0) { + /* AC coefficients are zero for columns 0, 1, 2, and 3. + * Use DC values for these columns. + */ + int16x4_t dcval = vget_low_s16(row0); + + /* Commence regular fast IDCT computation for columns 4, 5, 6, and 7. */ + + /* Load quantization table. */ + int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE + 4); + int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE + 4); + int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE + 4); + int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE + 4); + int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE + 4); + int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE + 4); + int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE + 4); + + /* Even part: dequantize DCT coefficients. */ + int16x4_t tmp0 = vget_high_s16(row0); + int16x4_t tmp1 = vmul_s16(vget_high_s16(row2), quant_row2); + int16x4_t tmp2 = vmul_s16(vget_high_s16(row4), quant_row4); + int16x4_t tmp3 = vmul_s16(vget_high_s16(row6), quant_row6); + + int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */ + int16x4_t tmp11 = vsub_s16(tmp0, tmp2); + + int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */ + int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3); + int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1); + tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3); + tmp12 = vsub_s16(tmp12, tmp13); + + tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */ + tmp3 = vsub_s16(tmp10, tmp13); + tmp1 = vadd_s16(tmp11, tmp12); + tmp2 = vsub_s16(tmp11, tmp12); + + /* Odd part: dequantize DCT coefficients. */ + int16x4_t tmp4 = vmul_s16(vget_high_s16(row1), quant_row1); + int16x4_t tmp5 = vmul_s16(vget_high_s16(row3), quant_row3); + int16x4_t tmp6 = vmul_s16(vget_high_s16(row5), quant_row5); + int16x4_t tmp7 = vmul_s16(vget_high_s16(row7), quant_row7); + + int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */ + int16x4_t neg_z10 = vsub_s16(tmp5, tmp6); + int16x4_t z11 = vadd_s16(tmp4, tmp7); + int16x4_t z12 = vsub_s16(tmp4, tmp7); + + tmp7 = vadd_s16(z11, z13); /* phase 5 */ + int16x4_t z11_sub_z13 = vsub_s16(z11, z13); + tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vadd_s16(tmp11, z11_sub_z13); + + int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10); + int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2); + z5 = vadd_s16(z5, z10_add_z12); + tmp10 = vqdmulh_lane_s16(z12, consts, 0); + tmp10 = vadd_s16(tmp10, z12); + tmp10 = vsub_s16(tmp10, z5); + tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3); + tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10)); + tmp12 = vadd_s16(tmp12, z5); + + tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */ + tmp5 = vsub_s16(tmp11, tmp6); + tmp4 = vadd_s16(tmp10, tmp5); + + row0 = vcombine_s16(dcval, vadd_s16(tmp0, tmp7)); + row7 = vcombine_s16(dcval, vsub_s16(tmp0, tmp7)); + row1 = vcombine_s16(dcval, vadd_s16(tmp1, tmp6)); + row6 = vcombine_s16(dcval, vsub_s16(tmp1, tmp6)); + row2 = vcombine_s16(dcval, vadd_s16(tmp2, tmp5)); + row5 = vcombine_s16(dcval, vsub_s16(tmp2, tmp5)); + row4 = vcombine_s16(dcval, vadd_s16(tmp3, tmp4)); + row3 = vcombine_s16(dcval, vsub_s16(tmp3, tmp4)); + } else if (right_ac_bitmap == 0) { + /* AC coefficients are zero for columns 4, 5, 6, and 7. + * Use DC values for these columns. + */ + int16x4_t dcval = vget_high_s16(row0); + + /* Commence regular fast IDCT computation for columns 0, 1, 2, and 3. */ + + /* Load quantization table. */ + int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE); + int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE); + int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE); + int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE); + int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE); + int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE); + int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE); + + /* Even part: dequantize DCT coefficients. */ + int16x4_t tmp0 = vget_low_s16(row0); + int16x4_t tmp1 = vmul_s16(vget_low_s16(row2), quant_row2); + int16x4_t tmp2 = vmul_s16(vget_low_s16(row4), quant_row4); + int16x4_t tmp3 = vmul_s16(vget_low_s16(row6), quant_row6); + + int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */ + int16x4_t tmp11 = vsub_s16(tmp0, tmp2); + + int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */ + int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3); + int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1); + tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3); + tmp12 = vsub_s16(tmp12, tmp13); + + tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */ + tmp3 = vsub_s16(tmp10, tmp13); + tmp1 = vadd_s16(tmp11, tmp12); + tmp2 = vsub_s16(tmp11, tmp12); + + /* Odd part: dequantize DCT coefficients. */ + int16x4_t tmp4 = vmul_s16(vget_low_s16(row1), quant_row1); + int16x4_t tmp5 = vmul_s16(vget_low_s16(row3), quant_row3); + int16x4_t tmp6 = vmul_s16(vget_low_s16(row5), quant_row5); + int16x4_t tmp7 = vmul_s16(vget_low_s16(row7), quant_row7); + + int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */ + int16x4_t neg_z10 = vsub_s16(tmp5, tmp6); + int16x4_t z11 = vadd_s16(tmp4, tmp7); + int16x4_t z12 = vsub_s16(tmp4, tmp7); + + tmp7 = vadd_s16(z11, z13); /* phase 5 */ + int16x4_t z11_sub_z13 = vsub_s16(z11, z13); + tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vadd_s16(tmp11, z11_sub_z13); + + int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10); + int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2); + z5 = vadd_s16(z5, z10_add_z12); + tmp10 = vqdmulh_lane_s16(z12, consts, 0); + tmp10 = vadd_s16(tmp10, z12); + tmp10 = vsub_s16(tmp10, z5); + tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3); + tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10)); + tmp12 = vadd_s16(tmp12, z5); + + tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */ + tmp5 = vsub_s16(tmp11, tmp6); + tmp4 = vadd_s16(tmp10, tmp5); + + row0 = vcombine_s16(vadd_s16(tmp0, tmp7), dcval); + row7 = vcombine_s16(vsub_s16(tmp0, tmp7), dcval); + row1 = vcombine_s16(vadd_s16(tmp1, tmp6), dcval); + row6 = vcombine_s16(vsub_s16(tmp1, tmp6), dcval); + row2 = vcombine_s16(vadd_s16(tmp2, tmp5), dcval); + row5 = vcombine_s16(vsub_s16(tmp2, tmp5), dcval); + row4 = vcombine_s16(vadd_s16(tmp3, tmp4), dcval); + row3 = vcombine_s16(vsub_s16(tmp3, tmp4), dcval); + } else { + /* Some AC coefficients are non-zero; full IDCT calculation required. */ + + /* Load quantization table. */ + int16x8_t quant_row1 = vld1q_s16(quantptr + 1 * DCTSIZE); + int16x8_t quant_row2 = vld1q_s16(quantptr + 2 * DCTSIZE); + int16x8_t quant_row3 = vld1q_s16(quantptr + 3 * DCTSIZE); + int16x8_t quant_row4 = vld1q_s16(quantptr + 4 * DCTSIZE); + int16x8_t quant_row5 = vld1q_s16(quantptr + 5 * DCTSIZE); + int16x8_t quant_row6 = vld1q_s16(quantptr + 6 * DCTSIZE); + int16x8_t quant_row7 = vld1q_s16(quantptr + 7 * DCTSIZE); + + /* Even part: dequantize DCT coefficients. */ + int16x8_t tmp0 = row0; + int16x8_t tmp1 = vmulq_s16(row2, quant_row2); + int16x8_t tmp2 = vmulq_s16(row4, quant_row4); + int16x8_t tmp3 = vmulq_s16(row6, quant_row6); + + int16x8_t tmp10 = vaddq_s16(tmp0, tmp2); /* phase 3 */ + int16x8_t tmp11 = vsubq_s16(tmp0, tmp2); + + int16x8_t tmp13 = vaddq_s16(tmp1, tmp3); /* phases 5-3 */ + int16x8_t tmp1_sub_tmp3 = vsubq_s16(tmp1, tmp3); + int16x8_t tmp12 = vqdmulhq_lane_s16(tmp1_sub_tmp3, consts, 1); + tmp12 = vaddq_s16(tmp12, tmp1_sub_tmp3); + tmp12 = vsubq_s16(tmp12, tmp13); + + tmp0 = vaddq_s16(tmp10, tmp13); /* phase 2 */ + tmp3 = vsubq_s16(tmp10, tmp13); + tmp1 = vaddq_s16(tmp11, tmp12); + tmp2 = vsubq_s16(tmp11, tmp12); + + /* Odd part: dequantize DCT coefficients. */ + int16x8_t tmp4 = vmulq_s16(row1, quant_row1); + int16x8_t tmp5 = vmulq_s16(row3, quant_row3); + int16x8_t tmp6 = vmulq_s16(row5, quant_row5); + int16x8_t tmp7 = vmulq_s16(row7, quant_row7); + + int16x8_t z13 = vaddq_s16(tmp6, tmp5); /* phase 6 */ + int16x8_t neg_z10 = vsubq_s16(tmp5, tmp6); + int16x8_t z11 = vaddq_s16(tmp4, tmp7); + int16x8_t z12 = vsubq_s16(tmp4, tmp7); + + tmp7 = vaddq_s16(z11, z13); /* phase 5 */ + int16x8_t z11_sub_z13 = vsubq_s16(z11, z13); + tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vaddq_s16(tmp11, z11_sub_z13); + + int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10); + int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2); + z5 = vaddq_s16(z5, z10_add_z12); + tmp10 = vqdmulhq_lane_s16(z12, consts, 0); + tmp10 = vaddq_s16(tmp10, z12); + tmp10 = vsubq_s16(tmp10, z5); + tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3); + tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10)); + tmp12 = vaddq_s16(tmp12, z5); + + tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */ + tmp5 = vsubq_s16(tmp11, tmp6); + tmp4 = vaddq_s16(tmp10, tmp5); + + row0 = vaddq_s16(tmp0, tmp7); + row7 = vsubq_s16(tmp0, tmp7); + row1 = vaddq_s16(tmp1, tmp6); + row6 = vsubq_s16(tmp1, tmp6); + row2 = vaddq_s16(tmp2, tmp5); + row5 = vsubq_s16(tmp2, tmp5); + row4 = vaddq_s16(tmp3, tmp4); + row3 = vsubq_s16(tmp3, tmp4); + } + + /* Transpose rows to work on columns in pass 2. */ + int16x8x2_t rows_01 = vtrnq_s16(row0, row1); + int16x8x2_t rows_23 = vtrnq_s16(row2, row3); + int16x8x2_t rows_45 = vtrnq_s16(row4, row5); + int16x8x2_t rows_67 = vtrnq_s16(row6, row7); + + int32x4x2_t rows_0145_l = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[0]), + vreinterpretq_s32_s16(rows_45.val[0])); + int32x4x2_t rows_0145_h = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[1]), + vreinterpretq_s32_s16(rows_45.val[1])); + int32x4x2_t rows_2367_l = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[0]), + vreinterpretq_s32_s16(rows_67.val[0])); + int32x4x2_t rows_2367_h = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[1]), + vreinterpretq_s32_s16(rows_67.val[1])); + + int32x4x2_t cols_04 = vzipq_s32(rows_0145_l.val[0], rows_2367_l.val[0]); + int32x4x2_t cols_15 = vzipq_s32(rows_0145_h.val[0], rows_2367_h.val[0]); + int32x4x2_t cols_26 = vzipq_s32(rows_0145_l.val[1], rows_2367_l.val[1]); + int32x4x2_t cols_37 = vzipq_s32(rows_0145_h.val[1], rows_2367_h.val[1]); + + int16x8_t col0 = vreinterpretq_s16_s32(cols_04.val[0]); + int16x8_t col1 = vreinterpretq_s16_s32(cols_15.val[0]); + int16x8_t col2 = vreinterpretq_s16_s32(cols_26.val[0]); + int16x8_t col3 = vreinterpretq_s16_s32(cols_37.val[0]); + int16x8_t col4 = vreinterpretq_s16_s32(cols_04.val[1]); + int16x8_t col5 = vreinterpretq_s16_s32(cols_15.val[1]); + int16x8_t col6 = vreinterpretq_s16_s32(cols_26.val[1]); + int16x8_t col7 = vreinterpretq_s16_s32(cols_37.val[1]); + + /* 1-D IDCT, pass 2 */ + + /* Even part */ + int16x8_t tmp10 = vaddq_s16(col0, col4); + int16x8_t tmp11 = vsubq_s16(col0, col4); + + int16x8_t tmp13 = vaddq_s16(col2, col6); + int16x8_t col2_sub_col6 = vsubq_s16(col2, col6); + int16x8_t tmp12 = vqdmulhq_lane_s16(col2_sub_col6, consts, 1); + tmp12 = vaddq_s16(tmp12, col2_sub_col6); + tmp12 = vsubq_s16(tmp12, tmp13); + + int16x8_t tmp0 = vaddq_s16(tmp10, tmp13); + int16x8_t tmp3 = vsubq_s16(tmp10, tmp13); + int16x8_t tmp1 = vaddq_s16(tmp11, tmp12); + int16x8_t tmp2 = vsubq_s16(tmp11, tmp12); + + /* Odd part */ + int16x8_t z13 = vaddq_s16(col5, col3); + int16x8_t neg_z10 = vsubq_s16(col3, col5); + int16x8_t z11 = vaddq_s16(col1, col7); + int16x8_t z12 = vsubq_s16(col1, col7); + + int16x8_t tmp7 = vaddq_s16(z11, z13); /* phase 5 */ + int16x8_t z11_sub_z13 = vsubq_s16(z11, z13); + tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1); + tmp11 = vaddq_s16(tmp11, z11_sub_z13); + + int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10); + int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2); + z5 = vaddq_s16(z5, z10_add_z12); + tmp10 = vqdmulhq_lane_s16(z12, consts, 0); + tmp10 = vaddq_s16(tmp10, z12); + tmp10 = vsubq_s16(tmp10, z5); + tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3); + tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10)); + tmp12 = vaddq_s16(tmp12, z5); + + int16x8_t tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */ + int16x8_t tmp5 = vsubq_s16(tmp11, tmp6); + int16x8_t tmp4 = vaddq_s16(tmp10, tmp5); + + col0 = vaddq_s16(tmp0, tmp7); + col7 = vsubq_s16(tmp0, tmp7); + col1 = vaddq_s16(tmp1, tmp6); + col6 = vsubq_s16(tmp1, tmp6); + col2 = vaddq_s16(tmp2, tmp5); + col5 = vsubq_s16(tmp2, tmp5); + col4 = vaddq_s16(tmp3, tmp4); + col3 = vsubq_s16(tmp3, tmp4); + + /* Scale down by a factor of 8, narrowing to 8-bit. */ + int8x16_t cols_01_s8 = vcombine_s8(vqshrn_n_s16(col0, PASS1_BITS + 3), + vqshrn_n_s16(col1, PASS1_BITS + 3)); + int8x16_t cols_45_s8 = vcombine_s8(vqshrn_n_s16(col4, PASS1_BITS + 3), + vqshrn_n_s16(col5, PASS1_BITS + 3)); + int8x16_t cols_23_s8 = vcombine_s8(vqshrn_n_s16(col2, PASS1_BITS + 3), + vqshrn_n_s16(col3, PASS1_BITS + 3)); + int8x16_t cols_67_s8 = vcombine_s8(vqshrn_n_s16(col6, PASS1_BITS + 3), + vqshrn_n_s16(col7, PASS1_BITS + 3)); + /* Clamp to range [0-255]. */ + uint8x16_t cols_01 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_01_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + uint8x16_t cols_45 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_45_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + uint8x16_t cols_23 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_23_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + uint8x16_t cols_67 = + vreinterpretq_u8_s8 + (vaddq_s8(cols_67_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE)))); + + /* Transpose block to prepare for store. */ + uint32x4x2_t cols_0415 = vzipq_u32(vreinterpretq_u32_u8(cols_01), + vreinterpretq_u32_u8(cols_45)); + uint32x4x2_t cols_2637 = vzipq_u32(vreinterpretq_u32_u8(cols_23), + vreinterpretq_u32_u8(cols_67)); + + uint8x16x2_t cols_0145 = vtrnq_u8(vreinterpretq_u8_u32(cols_0415.val[0]), + vreinterpretq_u8_u32(cols_0415.val[1])); + uint8x16x2_t cols_2367 = vtrnq_u8(vreinterpretq_u8_u32(cols_2637.val[0]), + vreinterpretq_u8_u32(cols_2637.val[1])); + uint16x8x2_t rows_0426 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[0]), + vreinterpretq_u16_u8(cols_2367.val[0])); + uint16x8x2_t rows_1537 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[1]), + vreinterpretq_u16_u8(cols_2367.val[1])); + + uint8x16_t rows_04 = vreinterpretq_u8_u16(rows_0426.val[0]); + uint8x16_t rows_15 = vreinterpretq_u8_u16(rows_1537.val[0]); + uint8x16_t rows_26 = vreinterpretq_u8_u16(rows_0426.val[1]); + uint8x16_t rows_37 = vreinterpretq_u8_u16(rows_1537.val[1]); + + JSAMPROW outptr0 = output_buf[0] + output_col; + JSAMPROW outptr1 = output_buf[1] + output_col; + JSAMPROW outptr2 = output_buf[2] + output_col; + JSAMPROW outptr3 = output_buf[3] + output_col; + JSAMPROW outptr4 = output_buf[4] + output_col; + JSAMPROW outptr5 = output_buf[5] + output_col; + JSAMPROW outptr6 = output_buf[6] + output_col; + JSAMPROW outptr7 = output_buf[7] + output_col; + + /* Store DCT block to memory. */ + vst1q_lane_u64((uint64_t *)outptr0, vreinterpretq_u64_u8(rows_04), 0); + vst1q_lane_u64((uint64_t *)outptr1, vreinterpretq_u64_u8(rows_15), 0); + vst1q_lane_u64((uint64_t *)outptr2, vreinterpretq_u64_u8(rows_26), 0); + vst1q_lane_u64((uint64_t *)outptr3, vreinterpretq_u64_u8(rows_37), 0); + vst1q_lane_u64((uint64_t *)outptr4, vreinterpretq_u64_u8(rows_04), 1); + vst1q_lane_u64((uint64_t *)outptr5, vreinterpretq_u64_u8(rows_15), 1); + vst1q_lane_u64((uint64_t *)outptr6, vreinterpretq_u64_u8(rows_26), 1); + vst1q_lane_u64((uint64_t *)outptr7, vreinterpretq_u64_u8(rows_37), 1); +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jidctint-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jidctint-neon.c new file mode 100644 index 00000000000..d25112ef7fd --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jidctint-neon.c @@ -0,0 +1,801 @@ +/* + * jidctint-neon.c - accurate integer IDCT (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" +#include "neon-compat.h" + +#include + + +#define CONST_BITS 13 +#define PASS1_BITS 2 + +#define DESCALE_P1 (CONST_BITS - PASS1_BITS) +#define DESCALE_P2 (CONST_BITS + PASS1_BITS + 3) + +/* The computation of the inverse DCT requires the use of constants known at + * compile time. Scaled integer constants are used to avoid floating-point + * arithmetic: + * 0.298631336 = 2446 * 2^-13 + * 0.390180644 = 3196 * 2^-13 + * 0.541196100 = 4433 * 2^-13 + * 0.765366865 = 6270 * 2^-13 + * 0.899976223 = 7373 * 2^-13 + * 1.175875602 = 9633 * 2^-13 + * 1.501321110 = 12299 * 2^-13 + * 1.847759065 = 15137 * 2^-13 + * 1.961570560 = 16069 * 2^-13 + * 2.053119869 = 16819 * 2^-13 + * 2.562915447 = 20995 * 2^-13 + * 3.072711026 = 25172 * 2^-13 + */ + +#define F_0_298 2446 +#define F_0_390 3196 +#define F_0_541 4433 +#define F_0_765 6270 +#define F_0_899 7373 +#define F_1_175 9633 +#define F_1_501 12299 +#define F_1_847 15137 +#define F_1_961 16069 +#define F_2_053 16819 +#define F_2_562 20995 +#define F_3_072 25172 + +#define F_1_175_MINUS_1_961 (F_1_175 - F_1_961) +#define F_1_175_MINUS_0_390 (F_1_175 - F_0_390) +#define F_0_541_MINUS_1_847 (F_0_541 - F_1_847) +#define F_3_072_MINUS_2_562 (F_3_072 - F_2_562) +#define F_0_298_MINUS_0_899 (F_0_298 - F_0_899) +#define F_1_501_MINUS_0_899 (F_1_501 - F_0_899) +#define F_2_053_MINUS_2_562 (F_2_053 - F_2_562) +#define F_0_541_PLUS_0_765 (F_0_541 + F_0_765) + + +ALIGN(16) static const int16_t jsimd_idct_islow_neon_consts[] = { + F_0_899, F_0_541, + F_2_562, F_0_298_MINUS_0_899, + F_1_501_MINUS_0_899, F_2_053_MINUS_2_562, + F_0_541_PLUS_0_765, F_1_175, + F_1_175_MINUS_0_390, F_0_541_MINUS_1_847, + F_3_072_MINUS_2_562, F_1_175_MINUS_1_961, + 0, 0, 0, 0 +}; + + +/* Forward declaration of regular and sparse IDCT helper functions */ + +static INLINE void jsimd_idct_islow_pass1_regular(int16x4_t row0, + int16x4_t row1, + int16x4_t row2, + int16x4_t row3, + int16x4_t row4, + int16x4_t row5, + int16x4_t row6, + int16x4_t row7, + int16x4_t quant_row0, + int16x4_t quant_row1, + int16x4_t quant_row2, + int16x4_t quant_row3, + int16x4_t quant_row4, + int16x4_t quant_row5, + int16x4_t quant_row6, + int16x4_t quant_row7, + int16_t *workspace_1, + int16_t *workspace_2); + +static INLINE void jsimd_idct_islow_pass1_sparse(int16x4_t row0, + int16x4_t row1, + int16x4_t row2, + int16x4_t row3, + int16x4_t quant_row0, + int16x4_t quant_row1, + int16x4_t quant_row2, + int16x4_t quant_row3, + int16_t *workspace_1, + int16_t *workspace_2); + +static INLINE void jsimd_idct_islow_pass2_regular(int16_t *workspace, + JSAMPARRAY output_buf, + JDIMENSION output_col, + unsigned buf_offset); + +static INLINE void jsimd_idct_islow_pass2_sparse(int16_t *workspace, + JSAMPARRAY output_buf, + JDIMENSION output_col, + unsigned buf_offset); + + +/* Perform dequantization and inverse DCT on one block of coefficients. For + * reference, the C implementation (jpeg_idct_slow()) can be found in + * jidctint.c. + * + * Optimization techniques used for fast data access: + * + * In each pass, the inverse DCT is computed for the left and right 4x8 halves + * of the DCT block. This avoids spilling due to register pressure, and the + * increased granularity allows for an optimized calculation depending on the + * values of the DCT coefficients. Between passes, intermediate data is stored + * in 4x8 workspace buffers. + * + * Transposing the 8x8 DCT block after each pass can be achieved by transposing + * each of the four 4x4 quadrants and swapping quadrants 1 and 2 (refer to the + * diagram below.) Swapping quadrants is cheap, since the second pass can just + * swap the workspace buffer pointers. + * + * +-------+-------+ +-------+-------+ + * | | | | | | + * | 0 | 1 | | 0 | 2 | + * | | | transpose | | | + * +-------+-------+ ------> +-------+-------+ + * | | | | | | + * | 2 | 3 | | 1 | 3 | + * | | | | | | + * +-------+-------+ +-------+-------+ + * + * Optimization techniques used to accelerate the inverse DCT calculation: + * + * In a DCT coefficient block, the coefficients are increasingly likely to be 0 + * as you move diagonally from top left to bottom right. If whole rows of + * coefficients are 0, then the inverse DCT calculation can be simplified. On + * the first pass of the inverse DCT, we test for three special cases before + * defaulting to a full "regular" inverse DCT: + * + * 1) Coefficients in rows 4-7 are all zero. In this case, we perform a + * "sparse" simplified inverse DCT on rows 0-3. + * 2) AC coefficients (rows 1-7) are all zero. In this case, the inverse DCT + * result is equal to the dequantized DC coefficients. + * 3) AC and DC coefficients are all zero. In this case, the inverse DCT + * result is all zero. For the left 4x8 half, this is handled identically + * to Case 2 above. For the right 4x8 half, we do no work and signal that + * the "sparse" algorithm is required for the second pass. + * + * In the second pass, only a single special case is tested: whether the AC and + * DC coefficients were all zero in the right 4x8 block during the first pass + * (refer to Case 3 above.) If this is the case, then a "sparse" variant of + * the second pass is performed for both the left and right halves of the DCT + * block. (The transposition after the first pass means that the right 4x8 + * block during the first pass becomes rows 4-7 during the second pass.) + */ + +void jsimd_idct_islow_neon(void *dct_table, JCOEFPTR coef_block, + JSAMPARRAY output_buf, JDIMENSION output_col) +{ + ISLOW_MULT_TYPE *quantptr = dct_table; + + int16_t workspace_l[8 * DCTSIZE / 2]; + int16_t workspace_r[8 * DCTSIZE / 2]; + + /* Compute IDCT first pass on left 4x8 coefficient block. */ + + /* Load DCT coefficients in left 4x8 block. */ + int16x4_t row0 = vld1_s16(coef_block + 0 * DCTSIZE); + int16x4_t row1 = vld1_s16(coef_block + 1 * DCTSIZE); + int16x4_t row2 = vld1_s16(coef_block + 2 * DCTSIZE); + int16x4_t row3 = vld1_s16(coef_block + 3 * DCTSIZE); + int16x4_t row4 = vld1_s16(coef_block + 4 * DCTSIZE); + int16x4_t row5 = vld1_s16(coef_block + 5 * DCTSIZE); + int16x4_t row6 = vld1_s16(coef_block + 6 * DCTSIZE); + int16x4_t row7 = vld1_s16(coef_block + 7 * DCTSIZE); + + /* Load quantization table for left 4x8 block. */ + int16x4_t quant_row0 = vld1_s16(quantptr + 0 * DCTSIZE); + int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE); + int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE); + int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE); + int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE); + int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE); + int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE); + int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE); + + /* Construct bitmap to test if DCT coefficients in left 4x8 block are 0. */ + int16x4_t bitmap = vorr_s16(row7, row6); + bitmap = vorr_s16(bitmap, row5); + bitmap = vorr_s16(bitmap, row4); + int64_t bitmap_rows_4567 = vget_lane_s64(vreinterpret_s64_s16(bitmap), 0); + + if (bitmap_rows_4567 == 0) { + bitmap = vorr_s16(bitmap, row3); + bitmap = vorr_s16(bitmap, row2); + bitmap = vorr_s16(bitmap, row1); + int64_t left_ac_bitmap = vget_lane_s64(vreinterpret_s64_s16(bitmap), 0); + + if (left_ac_bitmap == 0) { + int16x4_t dcval = vshl_n_s16(vmul_s16(row0, quant_row0), PASS1_BITS); + int16x4x4_t quadrant = { { dcval, dcval, dcval, dcval } }; + /* Store 4x4 blocks to workspace, transposing in the process. */ + vst4_s16(workspace_l, quadrant); + vst4_s16(workspace_r, quadrant); + } else { + jsimd_idct_islow_pass1_sparse(row0, row1, row2, row3, quant_row0, + quant_row1, quant_row2, quant_row3, + workspace_l, workspace_r); + } + } else { + jsimd_idct_islow_pass1_regular(row0, row1, row2, row3, row4, row5, + row6, row7, quant_row0, quant_row1, + quant_row2, quant_row3, quant_row4, + quant_row5, quant_row6, quant_row7, + workspace_l, workspace_r); + } + + /* Compute IDCT first pass on right 4x8 coefficient block. */ + + /* Load DCT coefficients in right 4x8 block. */ + row0 = vld1_s16(coef_block + 0 * DCTSIZE + 4); + row1 = vld1_s16(coef_block + 1 * DCTSIZE + 4); + row2 = vld1_s16(coef_block + 2 * DCTSIZE + 4); + row3 = vld1_s16(coef_block + 3 * DCTSIZE + 4); + row4 = vld1_s16(coef_block + 4 * DCTSIZE + 4); + row5 = vld1_s16(coef_block + 5 * DCTSIZE + 4); + row6 = vld1_s16(coef_block + 6 * DCTSIZE + 4); + row7 = vld1_s16(coef_block + 7 * DCTSIZE + 4); + + /* Load quantization table for right 4x8 block. */ + quant_row0 = vld1_s16(quantptr + 0 * DCTSIZE + 4); + quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE + 4); + quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE + 4); + quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE + 4); + quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE + 4); + quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE + 4); + quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE + 4); + quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE + 4); + + /* Construct bitmap to test if DCT coefficients in right 4x8 block are 0. */ + bitmap = vorr_s16(row7, row6); + bitmap = vorr_s16(bitmap, row5); + bitmap = vorr_s16(bitmap, row4); + bitmap_rows_4567 = vget_lane_s64(vreinterpret_s64_s16(bitmap), 0); + bitmap = vorr_s16(bitmap, row3); + bitmap = vorr_s16(bitmap, row2); + bitmap = vorr_s16(bitmap, row1); + int64_t right_ac_bitmap = vget_lane_s64(vreinterpret_s64_s16(bitmap), 0); + + /* If this remains non-zero, a "regular" second pass will be performed. */ + int64_t right_ac_dc_bitmap = 1; + + if (right_ac_bitmap == 0) { + bitmap = vorr_s16(bitmap, row0); + right_ac_dc_bitmap = vget_lane_s64(vreinterpret_s64_s16(bitmap), 0); + + if (right_ac_dc_bitmap != 0) { + int16x4_t dcval = vshl_n_s16(vmul_s16(row0, quant_row0), PASS1_BITS); + int16x4x4_t quadrant = { { dcval, dcval, dcval, dcval } }; + /* Store 4x4 blocks to workspace, transposing in the process. */ + vst4_s16(workspace_l + 4 * DCTSIZE / 2, quadrant); + vst4_s16(workspace_r + 4 * DCTSIZE / 2, quadrant); + } + } else { + if (bitmap_rows_4567 == 0) { + jsimd_idct_islow_pass1_sparse(row0, row1, row2, row3, quant_row0, + quant_row1, quant_row2, quant_row3, + workspace_l + 4 * DCTSIZE / 2, + workspace_r + 4 * DCTSIZE / 2); + } else { + jsimd_idct_islow_pass1_regular(row0, row1, row2, row3, row4, row5, + row6, row7, quant_row0, quant_row1, + quant_row2, quant_row3, quant_row4, + quant_row5, quant_row6, quant_row7, + workspace_l + 4 * DCTSIZE / 2, + workspace_r + 4 * DCTSIZE / 2); + } + } + + /* Second pass: compute IDCT on rows in workspace. */ + + /* If all coefficients in right 4x8 block are 0, use "sparse" second pass. */ + if (right_ac_dc_bitmap == 0) { + jsimd_idct_islow_pass2_sparse(workspace_l, output_buf, output_col, 0); + jsimd_idct_islow_pass2_sparse(workspace_r, output_buf, output_col, 4); + } else { + jsimd_idct_islow_pass2_regular(workspace_l, output_buf, output_col, 0); + jsimd_idct_islow_pass2_regular(workspace_r, output_buf, output_col, 4); + } +} + + +/* Perform dequantization and the first pass of the accurate inverse DCT on a + * 4x8 block of coefficients. (To process the full 8x8 DCT block, this + * function-- or some other optimized variant-- needs to be called for both the + * left and right 4x8 blocks.) + * + * This "regular" version assumes that no optimization can be made to the IDCT + * calculation, since no useful set of AC coefficients is all 0. + * + * The original C implementation of the accurate IDCT (jpeg_idct_slow()) can be + * found in jidctint.c. Algorithmic changes made here are documented inline. + */ + +static INLINE void jsimd_idct_islow_pass1_regular(int16x4_t row0, + int16x4_t row1, + int16x4_t row2, + int16x4_t row3, + int16x4_t row4, + int16x4_t row5, + int16x4_t row6, + int16x4_t row7, + int16x4_t quant_row0, + int16x4_t quant_row1, + int16x4_t quant_row2, + int16x4_t quant_row3, + int16x4_t quant_row4, + int16x4_t quant_row5, + int16x4_t quant_row6, + int16x4_t quant_row7, + int16_t *workspace_1, + int16_t *workspace_2) +{ + /* Load constants for IDCT computation. */ +#ifdef HAVE_VLD1_S16_X3 + const int16x4x3_t consts = vld1_s16_x3(jsimd_idct_islow_neon_consts); +#else + const int16x4_t consts1 = vld1_s16(jsimd_idct_islow_neon_consts); + const int16x4_t consts2 = vld1_s16(jsimd_idct_islow_neon_consts + 4); + const int16x4_t consts3 = vld1_s16(jsimd_idct_islow_neon_consts + 8); + const int16x4x3_t consts = { { consts1, consts2, consts3 } }; +#endif + + /* Even part */ + int16x4_t z2_s16 = vmul_s16(row2, quant_row2); + int16x4_t z3_s16 = vmul_s16(row6, quant_row6); + + int32x4_t tmp2 = vmull_lane_s16(z2_s16, consts.val[0], 1); + int32x4_t tmp3 = vmull_lane_s16(z2_s16, consts.val[1], 2); + tmp2 = vmlal_lane_s16(tmp2, z3_s16, consts.val[2], 1); + tmp3 = vmlal_lane_s16(tmp3, z3_s16, consts.val[0], 1); + + z2_s16 = vmul_s16(row0, quant_row0); + z3_s16 = vmul_s16(row4, quant_row4); + + int32x4_t tmp0 = vshll_n_s16(vadd_s16(z2_s16, z3_s16), CONST_BITS); + int32x4_t tmp1 = vshll_n_s16(vsub_s16(z2_s16, z3_s16), CONST_BITS); + + int32x4_t tmp10 = vaddq_s32(tmp0, tmp3); + int32x4_t tmp13 = vsubq_s32(tmp0, tmp3); + int32x4_t tmp11 = vaddq_s32(tmp1, tmp2); + int32x4_t tmp12 = vsubq_s32(tmp1, tmp2); + + /* Odd part */ + int16x4_t tmp0_s16 = vmul_s16(row7, quant_row7); + int16x4_t tmp1_s16 = vmul_s16(row5, quant_row5); + int16x4_t tmp2_s16 = vmul_s16(row3, quant_row3); + int16x4_t tmp3_s16 = vmul_s16(row1, quant_row1); + + z3_s16 = vadd_s16(tmp0_s16, tmp2_s16); + int16x4_t z4_s16 = vadd_s16(tmp1_s16, tmp3_s16); + + /* Implementation as per jpeg_idct_islow() in jidctint.c: + * z5 = (z3 + z4) * 1.175875602; + * z3 = z3 * -1.961570560; z4 = z4 * -0.390180644; + * z3 += z5; z4 += z5; + * + * This implementation: + * z3 = z3 * (1.175875602 - 1.961570560) + z4 * 1.175875602; + * z4 = z3 * 1.175875602 + z4 * (1.175875602 - 0.390180644); + */ + + int32x4_t z3 = vmull_lane_s16(z3_s16, consts.val[2], 3); + int32x4_t z4 = vmull_lane_s16(z3_s16, consts.val[1], 3); + z3 = vmlal_lane_s16(z3, z4_s16, consts.val[1], 3); + z4 = vmlal_lane_s16(z4, z4_s16, consts.val[2], 0); + + /* Implementation as per jpeg_idct_islow() in jidctint.c: + * z1 = tmp0 + tmp3; z2 = tmp1 + tmp2; + * tmp0 = tmp0 * 0.298631336; tmp1 = tmp1 * 2.053119869; + * tmp2 = tmp2 * 3.072711026; tmp3 = tmp3 * 1.501321110; + * z1 = z1 * -0.899976223; z2 = z2 * -2.562915447; + * tmp0 += z1 + z3; tmp1 += z2 + z4; + * tmp2 += z2 + z3; tmp3 += z1 + z4; + * + * This implementation: + * tmp0 = tmp0 * (0.298631336 - 0.899976223) + tmp3 * -0.899976223; + * tmp1 = tmp1 * (2.053119869 - 2.562915447) + tmp2 * -2.562915447; + * tmp2 = tmp1 * -2.562915447 + tmp2 * (3.072711026 - 2.562915447); + * tmp3 = tmp0 * -0.899976223 + tmp3 * (1.501321110 - 0.899976223); + * tmp0 += z3; tmp1 += z4; + * tmp2 += z3; tmp3 += z4; + */ + + tmp0 = vmull_lane_s16(tmp0_s16, consts.val[0], 3); + tmp1 = vmull_lane_s16(tmp1_s16, consts.val[1], 1); + tmp2 = vmull_lane_s16(tmp2_s16, consts.val[2], 2); + tmp3 = vmull_lane_s16(tmp3_s16, consts.val[1], 0); + + tmp0 = vmlsl_lane_s16(tmp0, tmp3_s16, consts.val[0], 0); + tmp1 = vmlsl_lane_s16(tmp1, tmp2_s16, consts.val[0], 2); + tmp2 = vmlsl_lane_s16(tmp2, tmp1_s16, consts.val[0], 2); + tmp3 = vmlsl_lane_s16(tmp3, tmp0_s16, consts.val[0], 0); + + tmp0 = vaddq_s32(tmp0, z3); + tmp1 = vaddq_s32(tmp1, z4); + tmp2 = vaddq_s32(tmp2, z3); + tmp3 = vaddq_s32(tmp3, z4); + + /* Final output stage: descale and narrow to 16-bit. */ + int16x4x4_t rows_0123 = { { + vrshrn_n_s32(vaddq_s32(tmp10, tmp3), DESCALE_P1), + vrshrn_n_s32(vaddq_s32(tmp11, tmp2), DESCALE_P1), + vrshrn_n_s32(vaddq_s32(tmp12, tmp1), DESCALE_P1), + vrshrn_n_s32(vaddq_s32(tmp13, tmp0), DESCALE_P1) + } }; + int16x4x4_t rows_4567 = { { + vrshrn_n_s32(vsubq_s32(tmp13, tmp0), DESCALE_P1), + vrshrn_n_s32(vsubq_s32(tmp12, tmp1), DESCALE_P1), + vrshrn_n_s32(vsubq_s32(tmp11, tmp2), DESCALE_P1), + vrshrn_n_s32(vsubq_s32(tmp10, tmp3), DESCALE_P1) + } }; + + /* Store 4x4 blocks to the intermediate workspace, ready for the second pass. + * (VST4 transposes the blocks. We need to operate on rows in the next + * pass.) + */ + vst4_s16(workspace_1, rows_0123); + vst4_s16(workspace_2, rows_4567); +} + + +/* Perform dequantization and the first pass of the accurate inverse DCT on a + * 4x8 block of coefficients. + * + * This "sparse" version assumes that the AC coefficients in rows 4-7 are all + * 0. This simplifies the IDCT calculation, accelerating overall performance. + */ + +static INLINE void jsimd_idct_islow_pass1_sparse(int16x4_t row0, + int16x4_t row1, + int16x4_t row2, + int16x4_t row3, + int16x4_t quant_row0, + int16x4_t quant_row1, + int16x4_t quant_row2, + int16x4_t quant_row3, + int16_t *workspace_1, + int16_t *workspace_2) +{ + /* Load constants for IDCT computation. */ +#ifdef HAVE_VLD1_S16_X3 + const int16x4x3_t consts = vld1_s16_x3(jsimd_idct_islow_neon_consts); +#else + const int16x4_t consts1 = vld1_s16(jsimd_idct_islow_neon_consts); + const int16x4_t consts2 = vld1_s16(jsimd_idct_islow_neon_consts + 4); + const int16x4_t consts3 = vld1_s16(jsimd_idct_islow_neon_consts + 8); + const int16x4x3_t consts = { { consts1, consts2, consts3 } }; +#endif + + /* Even part (z3 is all 0) */ + int16x4_t z2_s16 = vmul_s16(row2, quant_row2); + + int32x4_t tmp2 = vmull_lane_s16(z2_s16, consts.val[0], 1); + int32x4_t tmp3 = vmull_lane_s16(z2_s16, consts.val[1], 2); + + z2_s16 = vmul_s16(row0, quant_row0); + int32x4_t tmp0 = vshll_n_s16(z2_s16, CONST_BITS); + int32x4_t tmp1 = vshll_n_s16(z2_s16, CONST_BITS); + + int32x4_t tmp10 = vaddq_s32(tmp0, tmp3); + int32x4_t tmp13 = vsubq_s32(tmp0, tmp3); + int32x4_t tmp11 = vaddq_s32(tmp1, tmp2); + int32x4_t tmp12 = vsubq_s32(tmp1, tmp2); + + /* Odd part (tmp0 and tmp1 are both all 0) */ + int16x4_t tmp2_s16 = vmul_s16(row3, quant_row3); + int16x4_t tmp3_s16 = vmul_s16(row1, quant_row1); + + int16x4_t z3_s16 = tmp2_s16; + int16x4_t z4_s16 = tmp3_s16; + + int32x4_t z3 = vmull_lane_s16(z3_s16, consts.val[2], 3); + int32x4_t z4 = vmull_lane_s16(z3_s16, consts.val[1], 3); + z3 = vmlal_lane_s16(z3, z4_s16, consts.val[1], 3); + z4 = vmlal_lane_s16(z4, z4_s16, consts.val[2], 0); + + tmp0 = vmlsl_lane_s16(z3, tmp3_s16, consts.val[0], 0); + tmp1 = vmlsl_lane_s16(z4, tmp2_s16, consts.val[0], 2); + tmp2 = vmlal_lane_s16(z3, tmp2_s16, consts.val[2], 2); + tmp3 = vmlal_lane_s16(z4, tmp3_s16, consts.val[1], 0); + + /* Final output stage: descale and narrow to 16-bit. */ + int16x4x4_t rows_0123 = { { + vrshrn_n_s32(vaddq_s32(tmp10, tmp3), DESCALE_P1), + vrshrn_n_s32(vaddq_s32(tmp11, tmp2), DESCALE_P1), + vrshrn_n_s32(vaddq_s32(tmp12, tmp1), DESCALE_P1), + vrshrn_n_s32(vaddq_s32(tmp13, tmp0), DESCALE_P1) + } }; + int16x4x4_t rows_4567 = { { + vrshrn_n_s32(vsubq_s32(tmp13, tmp0), DESCALE_P1), + vrshrn_n_s32(vsubq_s32(tmp12, tmp1), DESCALE_P1), + vrshrn_n_s32(vsubq_s32(tmp11, tmp2), DESCALE_P1), + vrshrn_n_s32(vsubq_s32(tmp10, tmp3), DESCALE_P1) + } }; + + /* Store 4x4 blocks to the intermediate workspace, ready for the second pass. + * (VST4 transposes the blocks. We need to operate on rows in the next + * pass.) + */ + vst4_s16(workspace_1, rows_0123); + vst4_s16(workspace_2, rows_4567); +} + + +/* Perform the second pass of the accurate inverse DCT on a 4x8 block of + * coefficients. (To process the full 8x8 DCT block, this function-- or some + * other optimized variant-- needs to be called for both the right and left 4x8 + * blocks.) + * + * This "regular" version assumes that no optimization can be made to the IDCT + * calculation, since no useful set of coefficient values are all 0 after the + * first pass. + * + * Again, the original C implementation of the accurate IDCT (jpeg_idct_slow()) + * can be found in jidctint.c. Algorithmic changes made here are documented + * inline. + */ + +static INLINE void jsimd_idct_islow_pass2_regular(int16_t *workspace, + JSAMPARRAY output_buf, + JDIMENSION output_col, + unsigned buf_offset) +{ + /* Load constants for IDCT computation. */ +#ifdef HAVE_VLD1_S16_X3 + const int16x4x3_t consts = vld1_s16_x3(jsimd_idct_islow_neon_consts); +#else + const int16x4_t consts1 = vld1_s16(jsimd_idct_islow_neon_consts); + const int16x4_t consts2 = vld1_s16(jsimd_idct_islow_neon_consts + 4); + const int16x4_t consts3 = vld1_s16(jsimd_idct_islow_neon_consts + 8); + const int16x4x3_t consts = { { consts1, consts2, consts3 } }; +#endif + + /* Even part */ + int16x4_t z2_s16 = vld1_s16(workspace + 2 * DCTSIZE / 2); + int16x4_t z3_s16 = vld1_s16(workspace + 6 * DCTSIZE / 2); + + int32x4_t tmp2 = vmull_lane_s16(z2_s16, consts.val[0], 1); + int32x4_t tmp3 = vmull_lane_s16(z2_s16, consts.val[1], 2); + tmp2 = vmlal_lane_s16(tmp2, z3_s16, consts.val[2], 1); + tmp3 = vmlal_lane_s16(tmp3, z3_s16, consts.val[0], 1); + + z2_s16 = vld1_s16(workspace + 0 * DCTSIZE / 2); + z3_s16 = vld1_s16(workspace + 4 * DCTSIZE / 2); + + int32x4_t tmp0 = vshll_n_s16(vadd_s16(z2_s16, z3_s16), CONST_BITS); + int32x4_t tmp1 = vshll_n_s16(vsub_s16(z2_s16, z3_s16), CONST_BITS); + + int32x4_t tmp10 = vaddq_s32(tmp0, tmp3); + int32x4_t tmp13 = vsubq_s32(tmp0, tmp3); + int32x4_t tmp11 = vaddq_s32(tmp1, tmp2); + int32x4_t tmp12 = vsubq_s32(tmp1, tmp2); + + /* Odd part */ + int16x4_t tmp0_s16 = vld1_s16(workspace + 7 * DCTSIZE / 2); + int16x4_t tmp1_s16 = vld1_s16(workspace + 5 * DCTSIZE / 2); + int16x4_t tmp2_s16 = vld1_s16(workspace + 3 * DCTSIZE / 2); + int16x4_t tmp3_s16 = vld1_s16(workspace + 1 * DCTSIZE / 2); + + z3_s16 = vadd_s16(tmp0_s16, tmp2_s16); + int16x4_t z4_s16 = vadd_s16(tmp1_s16, tmp3_s16); + + /* Implementation as per jpeg_idct_islow() in jidctint.c: + * z5 = (z3 + z4) * 1.175875602; + * z3 = z3 * -1.961570560; z4 = z4 * -0.390180644; + * z3 += z5; z4 += z5; + * + * This implementation: + * z3 = z3 * (1.175875602 - 1.961570560) + z4 * 1.175875602; + * z4 = z3 * 1.175875602 + z4 * (1.175875602 - 0.390180644); + */ + + int32x4_t z3 = vmull_lane_s16(z3_s16, consts.val[2], 3); + int32x4_t z4 = vmull_lane_s16(z3_s16, consts.val[1], 3); + z3 = vmlal_lane_s16(z3, z4_s16, consts.val[1], 3); + z4 = vmlal_lane_s16(z4, z4_s16, consts.val[2], 0); + + /* Implementation as per jpeg_idct_islow() in jidctint.c: + * z1 = tmp0 + tmp3; z2 = tmp1 + tmp2; + * tmp0 = tmp0 * 0.298631336; tmp1 = tmp1 * 2.053119869; + * tmp2 = tmp2 * 3.072711026; tmp3 = tmp3 * 1.501321110; + * z1 = z1 * -0.899976223; z2 = z2 * -2.562915447; + * tmp0 += z1 + z3; tmp1 += z2 + z4; + * tmp2 += z2 + z3; tmp3 += z1 + z4; + * + * This implementation: + * tmp0 = tmp0 * (0.298631336 - 0.899976223) + tmp3 * -0.899976223; + * tmp1 = tmp1 * (2.053119869 - 2.562915447) + tmp2 * -2.562915447; + * tmp2 = tmp1 * -2.562915447 + tmp2 * (3.072711026 - 2.562915447); + * tmp3 = tmp0 * -0.899976223 + tmp3 * (1.501321110 - 0.899976223); + * tmp0 += z3; tmp1 += z4; + * tmp2 += z3; tmp3 += z4; + */ + + tmp0 = vmull_lane_s16(tmp0_s16, consts.val[0], 3); + tmp1 = vmull_lane_s16(tmp1_s16, consts.val[1], 1); + tmp2 = vmull_lane_s16(tmp2_s16, consts.val[2], 2); + tmp3 = vmull_lane_s16(tmp3_s16, consts.val[1], 0); + + tmp0 = vmlsl_lane_s16(tmp0, tmp3_s16, consts.val[0], 0); + tmp1 = vmlsl_lane_s16(tmp1, tmp2_s16, consts.val[0], 2); + tmp2 = vmlsl_lane_s16(tmp2, tmp1_s16, consts.val[0], 2); + tmp3 = vmlsl_lane_s16(tmp3, tmp0_s16, consts.val[0], 0); + + tmp0 = vaddq_s32(tmp0, z3); + tmp1 = vaddq_s32(tmp1, z4); + tmp2 = vaddq_s32(tmp2, z3); + tmp3 = vaddq_s32(tmp3, z4); + + /* Final output stage: descale and narrow to 16-bit. */ + int16x8_t cols_02_s16 = vcombine_s16(vaddhn_s32(tmp10, tmp3), + vaddhn_s32(tmp12, tmp1)); + int16x8_t cols_13_s16 = vcombine_s16(vaddhn_s32(tmp11, tmp2), + vaddhn_s32(tmp13, tmp0)); + int16x8_t cols_46_s16 = vcombine_s16(vsubhn_s32(tmp13, tmp0), + vsubhn_s32(tmp11, tmp2)); + int16x8_t cols_57_s16 = vcombine_s16(vsubhn_s32(tmp12, tmp1), + vsubhn_s32(tmp10, tmp3)); + /* Descale and narrow to 8-bit. */ + int8x8_t cols_02_s8 = vqrshrn_n_s16(cols_02_s16, DESCALE_P2 - 16); + int8x8_t cols_13_s8 = vqrshrn_n_s16(cols_13_s16, DESCALE_P2 - 16); + int8x8_t cols_46_s8 = vqrshrn_n_s16(cols_46_s16, DESCALE_P2 - 16); + int8x8_t cols_57_s8 = vqrshrn_n_s16(cols_57_s16, DESCALE_P2 - 16); + /* Clamp to range [0-255]. */ + uint8x8_t cols_02_u8 = vadd_u8(vreinterpret_u8_s8(cols_02_s8), + vdup_n_u8(CENTERJSAMPLE)); + uint8x8_t cols_13_u8 = vadd_u8(vreinterpret_u8_s8(cols_13_s8), + vdup_n_u8(CENTERJSAMPLE)); + uint8x8_t cols_46_u8 = vadd_u8(vreinterpret_u8_s8(cols_46_s8), + vdup_n_u8(CENTERJSAMPLE)); + uint8x8_t cols_57_u8 = vadd_u8(vreinterpret_u8_s8(cols_57_s8), + vdup_n_u8(CENTERJSAMPLE)); + + /* Transpose 4x8 block and store to memory. (Zipping adjacent columns + * together allows us to store 16-bit elements.) + */ + uint8x8x2_t cols_01_23 = vzip_u8(cols_02_u8, cols_13_u8); + uint8x8x2_t cols_45_67 = vzip_u8(cols_46_u8, cols_57_u8); + uint16x4x4_t cols_01_23_45_67 = { { + vreinterpret_u16_u8(cols_01_23.val[0]), + vreinterpret_u16_u8(cols_01_23.val[1]), + vreinterpret_u16_u8(cols_45_67.val[0]), + vreinterpret_u16_u8(cols_45_67.val[1]) + } }; + + JSAMPROW outptr0 = output_buf[buf_offset + 0] + output_col; + JSAMPROW outptr1 = output_buf[buf_offset + 1] + output_col; + JSAMPROW outptr2 = output_buf[buf_offset + 2] + output_col; + JSAMPROW outptr3 = output_buf[buf_offset + 3] + output_col; + /* VST4 of 16-bit elements completes the transpose. */ + vst4_lane_u16((uint16_t *)outptr0, cols_01_23_45_67, 0); + vst4_lane_u16((uint16_t *)outptr1, cols_01_23_45_67, 1); + vst4_lane_u16((uint16_t *)outptr2, cols_01_23_45_67, 2); + vst4_lane_u16((uint16_t *)outptr3, cols_01_23_45_67, 3); +} + + +/* Performs the second pass of the accurate inverse DCT on a 4x8 block + * of coefficients. + * + * This "sparse" version assumes that the coefficient values (after the first + * pass) in rows 4-7 are all 0. This simplifies the IDCT calculation, + * accelerating overall performance. + */ + +static INLINE void jsimd_idct_islow_pass2_sparse(int16_t *workspace, + JSAMPARRAY output_buf, + JDIMENSION output_col, + unsigned buf_offset) +{ + /* Load constants for IDCT computation. */ +#ifdef HAVE_VLD1_S16_X3 + const int16x4x3_t consts = vld1_s16_x3(jsimd_idct_islow_neon_consts); +#else + const int16x4_t consts1 = vld1_s16(jsimd_idct_islow_neon_consts); + const int16x4_t consts2 = vld1_s16(jsimd_idct_islow_neon_consts + 4); + const int16x4_t consts3 = vld1_s16(jsimd_idct_islow_neon_consts + 8); + const int16x4x3_t consts = { { consts1, consts2, consts3 } }; +#endif + + /* Even part (z3 is all 0) */ + int16x4_t z2_s16 = vld1_s16(workspace + 2 * DCTSIZE / 2); + + int32x4_t tmp2 = vmull_lane_s16(z2_s16, consts.val[0], 1); + int32x4_t tmp3 = vmull_lane_s16(z2_s16, consts.val[1], 2); + + z2_s16 = vld1_s16(workspace + 0 * DCTSIZE / 2); + int32x4_t tmp0 = vshll_n_s16(z2_s16, CONST_BITS); + int32x4_t tmp1 = vshll_n_s16(z2_s16, CONST_BITS); + + int32x4_t tmp10 = vaddq_s32(tmp0, tmp3); + int32x4_t tmp13 = vsubq_s32(tmp0, tmp3); + int32x4_t tmp11 = vaddq_s32(tmp1, tmp2); + int32x4_t tmp12 = vsubq_s32(tmp1, tmp2); + + /* Odd part (tmp0 and tmp1 are both all 0) */ + int16x4_t tmp2_s16 = vld1_s16(workspace + 3 * DCTSIZE / 2); + int16x4_t tmp3_s16 = vld1_s16(workspace + 1 * DCTSIZE / 2); + + int16x4_t z3_s16 = tmp2_s16; + int16x4_t z4_s16 = tmp3_s16; + + int32x4_t z3 = vmull_lane_s16(z3_s16, consts.val[2], 3); + z3 = vmlal_lane_s16(z3, z4_s16, consts.val[1], 3); + int32x4_t z4 = vmull_lane_s16(z3_s16, consts.val[1], 3); + z4 = vmlal_lane_s16(z4, z4_s16, consts.val[2], 0); + + tmp0 = vmlsl_lane_s16(z3, tmp3_s16, consts.val[0], 0); + tmp1 = vmlsl_lane_s16(z4, tmp2_s16, consts.val[0], 2); + tmp2 = vmlal_lane_s16(z3, tmp2_s16, consts.val[2], 2); + tmp3 = vmlal_lane_s16(z4, tmp3_s16, consts.val[1], 0); + + /* Final output stage: descale and narrow to 16-bit. */ + int16x8_t cols_02_s16 = vcombine_s16(vaddhn_s32(tmp10, tmp3), + vaddhn_s32(tmp12, tmp1)); + int16x8_t cols_13_s16 = vcombine_s16(vaddhn_s32(tmp11, tmp2), + vaddhn_s32(tmp13, tmp0)); + int16x8_t cols_46_s16 = vcombine_s16(vsubhn_s32(tmp13, tmp0), + vsubhn_s32(tmp11, tmp2)); + int16x8_t cols_57_s16 = vcombine_s16(vsubhn_s32(tmp12, tmp1), + vsubhn_s32(tmp10, tmp3)); + /* Descale and narrow to 8-bit. */ + int8x8_t cols_02_s8 = vqrshrn_n_s16(cols_02_s16, DESCALE_P2 - 16); + int8x8_t cols_13_s8 = vqrshrn_n_s16(cols_13_s16, DESCALE_P2 - 16); + int8x8_t cols_46_s8 = vqrshrn_n_s16(cols_46_s16, DESCALE_P2 - 16); + int8x8_t cols_57_s8 = vqrshrn_n_s16(cols_57_s16, DESCALE_P2 - 16); + /* Clamp to range [0-255]. */ + uint8x8_t cols_02_u8 = vadd_u8(vreinterpret_u8_s8(cols_02_s8), + vdup_n_u8(CENTERJSAMPLE)); + uint8x8_t cols_13_u8 = vadd_u8(vreinterpret_u8_s8(cols_13_s8), + vdup_n_u8(CENTERJSAMPLE)); + uint8x8_t cols_46_u8 = vadd_u8(vreinterpret_u8_s8(cols_46_s8), + vdup_n_u8(CENTERJSAMPLE)); + uint8x8_t cols_57_u8 = vadd_u8(vreinterpret_u8_s8(cols_57_s8), + vdup_n_u8(CENTERJSAMPLE)); + + /* Transpose 4x8 block and store to memory. (Zipping adjacent columns + * together allows us to store 16-bit elements.) + */ + uint8x8x2_t cols_01_23 = vzip_u8(cols_02_u8, cols_13_u8); + uint8x8x2_t cols_45_67 = vzip_u8(cols_46_u8, cols_57_u8); + uint16x4x4_t cols_01_23_45_67 = { { + vreinterpret_u16_u8(cols_01_23.val[0]), + vreinterpret_u16_u8(cols_01_23.val[1]), + vreinterpret_u16_u8(cols_45_67.val[0]), + vreinterpret_u16_u8(cols_45_67.val[1]) + } }; + + JSAMPROW outptr0 = output_buf[buf_offset + 0] + output_col; + JSAMPROW outptr1 = output_buf[buf_offset + 1] + output_col; + JSAMPROW outptr2 = output_buf[buf_offset + 2] + output_col; + JSAMPROW outptr3 = output_buf[buf_offset + 3] + output_col; + /* VST4 of 16-bit elements completes the transpose. */ + vst4_lane_u16((uint16_t *)outptr0, cols_01_23_45_67, 0); + vst4_lane_u16((uint16_t *)outptr1, cols_01_23_45_67, 1); + vst4_lane_u16((uint16_t *)outptr2, cols_01_23_45_67, 2); + vst4_lane_u16((uint16_t *)outptr3, cols_01_23_45_67, 3); +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jidctred-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jidctred-neon.c new file mode 100644 index 00000000000..be9627e61d4 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jidctred-neon.c @@ -0,0 +1,486 @@ +/* + * jidctred-neon.c - reduced-size IDCT (Arm Neon) + * + * Copyright (C) 2020, Arm Limited. All Rights Reserved. + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" +#include "align.h" +#include "neon-compat.h" + +#include + + +#define CONST_BITS 13 +#define PASS1_BITS 2 + +#define F_0_211 1730 +#define F_0_509 4176 +#define F_0_601 4926 +#define F_0_720 5906 +#define F_0_765 6270 +#define F_0_850 6967 +#define F_0_899 7373 +#define F_1_061 8697 +#define F_1_272 10426 +#define F_1_451 11893 +#define F_1_847 15137 +#define F_2_172 17799 +#define F_2_562 20995 +#define F_3_624 29692 + + +/* jsimd_idct_2x2_neon() is an inverse DCT function that produces reduced-size + * 2x2 output from an 8x8 DCT block. It uses the same calculations and + * produces exactly the same output as IJG's original jpeg_idct_2x2() function + * from jpeg-6b, which can be found in jidctred.c. + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.720959822 = 5906 * 2^-13 + * 0.850430095 = 6967 * 2^-13 + * 1.272758580 = 10426 * 2^-13 + * 3.624509785 = 29692 * 2^-13 + * + * See jidctred.c for further details of the 2x2 IDCT algorithm. Where + * possible, the variable names and comments here in jsimd_idct_2x2_neon() + * match up with those in jpeg_idct_2x2(). + */ + +ALIGN(16) static const int16_t jsimd_idct_2x2_neon_consts[] = { + -F_0_720, F_0_850, -F_1_272, F_3_624 +}; + +void jsimd_idct_2x2_neon(void *dct_table, JCOEFPTR coef_block, + JSAMPARRAY output_buf, JDIMENSION output_col) +{ + ISLOW_MULT_TYPE *quantptr = dct_table; + + /* Load DCT coefficients. */ + int16x8_t row0 = vld1q_s16(coef_block + 0 * DCTSIZE); + int16x8_t row1 = vld1q_s16(coef_block + 1 * DCTSIZE); + int16x8_t row3 = vld1q_s16(coef_block + 3 * DCTSIZE); + int16x8_t row5 = vld1q_s16(coef_block + 5 * DCTSIZE); + int16x8_t row7 = vld1q_s16(coef_block + 7 * DCTSIZE); + + /* Load quantization table values. */ + int16x8_t quant_row0 = vld1q_s16(quantptr + 0 * DCTSIZE); + int16x8_t quant_row1 = vld1q_s16(quantptr + 1 * DCTSIZE); + int16x8_t quant_row3 = vld1q_s16(quantptr + 3 * DCTSIZE); + int16x8_t quant_row5 = vld1q_s16(quantptr + 5 * DCTSIZE); + int16x8_t quant_row7 = vld1q_s16(quantptr + 7 * DCTSIZE); + + /* Dequantize DCT coefficients. */ + row0 = vmulq_s16(row0, quant_row0); + row1 = vmulq_s16(row1, quant_row1); + row3 = vmulq_s16(row3, quant_row3); + row5 = vmulq_s16(row5, quant_row5); + row7 = vmulq_s16(row7, quant_row7); + + /* Load IDCT conversion constants. */ + const int16x4_t consts = vld1_s16(jsimd_idct_2x2_neon_consts); + + /* Pass 1: process columns from input, put results in vectors row0 and + * row1. + */ + + /* Even part */ + int32x4_t tmp10_l = vshll_n_s16(vget_low_s16(row0), CONST_BITS + 2); + int32x4_t tmp10_h = vshll_n_s16(vget_high_s16(row0), CONST_BITS + 2); + + /* Odd part */ + int32x4_t tmp0_l = vmull_lane_s16(vget_low_s16(row1), consts, 3); + tmp0_l = vmlal_lane_s16(tmp0_l, vget_low_s16(row3), consts, 2); + tmp0_l = vmlal_lane_s16(tmp0_l, vget_low_s16(row5), consts, 1); + tmp0_l = vmlal_lane_s16(tmp0_l, vget_low_s16(row7), consts, 0); + int32x4_t tmp0_h = vmull_lane_s16(vget_high_s16(row1), consts, 3); + tmp0_h = vmlal_lane_s16(tmp0_h, vget_high_s16(row3), consts, 2); + tmp0_h = vmlal_lane_s16(tmp0_h, vget_high_s16(row5), consts, 1); + tmp0_h = vmlal_lane_s16(tmp0_h, vget_high_s16(row7), consts, 0); + + /* Final output stage: descale and narrow to 16-bit. */ + row0 = vcombine_s16(vrshrn_n_s32(vaddq_s32(tmp10_l, tmp0_l), CONST_BITS), + vrshrn_n_s32(vaddq_s32(tmp10_h, tmp0_h), CONST_BITS)); + row1 = vcombine_s16(vrshrn_n_s32(vsubq_s32(tmp10_l, tmp0_l), CONST_BITS), + vrshrn_n_s32(vsubq_s32(tmp10_h, tmp0_h), CONST_BITS)); + + /* Transpose two rows, ready for second pass. */ + int16x8x2_t cols_0246_1357 = vtrnq_s16(row0, row1); + int16x8_t cols_0246 = cols_0246_1357.val[0]; + int16x8_t cols_1357 = cols_0246_1357.val[1]; + /* Duplicate columns such that each is accessible in its own vector. */ + int32x4x2_t cols_1155_3377 = vtrnq_s32(vreinterpretq_s32_s16(cols_1357), + vreinterpretq_s32_s16(cols_1357)); + int16x8_t cols_1155 = vreinterpretq_s16_s32(cols_1155_3377.val[0]); + int16x8_t cols_3377 = vreinterpretq_s16_s32(cols_1155_3377.val[1]); + + /* Pass 2: process two rows, store to output array. */ + + /* Even part: we're only interested in col0; the top half of tmp10 is "don't + * care." + */ + int32x4_t tmp10 = vshll_n_s16(vget_low_s16(cols_0246), CONST_BITS + 2); + + /* Odd part: we're only interested in the bottom half of tmp0. */ + int32x4_t tmp0 = vmull_lane_s16(vget_low_s16(cols_1155), consts, 3); + tmp0 = vmlal_lane_s16(tmp0, vget_low_s16(cols_3377), consts, 2); + tmp0 = vmlal_lane_s16(tmp0, vget_high_s16(cols_1155), consts, 1); + tmp0 = vmlal_lane_s16(tmp0, vget_high_s16(cols_3377), consts, 0); + + /* Final output stage: descale and clamp to range [0-255]. */ + int16x8_t output_s16 = vcombine_s16(vaddhn_s32(tmp10, tmp0), + vsubhn_s32(tmp10, tmp0)); + output_s16 = vrsraq_n_s16(vdupq_n_s16(CENTERJSAMPLE), output_s16, + CONST_BITS + PASS1_BITS + 3 + 2 - 16); + /* Narrow to 8-bit and convert to unsigned. */ + uint8x8_t output_u8 = vqmovun_s16(output_s16); + + /* Store 2x2 block to memory. */ + vst1_lane_u8(output_buf[0] + output_col, output_u8, 0); + vst1_lane_u8(output_buf[1] + output_col, output_u8, 1); + vst1_lane_u8(output_buf[0] + output_col + 1, output_u8, 4); + vst1_lane_u8(output_buf[1] + output_col + 1, output_u8, 5); +} + + +/* jsimd_idct_4x4_neon() is an inverse DCT function that produces reduced-size + * 4x4 output from an 8x8 DCT block. It uses the same calculations and + * produces exactly the same output as IJG's original jpeg_idct_4x4() function + * from jpeg-6b, which can be found in jidctred.c. + * + * Scaled integer constants are used to avoid floating-point arithmetic: + * 0.211164243 = 1730 * 2^-13 + * 0.509795579 = 4176 * 2^-13 + * 0.601344887 = 4926 * 2^-13 + * 0.765366865 = 6270 * 2^-13 + * 0.899976223 = 7373 * 2^-13 + * 1.061594337 = 8697 * 2^-13 + * 1.451774981 = 11893 * 2^-13 + * 1.847759065 = 15137 * 2^-13 + * 2.172734803 = 17799 * 2^-13 + * 2.562915447 = 20995 * 2^-13 + * + * See jidctred.c for further details of the 4x4 IDCT algorithm. Where + * possible, the variable names and comments here in jsimd_idct_4x4_neon() + * match up with those in jpeg_idct_4x4(). + */ + +ALIGN(16) static const int16_t jsimd_idct_4x4_neon_consts[] = { + F_1_847, -F_0_765, -F_0_211, F_1_451, + -F_2_172, F_1_061, -F_0_509, -F_0_601, + F_0_899, F_2_562, 0, 0 +}; + +void jsimd_idct_4x4_neon(void *dct_table, JCOEFPTR coef_block, + JSAMPARRAY output_buf, JDIMENSION output_col) +{ + ISLOW_MULT_TYPE *quantptr = dct_table; + + /* Load DCT coefficients. */ + int16x8_t row0 = vld1q_s16(coef_block + 0 * DCTSIZE); + int16x8_t row1 = vld1q_s16(coef_block + 1 * DCTSIZE); + int16x8_t row2 = vld1q_s16(coef_block + 2 * DCTSIZE); + int16x8_t row3 = vld1q_s16(coef_block + 3 * DCTSIZE); + int16x8_t row5 = vld1q_s16(coef_block + 5 * DCTSIZE); + int16x8_t row6 = vld1q_s16(coef_block + 6 * DCTSIZE); + int16x8_t row7 = vld1q_s16(coef_block + 7 * DCTSIZE); + + /* Load quantization table values for DC coefficients. */ + int16x8_t quant_row0 = vld1q_s16(quantptr + 0 * DCTSIZE); + /* Dequantize DC coefficients. */ + row0 = vmulq_s16(row0, quant_row0); + + /* Construct bitmap to test if all AC coefficients are 0. */ + int16x8_t bitmap = vorrq_s16(row1, row2); + bitmap = vorrq_s16(bitmap, row3); + bitmap = vorrq_s16(bitmap, row5); + bitmap = vorrq_s16(bitmap, row6); + bitmap = vorrq_s16(bitmap, row7); + + int64_t left_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 0); + int64_t right_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 1); + + /* Load constants for IDCT computation. */ +#ifdef HAVE_VLD1_S16_X3 + const int16x4x3_t consts = vld1_s16_x3(jsimd_idct_4x4_neon_consts); +#else + /* GCC does not currently support the intrinsic vld1__x3(). */ + const int16x4_t consts1 = vld1_s16(jsimd_idct_4x4_neon_consts); + const int16x4_t consts2 = vld1_s16(jsimd_idct_4x4_neon_consts + 4); + const int16x4_t consts3 = vld1_s16(jsimd_idct_4x4_neon_consts + 8); + const int16x4x3_t consts = { { consts1, consts2, consts3 } }; +#endif + + if (left_ac_bitmap == 0 && right_ac_bitmap == 0) { + /* All AC coefficients are zero. + * Compute DC values and duplicate into row vectors 0, 1, 2, and 3. + */ + int16x8_t dcval = vshlq_n_s16(row0, PASS1_BITS); + row0 = dcval; + row1 = dcval; + row2 = dcval; + row3 = dcval; + } else if (left_ac_bitmap == 0) { + /* AC coefficients are zero for columns 0, 1, 2, and 3. + * Compute DC values for these columns. + */ + int16x4_t dcval = vshl_n_s16(vget_low_s16(row0), PASS1_BITS); + + /* Commence regular IDCT computation for columns 4, 5, 6, and 7. */ + + /* Load quantization table. */ + int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE + 4); + int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE + 4); + int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE + 4); + int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE + 4); + int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE + 4); + int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE + 4); + + /* Even part */ + int32x4_t tmp0 = vshll_n_s16(vget_high_s16(row0), CONST_BITS + 1); + + int16x4_t z2 = vmul_s16(vget_high_s16(row2), quant_row2); + int16x4_t z3 = vmul_s16(vget_high_s16(row6), quant_row6); + + int32x4_t tmp2 = vmull_lane_s16(z2, consts.val[0], 0); + tmp2 = vmlal_lane_s16(tmp2, z3, consts.val[0], 1); + + int32x4_t tmp10 = vaddq_s32(tmp0, tmp2); + int32x4_t tmp12 = vsubq_s32(tmp0, tmp2); + + /* Odd part */ + int16x4_t z1 = vmul_s16(vget_high_s16(row7), quant_row7); + z2 = vmul_s16(vget_high_s16(row5), quant_row5); + z3 = vmul_s16(vget_high_s16(row3), quant_row3); + int16x4_t z4 = vmul_s16(vget_high_s16(row1), quant_row1); + + tmp0 = vmull_lane_s16(z1, consts.val[0], 2); + tmp0 = vmlal_lane_s16(tmp0, z2, consts.val[0], 3); + tmp0 = vmlal_lane_s16(tmp0, z3, consts.val[1], 0); + tmp0 = vmlal_lane_s16(tmp0, z4, consts.val[1], 1); + + tmp2 = vmull_lane_s16(z1, consts.val[1], 2); + tmp2 = vmlal_lane_s16(tmp2, z2, consts.val[1], 3); + tmp2 = vmlal_lane_s16(tmp2, z3, consts.val[2], 0); + tmp2 = vmlal_lane_s16(tmp2, z4, consts.val[2], 1); + + /* Final output stage: descale and narrow to 16-bit. */ + row0 = vcombine_s16(dcval, vrshrn_n_s32(vaddq_s32(tmp10, tmp2), + CONST_BITS - PASS1_BITS + 1)); + row3 = vcombine_s16(dcval, vrshrn_n_s32(vsubq_s32(tmp10, tmp2), + CONST_BITS - PASS1_BITS + 1)); + row1 = vcombine_s16(dcval, vrshrn_n_s32(vaddq_s32(tmp12, tmp0), + CONST_BITS - PASS1_BITS + 1)); + row2 = vcombine_s16(dcval, vrshrn_n_s32(vsubq_s32(tmp12, tmp0), + CONST_BITS - PASS1_BITS + 1)); + } else if (right_ac_bitmap == 0) { + /* AC coefficients are zero for columns 4, 5, 6, and 7. + * Compute DC values for these columns. + */ + int16x4_t dcval = vshl_n_s16(vget_high_s16(row0), PASS1_BITS); + + /* Commence regular IDCT computation for columns 0, 1, 2, and 3. */ + + /* Load quantization table. */ + int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE); + int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE); + int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE); + int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE); + int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE); + int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE); + + /* Even part */ + int32x4_t tmp0 = vshll_n_s16(vget_low_s16(row0), CONST_BITS + 1); + + int16x4_t z2 = vmul_s16(vget_low_s16(row2), quant_row2); + int16x4_t z3 = vmul_s16(vget_low_s16(row6), quant_row6); + + int32x4_t tmp2 = vmull_lane_s16(z2, consts.val[0], 0); + tmp2 = vmlal_lane_s16(tmp2, z3, consts.val[0], 1); + + int32x4_t tmp10 = vaddq_s32(tmp0, tmp2); + int32x4_t tmp12 = vsubq_s32(tmp0, tmp2); + + /* Odd part */ + int16x4_t z1 = vmul_s16(vget_low_s16(row7), quant_row7); + z2 = vmul_s16(vget_low_s16(row5), quant_row5); + z3 = vmul_s16(vget_low_s16(row3), quant_row3); + int16x4_t z4 = vmul_s16(vget_low_s16(row1), quant_row1); + + tmp0 = vmull_lane_s16(z1, consts.val[0], 2); + tmp0 = vmlal_lane_s16(tmp0, z2, consts.val[0], 3); + tmp0 = vmlal_lane_s16(tmp0, z3, consts.val[1], 0); + tmp0 = vmlal_lane_s16(tmp0, z4, consts.val[1], 1); + + tmp2 = vmull_lane_s16(z1, consts.val[1], 2); + tmp2 = vmlal_lane_s16(tmp2, z2, consts.val[1], 3); + tmp2 = vmlal_lane_s16(tmp2, z3, consts.val[2], 0); + tmp2 = vmlal_lane_s16(tmp2, z4, consts.val[2], 1); + + /* Final output stage: descale and narrow to 16-bit. */ + row0 = vcombine_s16(vrshrn_n_s32(vaddq_s32(tmp10, tmp2), + CONST_BITS - PASS1_BITS + 1), dcval); + row3 = vcombine_s16(vrshrn_n_s32(vsubq_s32(tmp10, tmp2), + CONST_BITS - PASS1_BITS + 1), dcval); + row1 = vcombine_s16(vrshrn_n_s32(vaddq_s32(tmp12, tmp0), + CONST_BITS - PASS1_BITS + 1), dcval); + row2 = vcombine_s16(vrshrn_n_s32(vsubq_s32(tmp12, tmp0), + CONST_BITS - PASS1_BITS + 1), dcval); + } else { + /* All AC coefficients are non-zero; full IDCT calculation required. */ + int16x8_t quant_row1 = vld1q_s16(quantptr + 1 * DCTSIZE); + int16x8_t quant_row2 = vld1q_s16(quantptr + 2 * DCTSIZE); + int16x8_t quant_row3 = vld1q_s16(quantptr + 3 * DCTSIZE); + int16x8_t quant_row5 = vld1q_s16(quantptr + 5 * DCTSIZE); + int16x8_t quant_row6 = vld1q_s16(quantptr + 6 * DCTSIZE); + int16x8_t quant_row7 = vld1q_s16(quantptr + 7 * DCTSIZE); + + /* Even part */ + int32x4_t tmp0_l = vshll_n_s16(vget_low_s16(row0), CONST_BITS + 1); + int32x4_t tmp0_h = vshll_n_s16(vget_high_s16(row0), CONST_BITS + 1); + + int16x8_t z2 = vmulq_s16(row2, quant_row2); + int16x8_t z3 = vmulq_s16(row6, quant_row6); + + int32x4_t tmp2_l = vmull_lane_s16(vget_low_s16(z2), consts.val[0], 0); + int32x4_t tmp2_h = vmull_lane_s16(vget_high_s16(z2), consts.val[0], 0); + tmp2_l = vmlal_lane_s16(tmp2_l, vget_low_s16(z3), consts.val[0], 1); + tmp2_h = vmlal_lane_s16(tmp2_h, vget_high_s16(z3), consts.val[0], 1); + + int32x4_t tmp10_l = vaddq_s32(tmp0_l, tmp2_l); + int32x4_t tmp10_h = vaddq_s32(tmp0_h, tmp2_h); + int32x4_t tmp12_l = vsubq_s32(tmp0_l, tmp2_l); + int32x4_t tmp12_h = vsubq_s32(tmp0_h, tmp2_h); + + /* Odd part */ + int16x8_t z1 = vmulq_s16(row7, quant_row7); + z2 = vmulq_s16(row5, quant_row5); + z3 = vmulq_s16(row3, quant_row3); + int16x8_t z4 = vmulq_s16(row1, quant_row1); + + tmp0_l = vmull_lane_s16(vget_low_s16(z1), consts.val[0], 2); + tmp0_l = vmlal_lane_s16(tmp0_l, vget_low_s16(z2), consts.val[0], 3); + tmp0_l = vmlal_lane_s16(tmp0_l, vget_low_s16(z3), consts.val[1], 0); + tmp0_l = vmlal_lane_s16(tmp0_l, vget_low_s16(z4), consts.val[1], 1); + tmp0_h = vmull_lane_s16(vget_high_s16(z1), consts.val[0], 2); + tmp0_h = vmlal_lane_s16(tmp0_h, vget_high_s16(z2), consts.val[0], 3); + tmp0_h = vmlal_lane_s16(tmp0_h, vget_high_s16(z3), consts.val[1], 0); + tmp0_h = vmlal_lane_s16(tmp0_h, vget_high_s16(z4), consts.val[1], 1); + + tmp2_l = vmull_lane_s16(vget_low_s16(z1), consts.val[1], 2); + tmp2_l = vmlal_lane_s16(tmp2_l, vget_low_s16(z2), consts.val[1], 3); + tmp2_l = vmlal_lane_s16(tmp2_l, vget_low_s16(z3), consts.val[2], 0); + tmp2_l = vmlal_lane_s16(tmp2_l, vget_low_s16(z4), consts.val[2], 1); + tmp2_h = vmull_lane_s16(vget_high_s16(z1), consts.val[1], 2); + tmp2_h = vmlal_lane_s16(tmp2_h, vget_high_s16(z2), consts.val[1], 3); + tmp2_h = vmlal_lane_s16(tmp2_h, vget_high_s16(z3), consts.val[2], 0); + tmp2_h = vmlal_lane_s16(tmp2_h, vget_high_s16(z4), consts.val[2], 1); + + /* Final output stage: descale and narrow to 16-bit. */ + row0 = vcombine_s16(vrshrn_n_s32(vaddq_s32(tmp10_l, tmp2_l), + CONST_BITS - PASS1_BITS + 1), + vrshrn_n_s32(vaddq_s32(tmp10_h, tmp2_h), + CONST_BITS - PASS1_BITS + 1)); + row3 = vcombine_s16(vrshrn_n_s32(vsubq_s32(tmp10_l, tmp2_l), + CONST_BITS - PASS1_BITS + 1), + vrshrn_n_s32(vsubq_s32(tmp10_h, tmp2_h), + CONST_BITS - PASS1_BITS + 1)); + row1 = vcombine_s16(vrshrn_n_s32(vaddq_s32(tmp12_l, tmp0_l), + CONST_BITS - PASS1_BITS + 1), + vrshrn_n_s32(vaddq_s32(tmp12_h, tmp0_h), + CONST_BITS - PASS1_BITS + 1)); + row2 = vcombine_s16(vrshrn_n_s32(vsubq_s32(tmp12_l, tmp0_l), + CONST_BITS - PASS1_BITS + 1), + vrshrn_n_s32(vsubq_s32(tmp12_h, tmp0_h), + CONST_BITS - PASS1_BITS + 1)); + } + + /* Transpose 8x4 block to perform IDCT on rows in second pass. */ + int16x8x2_t row_01 = vtrnq_s16(row0, row1); + int16x8x2_t row_23 = vtrnq_s16(row2, row3); + + int32x4x2_t cols_0426 = vtrnq_s32(vreinterpretq_s32_s16(row_01.val[0]), + vreinterpretq_s32_s16(row_23.val[0])); + int32x4x2_t cols_1537 = vtrnq_s32(vreinterpretq_s32_s16(row_01.val[1]), + vreinterpretq_s32_s16(row_23.val[1])); + + int16x4_t col0 = vreinterpret_s16_s32(vget_low_s32(cols_0426.val[0])); + int16x4_t col1 = vreinterpret_s16_s32(vget_low_s32(cols_1537.val[0])); + int16x4_t col2 = vreinterpret_s16_s32(vget_low_s32(cols_0426.val[1])); + int16x4_t col3 = vreinterpret_s16_s32(vget_low_s32(cols_1537.val[1])); + int16x4_t col5 = vreinterpret_s16_s32(vget_high_s32(cols_1537.val[0])); + int16x4_t col6 = vreinterpret_s16_s32(vget_high_s32(cols_0426.val[1])); + int16x4_t col7 = vreinterpret_s16_s32(vget_high_s32(cols_1537.val[1])); + + /* Commence second pass of IDCT. */ + + /* Even part */ + int32x4_t tmp0 = vshll_n_s16(col0, CONST_BITS + 1); + int32x4_t tmp2 = vmull_lane_s16(col2, consts.val[0], 0); + tmp2 = vmlal_lane_s16(tmp2, col6, consts.val[0], 1); + + int32x4_t tmp10 = vaddq_s32(tmp0, tmp2); + int32x4_t tmp12 = vsubq_s32(tmp0, tmp2); + + /* Odd part */ + tmp0 = vmull_lane_s16(col7, consts.val[0], 2); + tmp0 = vmlal_lane_s16(tmp0, col5, consts.val[0], 3); + tmp0 = vmlal_lane_s16(tmp0, col3, consts.val[1], 0); + tmp0 = vmlal_lane_s16(tmp0, col1, consts.val[1], 1); + + tmp2 = vmull_lane_s16(col7, consts.val[1], 2); + tmp2 = vmlal_lane_s16(tmp2, col5, consts.val[1], 3); + tmp2 = vmlal_lane_s16(tmp2, col3, consts.val[2], 0); + tmp2 = vmlal_lane_s16(tmp2, col1, consts.val[2], 1); + + /* Final output stage: descale and clamp to range [0-255]. */ + int16x8_t output_cols_02 = vcombine_s16(vaddhn_s32(tmp10, tmp2), + vsubhn_s32(tmp12, tmp0)); + int16x8_t output_cols_13 = vcombine_s16(vaddhn_s32(tmp12, tmp0), + vsubhn_s32(tmp10, tmp2)); + output_cols_02 = vrsraq_n_s16(vdupq_n_s16(CENTERJSAMPLE), output_cols_02, + CONST_BITS + PASS1_BITS + 3 + 1 - 16); + output_cols_13 = vrsraq_n_s16(vdupq_n_s16(CENTERJSAMPLE), output_cols_13, + CONST_BITS + PASS1_BITS + 3 + 1 - 16); + /* Narrow to 8-bit and convert to unsigned while zipping 8-bit elements. + * An interleaving store completes the transpose. + */ + uint8x8x2_t output_0123 = vzip_u8(vqmovun_s16(output_cols_02), + vqmovun_s16(output_cols_13)); + uint16x4x2_t output_01_23 = { { + vreinterpret_u16_u8(output_0123.val[0]), + vreinterpret_u16_u8(output_0123.val[1]) + } }; + + /* Store 4x4 block to memory. */ + JSAMPROW outptr0 = output_buf[0] + output_col; + JSAMPROW outptr1 = output_buf[1] + output_col; + JSAMPROW outptr2 = output_buf[2] + output_col; + JSAMPROW outptr3 = output_buf[3] + output_col; + vst2_lane_u16((uint16_t *)outptr0, output_01_23, 0); + vst2_lane_u16((uint16_t *)outptr1, output_01_23, 1); + vst2_lane_u16((uint16_t *)outptr2, output_01_23, 2); + vst2_lane_u16((uint16_t *)outptr3, output_01_23, 3); +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jquanti-neon.c b/third-party/mozjpeg/mozjpeg/simd/arm/jquanti-neon.c new file mode 100644 index 00000000000..d5d95d89f67 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/jquanti-neon.c @@ -0,0 +1,193 @@ +/* + * jquanti-neon.c - sample data conversion and quantization (Arm Neon) + * + * Copyright (C) 2020-2021, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#define JPEG_INTERNALS +#include "../../jinclude.h" +#include "../../jpeglib.h" +#include "../../jsimd.h" +#include "../../jdct.h" +#include "../../jsimddct.h" +#include "../jsimd.h" + +#include + + +/* After downsampling, the resulting sample values are in the range [0, 255], + * but the Discrete Cosine Transform (DCT) operates on values centered around + * 0. + * + * To prepare sample values for the DCT, load samples into a DCT workspace, + * subtracting CENTERJSAMPLE (128). The samples, now in the range [-128, 127], + * are also widened from 8- to 16-bit. + * + * The equivalent scalar C function convsamp() can be found in jcdctmgr.c. + */ + +void jsimd_convsamp_neon(JSAMPARRAY sample_data, JDIMENSION start_col, + DCTELEM *workspace) +{ + uint8x8_t samp_row0 = vld1_u8(sample_data[0] + start_col); + uint8x8_t samp_row1 = vld1_u8(sample_data[1] + start_col); + uint8x8_t samp_row2 = vld1_u8(sample_data[2] + start_col); + uint8x8_t samp_row3 = vld1_u8(sample_data[3] + start_col); + uint8x8_t samp_row4 = vld1_u8(sample_data[4] + start_col); + uint8x8_t samp_row5 = vld1_u8(sample_data[5] + start_col); + uint8x8_t samp_row6 = vld1_u8(sample_data[6] + start_col); + uint8x8_t samp_row7 = vld1_u8(sample_data[7] + start_col); + + int16x8_t row0 = + vreinterpretq_s16_u16(vsubl_u8(samp_row0, vdup_n_u8(CENTERJSAMPLE))); + int16x8_t row1 = + vreinterpretq_s16_u16(vsubl_u8(samp_row1, vdup_n_u8(CENTERJSAMPLE))); + int16x8_t row2 = + vreinterpretq_s16_u16(vsubl_u8(samp_row2, vdup_n_u8(CENTERJSAMPLE))); + int16x8_t row3 = + vreinterpretq_s16_u16(vsubl_u8(samp_row3, vdup_n_u8(CENTERJSAMPLE))); + int16x8_t row4 = + vreinterpretq_s16_u16(vsubl_u8(samp_row4, vdup_n_u8(CENTERJSAMPLE))); + int16x8_t row5 = + vreinterpretq_s16_u16(vsubl_u8(samp_row5, vdup_n_u8(CENTERJSAMPLE))); + int16x8_t row6 = + vreinterpretq_s16_u16(vsubl_u8(samp_row6, vdup_n_u8(CENTERJSAMPLE))); + int16x8_t row7 = + vreinterpretq_s16_u16(vsubl_u8(samp_row7, vdup_n_u8(CENTERJSAMPLE))); + + vst1q_s16(workspace + 0 * DCTSIZE, row0); + vst1q_s16(workspace + 1 * DCTSIZE, row1); + vst1q_s16(workspace + 2 * DCTSIZE, row2); + vst1q_s16(workspace + 3 * DCTSIZE, row3); + vst1q_s16(workspace + 4 * DCTSIZE, row4); + vst1q_s16(workspace + 5 * DCTSIZE, row5); + vst1q_s16(workspace + 6 * DCTSIZE, row6); + vst1q_s16(workspace + 7 * DCTSIZE, row7); +} + + +/* After the DCT, the resulting array of coefficient values needs to be divided + * by an array of quantization values. + * + * To avoid a slow division operation, the DCT coefficients are multiplied by + * the (scaled) reciprocals of the quantization values and then right-shifted. + * + * The equivalent scalar C function quantize() can be found in jcdctmgr.c. + */ + +void jsimd_quantize_neon(JCOEFPTR coef_block, DCTELEM *divisors, + DCTELEM *workspace) +{ + JCOEFPTR out_ptr = coef_block; + UDCTELEM *recip_ptr = (UDCTELEM *)divisors; + UDCTELEM *corr_ptr = (UDCTELEM *)divisors + DCTSIZE2; + DCTELEM *shift_ptr = divisors + 3 * DCTSIZE2; + int i; + +#if defined(__clang__) && (defined(__aarch64__) || defined(_M_ARM64)) +#pragma unroll +#endif + for (i = 0; i < DCTSIZE; i += DCTSIZE / 2) { + /* Load DCT coefficients. */ + int16x8_t row0 = vld1q_s16(workspace + (i + 0) * DCTSIZE); + int16x8_t row1 = vld1q_s16(workspace + (i + 1) * DCTSIZE); + int16x8_t row2 = vld1q_s16(workspace + (i + 2) * DCTSIZE); + int16x8_t row3 = vld1q_s16(workspace + (i + 3) * DCTSIZE); + /* Load reciprocals of quantization values. */ + uint16x8_t recip0 = vld1q_u16(recip_ptr + (i + 0) * DCTSIZE); + uint16x8_t recip1 = vld1q_u16(recip_ptr + (i + 1) * DCTSIZE); + uint16x8_t recip2 = vld1q_u16(recip_ptr + (i + 2) * DCTSIZE); + uint16x8_t recip3 = vld1q_u16(recip_ptr + (i + 3) * DCTSIZE); + uint16x8_t corr0 = vld1q_u16(corr_ptr + (i + 0) * DCTSIZE); + uint16x8_t corr1 = vld1q_u16(corr_ptr + (i + 1) * DCTSIZE); + uint16x8_t corr2 = vld1q_u16(corr_ptr + (i + 2) * DCTSIZE); + uint16x8_t corr3 = vld1q_u16(corr_ptr + (i + 3) * DCTSIZE); + int16x8_t shift0 = vld1q_s16(shift_ptr + (i + 0) * DCTSIZE); + int16x8_t shift1 = vld1q_s16(shift_ptr + (i + 1) * DCTSIZE); + int16x8_t shift2 = vld1q_s16(shift_ptr + (i + 2) * DCTSIZE); + int16x8_t shift3 = vld1q_s16(shift_ptr + (i + 3) * DCTSIZE); + + /* Extract sign from coefficients. */ + int16x8_t sign_row0 = vshrq_n_s16(row0, 15); + int16x8_t sign_row1 = vshrq_n_s16(row1, 15); + int16x8_t sign_row2 = vshrq_n_s16(row2, 15); + int16x8_t sign_row3 = vshrq_n_s16(row3, 15); + /* Get absolute value of DCT coefficients. */ + uint16x8_t abs_row0 = vreinterpretq_u16_s16(vabsq_s16(row0)); + uint16x8_t abs_row1 = vreinterpretq_u16_s16(vabsq_s16(row1)); + uint16x8_t abs_row2 = vreinterpretq_u16_s16(vabsq_s16(row2)); + uint16x8_t abs_row3 = vreinterpretq_u16_s16(vabsq_s16(row3)); + /* Add correction. */ + abs_row0 = vaddq_u16(abs_row0, corr0); + abs_row1 = vaddq_u16(abs_row1, corr1); + abs_row2 = vaddq_u16(abs_row2, corr2); + abs_row3 = vaddq_u16(abs_row3, corr3); + + /* Multiply DCT coefficients by quantization reciprocals. */ + int32x4_t row0_l = vreinterpretq_s32_u32(vmull_u16(vget_low_u16(abs_row0), + vget_low_u16(recip0))); + int32x4_t row0_h = vreinterpretq_s32_u32(vmull_u16(vget_high_u16(abs_row0), + vget_high_u16(recip0))); + int32x4_t row1_l = vreinterpretq_s32_u32(vmull_u16(vget_low_u16(abs_row1), + vget_low_u16(recip1))); + int32x4_t row1_h = vreinterpretq_s32_u32(vmull_u16(vget_high_u16(abs_row1), + vget_high_u16(recip1))); + int32x4_t row2_l = vreinterpretq_s32_u32(vmull_u16(vget_low_u16(abs_row2), + vget_low_u16(recip2))); + int32x4_t row2_h = vreinterpretq_s32_u32(vmull_u16(vget_high_u16(abs_row2), + vget_high_u16(recip2))); + int32x4_t row3_l = vreinterpretq_s32_u32(vmull_u16(vget_low_u16(abs_row3), + vget_low_u16(recip3))); + int32x4_t row3_h = vreinterpretq_s32_u32(vmull_u16(vget_high_u16(abs_row3), + vget_high_u16(recip3))); + /* Narrow back to 16-bit. */ + row0 = vcombine_s16(vshrn_n_s32(row0_l, 16), vshrn_n_s32(row0_h, 16)); + row1 = vcombine_s16(vshrn_n_s32(row1_l, 16), vshrn_n_s32(row1_h, 16)); + row2 = vcombine_s16(vshrn_n_s32(row2_l, 16), vshrn_n_s32(row2_h, 16)); + row3 = vcombine_s16(vshrn_n_s32(row3_l, 16), vshrn_n_s32(row3_h, 16)); + + /* Since VSHR only supports an immediate as its second argument, negate the + * shift value and shift left. + */ + row0 = vreinterpretq_s16_u16(vshlq_u16(vreinterpretq_u16_s16(row0), + vnegq_s16(shift0))); + row1 = vreinterpretq_s16_u16(vshlq_u16(vreinterpretq_u16_s16(row1), + vnegq_s16(shift1))); + row2 = vreinterpretq_s16_u16(vshlq_u16(vreinterpretq_u16_s16(row2), + vnegq_s16(shift2))); + row3 = vreinterpretq_s16_u16(vshlq_u16(vreinterpretq_u16_s16(row3), + vnegq_s16(shift3))); + + /* Restore sign to original product. */ + row0 = veorq_s16(row0, sign_row0); + row0 = vsubq_s16(row0, sign_row0); + row1 = veorq_s16(row1, sign_row1); + row1 = vsubq_s16(row1, sign_row1); + row2 = veorq_s16(row2, sign_row2); + row2 = vsubq_s16(row2, sign_row2); + row3 = veorq_s16(row3, sign_row3); + row3 = vsubq_s16(row3, sign_row3); + + /* Store quantized coefficients to memory. */ + vst1q_s16(out_ptr + (i + 0) * DCTSIZE, row0); + vst1q_s16(out_ptr + (i + 1) * DCTSIZE, row1); + vst1q_s16(out_ptr + (i + 2) * DCTSIZE, row2); + vst1q_s16(out_ptr + (i + 3) * DCTSIZE, row3); + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/jsimd_neon.S b/third-party/mozjpeg/mozjpeg/simd/arm/jsimd_neon.S deleted file mode 100644 index af929fe6d33..00000000000 --- a/third-party/mozjpeg/mozjpeg/simd/arm/jsimd_neon.S +++ /dev/null @@ -1,2878 +0,0 @@ -/* - * ARMv7 NEON optimizations for libjpeg-turbo - * - * Copyright (C) 2009-2011, Nokia Corporation and/or its subsidiary(-ies). - * All Rights Reserved. - * Author: Siarhei Siamashka - * Copyright (C) 2014, Siarhei Siamashka. All Rights Reserved. - * Copyright (C) 2014, Linaro Limited. All Rights Reserved. - * Copyright (C) 2015, D. R. Commander. All Rights Reserved. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. All Rights Reserved. - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ - -#if defined(__linux__) && defined(__ELF__) -.section .note.GNU-stack, "", %progbits /* mark stack as non-executable */ -#endif - -.text -.fpu neon -.arch armv7a -.object_arch armv4 -.arm -.syntax unified - - -#define RESPECT_STRICT_ALIGNMENT 1 - - -/*****************************************************************************/ - -/* Supplementary macro for setting function attributes */ -.macro asm_function fname -#ifdef __APPLE__ - .private_extern _\fname - .globl _\fname -_\fname: -#else - .global \fname -#ifdef __ELF__ - .hidden \fname - .type \fname, %function -#endif -\fname: -#endif -.endm - -/* Transpose a block of 4x4 coefficients in four 64-bit registers */ -.macro transpose_4x4 x0, x1, x2, x3 - vtrn.16 \x0, \x1 - vtrn.16 \x2, \x3 - vtrn.32 \x0, \x2 - vtrn.32 \x1, \x3 -.endm - - -#define CENTERJSAMPLE 128 - -/*****************************************************************************/ - -/* - * Perform dequantization and inverse DCT on one block of coefficients. - * - * GLOBAL(void) - * jsimd_idct_islow_neon(void *dct_table, JCOEFPTR coef_block, - * JSAMPARRAY output_buf, JDIMENSION output_col) - */ - -#define FIX_0_298631336 (2446) -#define FIX_0_390180644 (3196) -#define FIX_0_541196100 (4433) -#define FIX_0_765366865 (6270) -#define FIX_0_899976223 (7373) -#define FIX_1_175875602 (9633) -#define FIX_1_501321110 (12299) -#define FIX_1_847759065 (15137) -#define FIX_1_961570560 (16069) -#define FIX_2_053119869 (16819) -#define FIX_2_562915447 (20995) -#define FIX_3_072711026 (25172) - -#define FIX_1_175875602_MINUS_1_961570560 (FIX_1_175875602 - FIX_1_961570560) -#define FIX_1_175875602_MINUS_0_390180644 (FIX_1_175875602 - FIX_0_390180644) -#define FIX_0_541196100_MINUS_1_847759065 (FIX_0_541196100 - FIX_1_847759065) -#define FIX_3_072711026_MINUS_2_562915447 (FIX_3_072711026 - FIX_2_562915447) -#define FIX_0_298631336_MINUS_0_899976223 (FIX_0_298631336 - FIX_0_899976223) -#define FIX_1_501321110_MINUS_0_899976223 (FIX_1_501321110 - FIX_0_899976223) -#define FIX_2_053119869_MINUS_2_562915447 (FIX_2_053119869 - FIX_2_562915447) -#define FIX_0_541196100_PLUS_0_765366865 (FIX_0_541196100 + FIX_0_765366865) - -/* - * Reference SIMD-friendly 1-D ISLOW iDCT C implementation. - * Uses some ideas from the comments in 'simd/jiss2int-64.asm' - */ -#define REF_1D_IDCT(xrow0, xrow1, xrow2, xrow3, xrow4, xrow5, xrow6, xrow7) { \ - DCTELEM row0, row1, row2, row3, row4, row5, row6, row7; \ - JLONG q1, q2, q3, q4, q5, q6, q7; \ - JLONG tmp11_plus_tmp2, tmp11_minus_tmp2; \ - \ - /* 1-D iDCT input data */ \ - row0 = xrow0; \ - row1 = xrow1; \ - row2 = xrow2; \ - row3 = xrow3; \ - row4 = xrow4; \ - row5 = xrow5; \ - row6 = xrow6; \ - row7 = xrow7; \ - \ - q5 = row7 + row3; \ - q4 = row5 + row1; \ - q6 = MULTIPLY(q5, FIX_1_175875602_MINUS_1_961570560) + \ - MULTIPLY(q4, FIX_1_175875602); \ - q7 = MULTIPLY(q5, FIX_1_175875602) + \ - MULTIPLY(q4, FIX_1_175875602_MINUS_0_390180644); \ - q2 = MULTIPLY(row2, FIX_0_541196100) + \ - MULTIPLY(row6, FIX_0_541196100_MINUS_1_847759065); \ - q4 = q6; \ - q3 = ((JLONG)row0 - (JLONG)row4) << 13; \ - q6 += MULTIPLY(row5, -FIX_2_562915447) + \ - MULTIPLY(row3, FIX_3_072711026_MINUS_2_562915447); \ - /* now we can use q1 (reloadable constants have been used up) */ \ - q1 = q3 + q2; \ - q4 += MULTIPLY(row7, FIX_0_298631336_MINUS_0_899976223) + \ - MULTIPLY(row1, -FIX_0_899976223); \ - q5 = q7; \ - q1 = q1 + q6; \ - q7 += MULTIPLY(row7, -FIX_0_899976223) + \ - MULTIPLY(row1, FIX_1_501321110_MINUS_0_899976223); \ - \ - /* (tmp11 + tmp2) has been calculated (out_row1 before descale) */ \ - tmp11_plus_tmp2 = q1; \ - row1 = 0; \ - \ - q1 = q1 - q6; \ - q5 += MULTIPLY(row5, FIX_2_053119869_MINUS_2_562915447) + \ - MULTIPLY(row3, -FIX_2_562915447); \ - q1 = q1 - q6; \ - q6 = MULTIPLY(row2, FIX_0_541196100_PLUS_0_765366865) + \ - MULTIPLY(row6, FIX_0_541196100); \ - q3 = q3 - q2; \ - \ - /* (tmp11 - tmp2) has been calculated (out_row6 before descale) */ \ - tmp11_minus_tmp2 = q1; \ - \ - q1 = ((JLONG)row0 + (JLONG)row4) << 13; \ - q2 = q1 + q6; \ - q1 = q1 - q6; \ - \ - /* pick up the results */ \ - tmp0 = q4; \ - tmp1 = q5; \ - tmp2 = (tmp11_plus_tmp2 - tmp11_minus_tmp2) / 2; \ - tmp3 = q7; \ - tmp10 = q2; \ - tmp11 = (tmp11_plus_tmp2 + tmp11_minus_tmp2) / 2; \ - tmp12 = q3; \ - tmp13 = q1; \ -} - -#define XFIX_0_899976223 d0[0] -#define XFIX_0_541196100 d0[1] -#define XFIX_2_562915447 d0[2] -#define XFIX_0_298631336_MINUS_0_899976223 d0[3] -#define XFIX_1_501321110_MINUS_0_899976223 d1[0] -#define XFIX_2_053119869_MINUS_2_562915447 d1[1] -#define XFIX_0_541196100_PLUS_0_765366865 d1[2] -#define XFIX_1_175875602 d1[3] -#define XFIX_1_175875602_MINUS_0_390180644 d2[0] -#define XFIX_0_541196100_MINUS_1_847759065 d2[1] -#define XFIX_3_072711026_MINUS_2_562915447 d2[2] -#define XFIX_1_175875602_MINUS_1_961570560 d2[3] - -.balign 16 -jsimd_idct_islow_neon_consts: - .short FIX_0_899976223 /* d0[0] */ - .short FIX_0_541196100 /* d0[1] */ - .short FIX_2_562915447 /* d0[2] */ - .short FIX_0_298631336_MINUS_0_899976223 /* d0[3] */ - .short FIX_1_501321110_MINUS_0_899976223 /* d1[0] */ - .short FIX_2_053119869_MINUS_2_562915447 /* d1[1] */ - .short FIX_0_541196100_PLUS_0_765366865 /* d1[2] */ - .short FIX_1_175875602 /* d1[3] */ - /* reloadable constants */ - .short FIX_1_175875602_MINUS_0_390180644 /* d2[0] */ - .short FIX_0_541196100_MINUS_1_847759065 /* d2[1] */ - .short FIX_3_072711026_MINUS_2_562915447 /* d2[2] */ - .short FIX_1_175875602_MINUS_1_961570560 /* d2[3] */ - -asm_function jsimd_idct_islow_neon - - DCT_TABLE .req r0 - COEF_BLOCK .req r1 - OUTPUT_BUF .req r2 - OUTPUT_COL .req r3 - TMP1 .req r0 - TMP2 .req r1 - TMP3 .req r2 - TMP4 .req ip - - ROW0L .req d16 - ROW0R .req d17 - ROW1L .req d18 - ROW1R .req d19 - ROW2L .req d20 - ROW2R .req d21 - ROW3L .req d22 - ROW3R .req d23 - ROW4L .req d24 - ROW4R .req d25 - ROW5L .req d26 - ROW5R .req d27 - ROW6L .req d28 - ROW6R .req d29 - ROW7L .req d30 - ROW7R .req d31 - - /* Load and dequantize coefficients into NEON registers - * with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | d16 | d17 ( q8 ) - * 1 | d18 | d19 ( q9 ) - * 2 | d20 | d21 ( q10 ) - * 3 | d22 | d23 ( q11 ) - * 4 | d24 | d25 ( q12 ) - * 5 | d26 | d27 ( q13 ) - * 6 | d28 | d29 ( q14 ) - * 7 | d30 | d31 ( q15 ) - */ - adr ip, jsimd_idct_islow_neon_consts - vld1.16 {d16, d17, d18, d19}, [COEF_BLOCK, :128]! - vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! - vld1.16 {d20, d21, d22, d23}, [COEF_BLOCK, :128]! - vmul.s16 q8, q8, q0 - vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! - vmul.s16 q9, q9, q1 - vld1.16 {d24, d25, d26, d27}, [COEF_BLOCK, :128]! - vmul.s16 q10, q10, q2 - vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! - vmul.s16 q11, q11, q3 - vld1.16 {d28, d29, d30, d31}, [COEF_BLOCK, :128] - vmul.s16 q12, q12, q0 - vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! - vmul.s16 q14, q14, q2 - vmul.s16 q13, q13, q1 - vld1.16 {d0, d1, d2, d3}, [ip, :128] /* load constants */ - add ip, ip, #16 - vmul.s16 q15, q15, q3 - vpush {d8-d15} /* save NEON registers */ - /* 1-D IDCT, pass 1, left 4x8 half */ - vadd.s16 d4, ROW7L, ROW3L - vadd.s16 d5, ROW5L, ROW1L - vmull.s16 q6, d4, XFIX_1_175875602_MINUS_1_961570560 - vmlal.s16 q6, d5, XFIX_1_175875602 - vmull.s16 q7, d4, XFIX_1_175875602 - /* Check for the zero coefficients in the right 4x8 half */ - push {r4, r5} - vmlal.s16 q7, d5, XFIX_1_175875602_MINUS_0_390180644 - vsubl.s16 q3, ROW0L, ROW4L - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 1 * 8))] - vmull.s16 q2, ROW2L, XFIX_0_541196100 - vmlal.s16 q2, ROW6L, XFIX_0_541196100_MINUS_1_847759065 - orr r0, r4, r5 - vmov q4, q6 - vmlsl.s16 q6, ROW5L, XFIX_2_562915447 - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 2 * 8))] - vmlal.s16 q6, ROW3L, XFIX_3_072711026_MINUS_2_562915447 - vshl.s32 q3, q3, #13 - orr r0, r0, r4 - vmlsl.s16 q4, ROW1L, XFIX_0_899976223 - orr r0, r0, r5 - vadd.s32 q1, q3, q2 - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 3 * 8))] - vmov q5, q7 - vadd.s32 q1, q1, q6 - orr r0, r0, r4 - vmlsl.s16 q7, ROW7L, XFIX_0_899976223 - orr r0, r0, r5 - vmlal.s16 q7, ROW1L, XFIX_1_501321110_MINUS_0_899976223 - vrshrn.s32 ROW1L, q1, #11 - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 4 * 8))] - vsub.s32 q1, q1, q6 - vmlal.s16 q5, ROW5L, XFIX_2_053119869_MINUS_2_562915447 - orr r0, r0, r4 - vmlsl.s16 q5, ROW3L, XFIX_2_562915447 - orr r0, r0, r5 - vsub.s32 q1, q1, q6 - vmull.s16 q6, ROW2L, XFIX_0_541196100_PLUS_0_765366865 - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 5 * 8))] - vmlal.s16 q6, ROW6L, XFIX_0_541196100 - vsub.s32 q3, q3, q2 - orr r0, r0, r4 - vrshrn.s32 ROW6L, q1, #11 - orr r0, r0, r5 - vadd.s32 q1, q3, q5 - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 6 * 8))] - vsub.s32 q3, q3, q5 - vaddl.s16 q5, ROW0L, ROW4L - orr r0, r0, r4 - vrshrn.s32 ROW2L, q1, #11 - orr r0, r0, r5 - vrshrn.s32 ROW5L, q3, #11 - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 7 * 8))] - vshl.s32 q5, q5, #13 - vmlal.s16 q4, ROW7L, XFIX_0_298631336_MINUS_0_899976223 - orr r0, r0, r4 - vadd.s32 q2, q5, q6 - orrs r0, r0, r5 - vsub.s32 q1, q5, q6 - vadd.s32 q6, q2, q7 - ldrd r4, [COEF_BLOCK, #(-96 + 2 * (4 + 0 * 8))] - vsub.s32 q2, q2, q7 - vadd.s32 q5, q1, q4 - orr r0, r4, r5 - vsub.s32 q3, q1, q4 - pop {r4, r5} - vrshrn.s32 ROW7L, q2, #11 - vrshrn.s32 ROW3L, q5, #11 - vrshrn.s32 ROW0L, q6, #11 - vrshrn.s32 ROW4L, q3, #11 - - beq 3f /* Go to do some special handling for the sparse - right 4x8 half */ - - /* 1-D IDCT, pass 1, right 4x8 half */ - vld1.s16 {d2}, [ip, :64] /* reload constants */ - vadd.s16 d10, ROW7R, ROW3R - vadd.s16 d8, ROW5R, ROW1R - /* Transpose left 4x8 half */ - vtrn.16 ROW6L, ROW7L - vmull.s16 q6, d10, XFIX_1_175875602_MINUS_1_961570560 - vmlal.s16 q6, d8, XFIX_1_175875602 - vtrn.16 ROW2L, ROW3L - vmull.s16 q7, d10, XFIX_1_175875602 - vmlal.s16 q7, d8, XFIX_1_175875602_MINUS_0_390180644 - vtrn.16 ROW0L, ROW1L - vsubl.s16 q3, ROW0R, ROW4R - vmull.s16 q2, ROW2R, XFIX_0_541196100 - vmlal.s16 q2, ROW6R, XFIX_0_541196100_MINUS_1_847759065 - vtrn.16 ROW4L, ROW5L - vmov q4, q6 - vmlsl.s16 q6, ROW5R, XFIX_2_562915447 - vmlal.s16 q6, ROW3R, XFIX_3_072711026_MINUS_2_562915447 - vtrn.32 ROW1L, ROW3L - vshl.s32 q3, q3, #13 - vmlsl.s16 q4, ROW1R, XFIX_0_899976223 - vtrn.32 ROW4L, ROW6L - vadd.s32 q1, q3, q2 - vmov q5, q7 - vadd.s32 q1, q1, q6 - vtrn.32 ROW0L, ROW2L - vmlsl.s16 q7, ROW7R, XFIX_0_899976223 - vmlal.s16 q7, ROW1R, XFIX_1_501321110_MINUS_0_899976223 - vrshrn.s32 ROW1R, q1, #11 - vtrn.32 ROW5L, ROW7L - vsub.s32 q1, q1, q6 - vmlal.s16 q5, ROW5R, XFIX_2_053119869_MINUS_2_562915447 - vmlsl.s16 q5, ROW3R, XFIX_2_562915447 - vsub.s32 q1, q1, q6 - vmull.s16 q6, ROW2R, XFIX_0_541196100_PLUS_0_765366865 - vmlal.s16 q6, ROW6R, XFIX_0_541196100 - vsub.s32 q3, q3, q2 - vrshrn.s32 ROW6R, q1, #11 - vadd.s32 q1, q3, q5 - vsub.s32 q3, q3, q5 - vaddl.s16 q5, ROW0R, ROW4R - vrshrn.s32 ROW2R, q1, #11 - vrshrn.s32 ROW5R, q3, #11 - vshl.s32 q5, q5, #13 - vmlal.s16 q4, ROW7R, XFIX_0_298631336_MINUS_0_899976223 - vadd.s32 q2, q5, q6 - vsub.s32 q1, q5, q6 - vadd.s32 q6, q2, q7 - vsub.s32 q2, q2, q7 - vadd.s32 q5, q1, q4 - vsub.s32 q3, q1, q4 - vrshrn.s32 ROW7R, q2, #11 - vrshrn.s32 ROW3R, q5, #11 - vrshrn.s32 ROW0R, q6, #11 - vrshrn.s32 ROW4R, q3, #11 - /* Transpose right 4x8 half */ - vtrn.16 ROW6R, ROW7R - vtrn.16 ROW2R, ROW3R - vtrn.16 ROW0R, ROW1R - vtrn.16 ROW4R, ROW5R - vtrn.32 ROW1R, ROW3R - vtrn.32 ROW4R, ROW6R - vtrn.32 ROW0R, ROW2R - vtrn.32 ROW5R, ROW7R - -1: /* 1-D IDCT, pass 2 (normal variant), left 4x8 half */ - vld1.s16 {d2}, [ip, :64] /* reload constants */ - vmull.s16 q6, ROW1R, XFIX_1_175875602 /* ROW5L <-> ROW1R */ - vmlal.s16 q6, ROW1L, XFIX_1_175875602 - vmlal.s16 q6, ROW3R, XFIX_1_175875602_MINUS_1_961570560 /* ROW7L <-> ROW3R */ - vmlal.s16 q6, ROW3L, XFIX_1_175875602_MINUS_1_961570560 - vmull.s16 q7, ROW3R, XFIX_1_175875602 /* ROW7L <-> ROW3R */ - vmlal.s16 q7, ROW3L, XFIX_1_175875602 - vmlal.s16 q7, ROW1R, XFIX_1_175875602_MINUS_0_390180644 /* ROW5L <-> ROW1R */ - vmlal.s16 q7, ROW1L, XFIX_1_175875602_MINUS_0_390180644 - vsubl.s16 q3, ROW0L, ROW0R /* ROW4L <-> ROW0R */ - vmull.s16 q2, ROW2L, XFIX_0_541196100 - vmlal.s16 q2, ROW2R, XFIX_0_541196100_MINUS_1_847759065 /* ROW6L <-> ROW2R */ - vmov q4, q6 - vmlsl.s16 q6, ROW1R, XFIX_2_562915447 /* ROW5L <-> ROW1R */ - vmlal.s16 q6, ROW3L, XFIX_3_072711026_MINUS_2_562915447 - vshl.s32 q3, q3, #13 - vmlsl.s16 q4, ROW1L, XFIX_0_899976223 - vadd.s32 q1, q3, q2 - vmov q5, q7 - vadd.s32 q1, q1, q6 - vmlsl.s16 q7, ROW3R, XFIX_0_899976223 /* ROW7L <-> ROW3R */ - vmlal.s16 q7, ROW1L, XFIX_1_501321110_MINUS_0_899976223 - vshrn.s32 ROW1L, q1, #16 - vsub.s32 q1, q1, q6 - vmlal.s16 q5, ROW1R, XFIX_2_053119869_MINUS_2_562915447 /* ROW5L <-> ROW1R */ - vmlsl.s16 q5, ROW3L, XFIX_2_562915447 - vsub.s32 q1, q1, q6 - vmull.s16 q6, ROW2L, XFIX_0_541196100_PLUS_0_765366865 - vmlal.s16 q6, ROW2R, XFIX_0_541196100 /* ROW6L <-> ROW2R */ - vsub.s32 q3, q3, q2 - vshrn.s32 ROW2R, q1, #16 /* ROW6L <-> ROW2R */ - vadd.s32 q1, q3, q5 - vsub.s32 q3, q3, q5 - vaddl.s16 q5, ROW0L, ROW0R /* ROW4L <-> ROW0R */ - vshrn.s32 ROW2L, q1, #16 - vshrn.s32 ROW1R, q3, #16 /* ROW5L <-> ROW1R */ - vshl.s32 q5, q5, #13 - vmlal.s16 q4, ROW3R, XFIX_0_298631336_MINUS_0_899976223 /* ROW7L <-> ROW3R */ - vadd.s32 q2, q5, q6 - vsub.s32 q1, q5, q6 - vadd.s32 q6, q2, q7 - vsub.s32 q2, q2, q7 - vadd.s32 q5, q1, q4 - vsub.s32 q3, q1, q4 - vshrn.s32 ROW3R, q2, #16 /* ROW7L <-> ROW3R */ - vshrn.s32 ROW3L, q5, #16 - vshrn.s32 ROW0L, q6, #16 - vshrn.s32 ROW0R, q3, #16 /* ROW4L <-> ROW0R */ - /* 1-D IDCT, pass 2, right 4x8 half */ - vld1.s16 {d2}, [ip, :64] /* reload constants */ - vmull.s16 q6, ROW5R, XFIX_1_175875602 - vmlal.s16 q6, ROW5L, XFIX_1_175875602 /* ROW5L <-> ROW1R */ - vmlal.s16 q6, ROW7R, XFIX_1_175875602_MINUS_1_961570560 - vmlal.s16 q6, ROW7L, XFIX_1_175875602_MINUS_1_961570560 /* ROW7L <-> ROW3R */ - vmull.s16 q7, ROW7R, XFIX_1_175875602 - vmlal.s16 q7, ROW7L, XFIX_1_175875602 /* ROW7L <-> ROW3R */ - vmlal.s16 q7, ROW5R, XFIX_1_175875602_MINUS_0_390180644 - vmlal.s16 q7, ROW5L, XFIX_1_175875602_MINUS_0_390180644 /* ROW5L <-> ROW1R */ - vsubl.s16 q3, ROW4L, ROW4R /* ROW4L <-> ROW0R */ - vmull.s16 q2, ROW6L, XFIX_0_541196100 /* ROW6L <-> ROW2R */ - vmlal.s16 q2, ROW6R, XFIX_0_541196100_MINUS_1_847759065 - vmov q4, q6 - vmlsl.s16 q6, ROW5R, XFIX_2_562915447 - vmlal.s16 q6, ROW7L, XFIX_3_072711026_MINUS_2_562915447 /* ROW7L <-> ROW3R */ - vshl.s32 q3, q3, #13 - vmlsl.s16 q4, ROW5L, XFIX_0_899976223 /* ROW5L <-> ROW1R */ - vadd.s32 q1, q3, q2 - vmov q5, q7 - vadd.s32 q1, q1, q6 - vmlsl.s16 q7, ROW7R, XFIX_0_899976223 - vmlal.s16 q7, ROW5L, XFIX_1_501321110_MINUS_0_899976223 /* ROW5L <-> ROW1R */ - vshrn.s32 ROW5L, q1, #16 /* ROW5L <-> ROW1R */ - vsub.s32 q1, q1, q6 - vmlal.s16 q5, ROW5R, XFIX_2_053119869_MINUS_2_562915447 - vmlsl.s16 q5, ROW7L, XFIX_2_562915447 /* ROW7L <-> ROW3R */ - vsub.s32 q1, q1, q6 - vmull.s16 q6, ROW6L, XFIX_0_541196100_PLUS_0_765366865 /* ROW6L <-> ROW2R */ - vmlal.s16 q6, ROW6R, XFIX_0_541196100 - vsub.s32 q3, q3, q2 - vshrn.s32 ROW6R, q1, #16 - vadd.s32 q1, q3, q5 - vsub.s32 q3, q3, q5 - vaddl.s16 q5, ROW4L, ROW4R /* ROW4L <-> ROW0R */ - vshrn.s32 ROW6L, q1, #16 /* ROW6L <-> ROW2R */ - vshrn.s32 ROW5R, q3, #16 - vshl.s32 q5, q5, #13 - vmlal.s16 q4, ROW7R, XFIX_0_298631336_MINUS_0_899976223 - vadd.s32 q2, q5, q6 - vsub.s32 q1, q5, q6 - vadd.s32 q6, q2, q7 - vsub.s32 q2, q2, q7 - vadd.s32 q5, q1, q4 - vsub.s32 q3, q1, q4 - vshrn.s32 ROW7R, q2, #16 - vshrn.s32 ROW7L, q5, #16 /* ROW7L <-> ROW3R */ - vshrn.s32 ROW4L, q6, #16 /* ROW4L <-> ROW0R */ - vshrn.s32 ROW4R, q3, #16 - -2: /* Descale to 8-bit and range limit */ - vqrshrn.s16 d16, q8, #2 - vqrshrn.s16 d17, q9, #2 - vqrshrn.s16 d18, q10, #2 - vqrshrn.s16 d19, q11, #2 - vpop {d8-d15} /* restore NEON registers */ - vqrshrn.s16 d20, q12, #2 - /* Transpose the final 8-bit samples and do signed->unsigned conversion */ - vtrn.16 q8, q9 - vqrshrn.s16 d21, q13, #2 - vqrshrn.s16 d22, q14, #2 - vmov.u8 q0, #(CENTERJSAMPLE) - vqrshrn.s16 d23, q15, #2 - vtrn.8 d16, d17 - vtrn.8 d18, d19 - vadd.u8 q8, q8, q0 - vadd.u8 q9, q9, q0 - vtrn.16 q10, q11 - /* Store results to the output buffer */ - ldmia OUTPUT_BUF!, {TMP1, TMP2} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - vst1.8 {d16}, [TMP1] - vtrn.8 d20, d21 - vst1.8 {d17}, [TMP2] - ldmia OUTPUT_BUF!, {TMP1, TMP2} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - vst1.8 {d18}, [TMP1] - vadd.u8 q10, q10, q0 - vst1.8 {d19}, [TMP2] - ldmia OUTPUT_BUF, {TMP1, TMP2, TMP3, TMP4} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - add TMP3, TMP3, OUTPUT_COL - add TMP4, TMP4, OUTPUT_COL - vtrn.8 d22, d23 - vst1.8 {d20}, [TMP1] - vadd.u8 q11, q11, q0 - vst1.8 {d21}, [TMP2] - vst1.8 {d22}, [TMP3] - vst1.8 {d23}, [TMP4] - bx lr - -3: /* Left 4x8 half is done, right 4x8 half contains mostly zeros */ - - /* Transpose left 4x8 half */ - vtrn.16 ROW6L, ROW7L - vtrn.16 ROW2L, ROW3L - vtrn.16 ROW0L, ROW1L - vtrn.16 ROW4L, ROW5L - vshl.s16 ROW0R, ROW0R, #2 /* PASS1_BITS */ - vtrn.32 ROW1L, ROW3L - vtrn.32 ROW4L, ROW6L - vtrn.32 ROW0L, ROW2L - vtrn.32 ROW5L, ROW7L - - cmp r0, #0 - beq 4f /* Right 4x8 half has all zeros, go to 'sparse' second - pass */ - - /* Only row 0 is non-zero for the right 4x8 half */ - vdup.s16 ROW1R, ROW0R[1] - vdup.s16 ROW2R, ROW0R[2] - vdup.s16 ROW3R, ROW0R[3] - vdup.s16 ROW4R, ROW0R[0] - vdup.s16 ROW5R, ROW0R[1] - vdup.s16 ROW6R, ROW0R[2] - vdup.s16 ROW7R, ROW0R[3] - vdup.s16 ROW0R, ROW0R[0] - b 1b /* Go to 'normal' second pass */ - -4: /* 1-D IDCT, pass 2 (sparse variant with zero rows 4-7), left 4x8 half */ - vld1.s16 {d2}, [ip, :64] /* reload constants */ - vmull.s16 q6, ROW1L, XFIX_1_175875602 - vmlal.s16 q6, ROW3L, XFIX_1_175875602_MINUS_1_961570560 - vmull.s16 q7, ROW3L, XFIX_1_175875602 - vmlal.s16 q7, ROW1L, XFIX_1_175875602_MINUS_0_390180644 - vmull.s16 q2, ROW2L, XFIX_0_541196100 - vshll.s16 q3, ROW0L, #13 - vmov q4, q6 - vmlal.s16 q6, ROW3L, XFIX_3_072711026_MINUS_2_562915447 - vmlsl.s16 q4, ROW1L, XFIX_0_899976223 - vadd.s32 q1, q3, q2 - vmov q5, q7 - vmlal.s16 q7, ROW1L, XFIX_1_501321110_MINUS_0_899976223 - vadd.s32 q1, q1, q6 - vadd.s32 q6, q6, q6 - vmlsl.s16 q5, ROW3L, XFIX_2_562915447 - vshrn.s32 ROW1L, q1, #16 - vsub.s32 q1, q1, q6 - vmull.s16 q6, ROW2L, XFIX_0_541196100_PLUS_0_765366865 - vsub.s32 q3, q3, q2 - vshrn.s32 ROW2R, q1, #16 /* ROW6L <-> ROW2R */ - vadd.s32 q1, q3, q5 - vsub.s32 q3, q3, q5 - vshll.s16 q5, ROW0L, #13 - vshrn.s32 ROW2L, q1, #16 - vshrn.s32 ROW1R, q3, #16 /* ROW5L <-> ROW1R */ - vadd.s32 q2, q5, q6 - vsub.s32 q1, q5, q6 - vadd.s32 q6, q2, q7 - vsub.s32 q2, q2, q7 - vadd.s32 q5, q1, q4 - vsub.s32 q3, q1, q4 - vshrn.s32 ROW3R, q2, #16 /* ROW7L <-> ROW3R */ - vshrn.s32 ROW3L, q5, #16 - vshrn.s32 ROW0L, q6, #16 - vshrn.s32 ROW0R, q3, #16 /* ROW4L <-> ROW0R */ - /* 1-D IDCT, pass 2 (sparse variant with zero rows 4-7), right 4x8 half */ - vld1.s16 {d2}, [ip, :64] /* reload constants */ - vmull.s16 q6, ROW5L, XFIX_1_175875602 - vmlal.s16 q6, ROW7L, XFIX_1_175875602_MINUS_1_961570560 - vmull.s16 q7, ROW7L, XFIX_1_175875602 - vmlal.s16 q7, ROW5L, XFIX_1_175875602_MINUS_0_390180644 - vmull.s16 q2, ROW6L, XFIX_0_541196100 - vshll.s16 q3, ROW4L, #13 - vmov q4, q6 - vmlal.s16 q6, ROW7L, XFIX_3_072711026_MINUS_2_562915447 - vmlsl.s16 q4, ROW5L, XFIX_0_899976223 - vadd.s32 q1, q3, q2 - vmov q5, q7 - vmlal.s16 q7, ROW5L, XFIX_1_501321110_MINUS_0_899976223 - vadd.s32 q1, q1, q6 - vadd.s32 q6, q6, q6 - vmlsl.s16 q5, ROW7L, XFIX_2_562915447 - vshrn.s32 ROW5L, q1, #16 /* ROW5L <-> ROW1R */ - vsub.s32 q1, q1, q6 - vmull.s16 q6, ROW6L, XFIX_0_541196100_PLUS_0_765366865 - vsub.s32 q3, q3, q2 - vshrn.s32 ROW6R, q1, #16 - vadd.s32 q1, q3, q5 - vsub.s32 q3, q3, q5 - vshll.s16 q5, ROW4L, #13 - vshrn.s32 ROW6L, q1, #16 /* ROW6L <-> ROW2R */ - vshrn.s32 ROW5R, q3, #16 - vadd.s32 q2, q5, q6 - vsub.s32 q1, q5, q6 - vadd.s32 q6, q2, q7 - vsub.s32 q2, q2, q7 - vadd.s32 q5, q1, q4 - vsub.s32 q3, q1, q4 - vshrn.s32 ROW7R, q2, #16 - vshrn.s32 ROW7L, q5, #16 /* ROW7L <-> ROW3R */ - vshrn.s32 ROW4L, q6, #16 /* ROW4L <-> ROW0R */ - vshrn.s32 ROW4R, q3, #16 - b 2b /* Go to epilogue */ - - .unreq DCT_TABLE - .unreq COEF_BLOCK - .unreq OUTPUT_BUF - .unreq OUTPUT_COL - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMP4 - - .unreq ROW0L - .unreq ROW0R - .unreq ROW1L - .unreq ROW1R - .unreq ROW2L - .unreq ROW2R - .unreq ROW3L - .unreq ROW3R - .unreq ROW4L - .unreq ROW4R - .unreq ROW5L - .unreq ROW5R - .unreq ROW6L - .unreq ROW6R - .unreq ROW7L - .unreq ROW7R - - -/*****************************************************************************/ - -/* - * jsimd_idct_ifast_neon - * - * This function contains a fast, not so accurate integer implementation of - * the inverse DCT (Discrete Cosine Transform). It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_idct_ifast' - * function from jidctfst.c - * - * Normally 1-D AAN DCT needs 5 multiplications and 29 additions. - * But in ARM NEON case some extra additions are required because VQDMULH - * instruction can't handle the constants larger than 1. So the expressions - * like "x * 1.082392200" have to be converted to "x * 0.082392200 + x", - * which introduces an extra addition. Overall, there are 6 extra additions - * per 1-D IDCT pass, totalling to 5 VQDMULH and 35 VADD/VSUB instructions. - */ - -#define XFIX_1_082392200 d0[0] -#define XFIX_1_414213562 d0[1] -#define XFIX_1_847759065 d0[2] -#define XFIX_2_613125930 d0[3] - -.balign 16 -jsimd_idct_ifast_neon_consts: - .short (277 * 128 - 256 * 128) /* XFIX_1_082392200 */ - .short (362 * 128 - 256 * 128) /* XFIX_1_414213562 */ - .short (473 * 128 - 256 * 128) /* XFIX_1_847759065 */ - .short (669 * 128 - 512 * 128) /* XFIX_2_613125930 */ - -asm_function jsimd_idct_ifast_neon - - DCT_TABLE .req r0 - COEF_BLOCK .req r1 - OUTPUT_BUF .req r2 - OUTPUT_COL .req r3 - TMP1 .req r0 - TMP2 .req r1 - TMP3 .req r2 - TMP4 .req ip - - /* Load and dequantize coefficients into NEON registers - * with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | d16 | d17 ( q8 ) - * 1 | d18 | d19 ( q9 ) - * 2 | d20 | d21 ( q10 ) - * 3 | d22 | d23 ( q11 ) - * 4 | d24 | d25 ( q12 ) - * 5 | d26 | d27 ( q13 ) - * 6 | d28 | d29 ( q14 ) - * 7 | d30 | d31 ( q15 ) - */ - adr ip, jsimd_idct_ifast_neon_consts - vld1.16 {d16, d17, d18, d19}, [COEF_BLOCK, :128]! - vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! - vld1.16 {d20, d21, d22, d23}, [COEF_BLOCK, :128]! - vmul.s16 q8, q8, q0 - vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! - vmul.s16 q9, q9, q1 - vld1.16 {d24, d25, d26, d27}, [COEF_BLOCK, :128]! - vmul.s16 q10, q10, q2 - vld1.16 {d0, d1, d2, d3}, [DCT_TABLE, :128]! - vmul.s16 q11, q11, q3 - vld1.16 {d28, d29, d30, d31}, [COEF_BLOCK, :128] - vmul.s16 q12, q12, q0 - vld1.16 {d4, d5, d6, d7}, [DCT_TABLE, :128]! - vmul.s16 q14, q14, q2 - vmul.s16 q13, q13, q1 - vld1.16 {d0}, [ip, :64] /* load constants */ - vmul.s16 q15, q15, q3 - vpush {d8-d13} /* save NEON registers */ - /* 1-D IDCT, pass 1 */ - vsub.s16 q2, q10, q14 - vadd.s16 q14, q10, q14 - vsub.s16 q1, q11, q13 - vadd.s16 q13, q11, q13 - vsub.s16 q5, q9, q15 - vadd.s16 q15, q9, q15 - vqdmulh.s16 q4, q2, XFIX_1_414213562 - vqdmulh.s16 q6, q1, XFIX_2_613125930 - vadd.s16 q3, q1, q1 - vsub.s16 q1, q5, q1 - vadd.s16 q10, q2, q4 - vqdmulh.s16 q4, q1, XFIX_1_847759065 - vsub.s16 q2, q15, q13 - vadd.s16 q3, q3, q6 - vqdmulh.s16 q6, q2, XFIX_1_414213562 - vadd.s16 q1, q1, q4 - vqdmulh.s16 q4, q5, XFIX_1_082392200 - vsub.s16 q10, q10, q14 - vadd.s16 q2, q2, q6 - vsub.s16 q6, q8, q12 - vadd.s16 q12, q8, q12 - vadd.s16 q9, q5, q4 - vadd.s16 q5, q6, q10 - vsub.s16 q10, q6, q10 - vadd.s16 q6, q15, q13 - vadd.s16 q8, q12, q14 - vsub.s16 q3, q6, q3 - vsub.s16 q12, q12, q14 - vsub.s16 q3, q3, q1 - vsub.s16 q1, q9, q1 - vadd.s16 q2, q3, q2 - vsub.s16 q15, q8, q6 - vadd.s16 q1, q1, q2 - vadd.s16 q8, q8, q6 - vadd.s16 q14, q5, q3 - vsub.s16 q9, q5, q3 - vsub.s16 q13, q10, q2 - vadd.s16 q10, q10, q2 - /* Transpose */ - vtrn.16 q8, q9 - vsub.s16 q11, q12, q1 - vtrn.16 q14, q15 - vadd.s16 q12, q12, q1 - vtrn.16 q10, q11 - vtrn.16 q12, q13 - vtrn.32 q9, q11 - vtrn.32 q12, q14 - vtrn.32 q8, q10 - vtrn.32 q13, q15 - vswp d28, d21 - vswp d26, d19 - /* 1-D IDCT, pass 2 */ - vsub.s16 q2, q10, q14 - vswp d30, d23 - vadd.s16 q14, q10, q14 - vswp d24, d17 - vsub.s16 q1, q11, q13 - vadd.s16 q13, q11, q13 - vsub.s16 q5, q9, q15 - vadd.s16 q15, q9, q15 - vqdmulh.s16 q4, q2, XFIX_1_414213562 - vqdmulh.s16 q6, q1, XFIX_2_613125930 - vadd.s16 q3, q1, q1 - vsub.s16 q1, q5, q1 - vadd.s16 q10, q2, q4 - vqdmulh.s16 q4, q1, XFIX_1_847759065 - vsub.s16 q2, q15, q13 - vadd.s16 q3, q3, q6 - vqdmulh.s16 q6, q2, XFIX_1_414213562 - vadd.s16 q1, q1, q4 - vqdmulh.s16 q4, q5, XFIX_1_082392200 - vsub.s16 q10, q10, q14 - vadd.s16 q2, q2, q6 - vsub.s16 q6, q8, q12 - vadd.s16 q12, q8, q12 - vadd.s16 q9, q5, q4 - vadd.s16 q5, q6, q10 - vsub.s16 q10, q6, q10 - vadd.s16 q6, q15, q13 - vadd.s16 q8, q12, q14 - vsub.s16 q3, q6, q3 - vsub.s16 q12, q12, q14 - vsub.s16 q3, q3, q1 - vsub.s16 q1, q9, q1 - vadd.s16 q2, q3, q2 - vsub.s16 q15, q8, q6 - vadd.s16 q1, q1, q2 - vadd.s16 q8, q8, q6 - vadd.s16 q14, q5, q3 - vsub.s16 q9, q5, q3 - vsub.s16 q13, q10, q2 - vpop {d8-d13} /* restore NEON registers */ - vadd.s16 q10, q10, q2 - vsub.s16 q11, q12, q1 - vadd.s16 q12, q12, q1 - /* Descale to 8-bit and range limit */ - vmov.u8 q0, #0x80 - vqshrn.s16 d16, q8, #5 - vqshrn.s16 d17, q9, #5 - vqshrn.s16 d18, q10, #5 - vqshrn.s16 d19, q11, #5 - vqshrn.s16 d20, q12, #5 - vqshrn.s16 d21, q13, #5 - vqshrn.s16 d22, q14, #5 - vqshrn.s16 d23, q15, #5 - vadd.u8 q8, q8, q0 - vadd.u8 q9, q9, q0 - vadd.u8 q10, q10, q0 - vadd.u8 q11, q11, q0 - /* Transpose the final 8-bit samples */ - vtrn.16 q8, q9 - vtrn.16 q10, q11 - vtrn.32 q8, q10 - vtrn.32 q9, q11 - vtrn.8 d16, d17 - vtrn.8 d18, d19 - /* Store results to the output buffer */ - ldmia OUTPUT_BUF!, {TMP1, TMP2} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - vst1.8 {d16}, [TMP1] - vst1.8 {d17}, [TMP2] - ldmia OUTPUT_BUF!, {TMP1, TMP2} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - vst1.8 {d18}, [TMP1] - vtrn.8 d20, d21 - vst1.8 {d19}, [TMP2] - ldmia OUTPUT_BUF, {TMP1, TMP2, TMP3, TMP4} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - add TMP3, TMP3, OUTPUT_COL - add TMP4, TMP4, OUTPUT_COL - vst1.8 {d20}, [TMP1] - vtrn.8 d22, d23 - vst1.8 {d21}, [TMP2] - vst1.8 {d22}, [TMP3] - vst1.8 {d23}, [TMP4] - bx lr - - .unreq DCT_TABLE - .unreq COEF_BLOCK - .unreq OUTPUT_BUF - .unreq OUTPUT_COL - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMP4 - - -/*****************************************************************************/ - -/* - * jsimd_idct_4x4_neon - * - * This function contains inverse-DCT code for getting reduced-size - * 4x4 pixels output from an 8x8 DCT block. It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_idct_4x4' - * function from jpeg-6b (jidctred.c). - * - * NOTE: jpeg-8 has an improved implementation of 4x4 inverse-DCT, which - * requires much less arithmetic operations and hence should be faster. - * The primary purpose of this particular NEON optimized function is - * bit exact compatibility with jpeg-6b. - * - * TODO: a bit better instructions scheduling can be achieved by expanding - * idct_helper/transpose_4x4 macros and reordering instructions, - * but readability will suffer somewhat. - */ - -#define CONST_BITS 13 - -#define FIX_0_211164243 (1730) /* FIX(0.211164243) */ -#define FIX_0_509795579 (4176) /* FIX(0.509795579) */ -#define FIX_0_601344887 (4926) /* FIX(0.601344887) */ -#define FIX_0_720959822 (5906) /* FIX(0.720959822) */ -#define FIX_0_765366865 (6270) /* FIX(0.765366865) */ -#define FIX_0_850430095 (6967) /* FIX(0.850430095) */ -#define FIX_0_899976223 (7373) /* FIX(0.899976223) */ -#define FIX_1_061594337 (8697) /* FIX(1.061594337) */ -#define FIX_1_272758580 (10426) /* FIX(1.272758580) */ -#define FIX_1_451774981 (11893) /* FIX(1.451774981) */ -#define FIX_1_847759065 (15137) /* FIX(1.847759065) */ -#define FIX_2_172734803 (17799) /* FIX(2.172734803) */ -#define FIX_2_562915447 (20995) /* FIX(2.562915447) */ -#define FIX_3_624509785 (29692) /* FIX(3.624509785) */ - -.balign 16 -jsimd_idct_4x4_neon_consts: - .short FIX_1_847759065 /* d0[0] */ - .short -FIX_0_765366865 /* d0[1] */ - .short -FIX_0_211164243 /* d0[2] */ - .short FIX_1_451774981 /* d0[3] */ - .short -FIX_2_172734803 /* d1[0] */ - .short FIX_1_061594337 /* d1[1] */ - .short -FIX_0_509795579 /* d1[2] */ - .short -FIX_0_601344887 /* d1[3] */ - .short FIX_0_899976223 /* d2[0] */ - .short FIX_2_562915447 /* d2[1] */ - .short 1 << (CONST_BITS + 1) /* d2[2] */ - .short 0 /* d2[3] */ - -.macro idct_helper x4, x6, x8, x10, x12, x14, x16, shift, y26, y27, y28, y29 - vmull.s16 q14, \x4, d2[2] - vmlal.s16 q14, \x8, d0[0] - vmlal.s16 q14, \x14, d0[1] - - vmull.s16 q13, \x16, d1[2] - vmlal.s16 q13, \x12, d1[3] - vmlal.s16 q13, \x10, d2[0] - vmlal.s16 q13, \x6, d2[1] - - vmull.s16 q15, \x4, d2[2] - vmlsl.s16 q15, \x8, d0[0] - vmlsl.s16 q15, \x14, d0[1] - - vmull.s16 q12, \x16, d0[2] - vmlal.s16 q12, \x12, d0[3] - vmlal.s16 q12, \x10, d1[0] - vmlal.s16 q12, \x6, d1[1] - - vadd.s32 q10, q14, q13 - vsub.s32 q14, q14, q13 - - .if \shift > 16 - vrshr.s32 q10, q10, #\shift - vrshr.s32 q14, q14, #\shift - vmovn.s32 \y26, q10 - vmovn.s32 \y29, q14 - .else - vrshrn.s32 \y26, q10, #\shift - vrshrn.s32 \y29, q14, #\shift - .endif - - vadd.s32 q10, q15, q12 - vsub.s32 q15, q15, q12 - - .if \shift > 16 - vrshr.s32 q10, q10, #\shift - vrshr.s32 q15, q15, #\shift - vmovn.s32 \y27, q10 - vmovn.s32 \y28, q15 - .else - vrshrn.s32 \y27, q10, #\shift - vrshrn.s32 \y28, q15, #\shift - .endif -.endm - -asm_function jsimd_idct_4x4_neon - - DCT_TABLE .req r0 - COEF_BLOCK .req r1 - OUTPUT_BUF .req r2 - OUTPUT_COL .req r3 - TMP1 .req r0 - TMP2 .req r1 - TMP3 .req r2 - TMP4 .req ip - - vpush {d8-d15} - - /* Load constants (d3 is just used for padding) */ - adr TMP4, jsimd_idct_4x4_neon_consts - vld1.16 {d0, d1, d2, d3}, [TMP4, :128] - - /* Load all COEF_BLOCK into NEON registers with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | d4 | d5 - * 1 | d6 | d7 - * 2 | d8 | d9 - * 3 | d10 | d11 - * 4 | - | - - * 5 | d12 | d13 - * 6 | d14 | d15 - * 7 | d16 | d17 - */ - vld1.16 {d4, d5, d6, d7}, [COEF_BLOCK, :128]! - vld1.16 {d8, d9, d10, d11}, [COEF_BLOCK, :128]! - add COEF_BLOCK, COEF_BLOCK, #16 - vld1.16 {d12, d13, d14, d15}, [COEF_BLOCK, :128]! - vld1.16 {d16, d17}, [COEF_BLOCK, :128]! - /* dequantize */ - vld1.16 {d18, d19, d20, d21}, [DCT_TABLE, :128]! - vmul.s16 q2, q2, q9 - vld1.16 {d22, d23, d24, d25}, [DCT_TABLE, :128]! - vmul.s16 q3, q3, q10 - vmul.s16 q4, q4, q11 - add DCT_TABLE, DCT_TABLE, #16 - vld1.16 {d26, d27, d28, d29}, [DCT_TABLE, :128]! - vmul.s16 q5, q5, q12 - vmul.s16 q6, q6, q13 - vld1.16 {d30, d31}, [DCT_TABLE, :128]! - vmul.s16 q7, q7, q14 - vmul.s16 q8, q8, q15 - - /* Pass 1 */ - idct_helper d4, d6, d8, d10, d12, d14, d16, 12, d4, d6, d8, d10 - transpose_4x4 d4, d6, d8, d10 - idct_helper d5, d7, d9, d11, d13, d15, d17, 12, d5, d7, d9, d11 - transpose_4x4 d5, d7, d9, d11 - - /* Pass 2 */ - idct_helper d4, d6, d8, d10, d7, d9, d11, 19, d26, d27, d28, d29 - transpose_4x4 d26, d27, d28, d29 - - /* Range limit */ - vmov.u16 q15, #0x80 - vadd.s16 q13, q13, q15 - vadd.s16 q14, q14, q15 - vqmovun.s16 d26, q13 - vqmovun.s16 d27, q14 - - /* Store results to the output buffer */ - ldmia OUTPUT_BUF, {TMP1, TMP2, TMP3, TMP4} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - add TMP3, TMP3, OUTPUT_COL - add TMP4, TMP4, OUTPUT_COL - -#if defined(__ARMEL__) && !RESPECT_STRICT_ALIGNMENT - /* We can use much less instructions on little endian systems if the - * OS kernel is not configured to trap unaligned memory accesses - */ - vst1.32 {d26[0]}, [TMP1]! - vst1.32 {d27[0]}, [TMP3]! - vst1.32 {d26[1]}, [TMP2]! - vst1.32 {d27[1]}, [TMP4]! -#else - vst1.8 {d26[0]}, [TMP1]! - vst1.8 {d27[0]}, [TMP3]! - vst1.8 {d26[1]}, [TMP1]! - vst1.8 {d27[1]}, [TMP3]! - vst1.8 {d26[2]}, [TMP1]! - vst1.8 {d27[2]}, [TMP3]! - vst1.8 {d26[3]}, [TMP1]! - vst1.8 {d27[3]}, [TMP3]! - - vst1.8 {d26[4]}, [TMP2]! - vst1.8 {d27[4]}, [TMP4]! - vst1.8 {d26[5]}, [TMP2]! - vst1.8 {d27[5]}, [TMP4]! - vst1.8 {d26[6]}, [TMP2]! - vst1.8 {d27[6]}, [TMP4]! - vst1.8 {d26[7]}, [TMP2]! - vst1.8 {d27[7]}, [TMP4]! -#endif - - vpop {d8-d15} - bx lr - - .unreq DCT_TABLE - .unreq COEF_BLOCK - .unreq OUTPUT_BUF - .unreq OUTPUT_COL - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMP4 - -.purgem idct_helper - - -/*****************************************************************************/ - -/* - * jsimd_idct_2x2_neon - * - * This function contains inverse-DCT code for getting reduced-size - * 2x2 pixels output from an 8x8 DCT block. It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_idct_2x2' - * function from jpeg-6b (jidctred.c). - * - * NOTE: jpeg-8 has an improved implementation of 2x2 inverse-DCT, which - * requires much less arithmetic operations and hence should be faster. - * The primary purpose of this particular NEON optimized function is - * bit exact compatibility with jpeg-6b. - */ - -.balign 8 -jsimd_idct_2x2_neon_consts: - .short -FIX_0_720959822 /* d0[0] */ - .short FIX_0_850430095 /* d0[1] */ - .short -FIX_1_272758580 /* d0[2] */ - .short FIX_3_624509785 /* d0[3] */ - -.macro idct_helper x4, x6, x10, x12, x16, shift, y26, y27 - vshll.s16 q14, \x4, #15 - vmull.s16 q13, \x6, d0[3] - vmlal.s16 q13, \x10, d0[2] - vmlal.s16 q13, \x12, d0[1] - vmlal.s16 q13, \x16, d0[0] - - vadd.s32 q10, q14, q13 - vsub.s32 q14, q14, q13 - - .if \shift > 16 - vrshr.s32 q10, q10, #\shift - vrshr.s32 q14, q14, #\shift - vmovn.s32 \y26, q10 - vmovn.s32 \y27, q14 - .else - vrshrn.s32 \y26, q10, #\shift - vrshrn.s32 \y27, q14, #\shift - .endif -.endm - -asm_function jsimd_idct_2x2_neon - - DCT_TABLE .req r0 - COEF_BLOCK .req r1 - OUTPUT_BUF .req r2 - OUTPUT_COL .req r3 - TMP1 .req r0 - TMP2 .req ip - - vpush {d8-d15} - - /* Load constants */ - adr TMP2, jsimd_idct_2x2_neon_consts - vld1.16 {d0}, [TMP2, :64] - - /* Load all COEF_BLOCK into NEON registers with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | d4 | d5 - * 1 | d6 | d7 - * 2 | - | - - * 3 | d10 | d11 - * 4 | - | - - * 5 | d12 | d13 - * 6 | - | - - * 7 | d16 | d17 - */ - vld1.16 {d4, d5, d6, d7}, [COEF_BLOCK, :128]! - add COEF_BLOCK, COEF_BLOCK, #16 - vld1.16 {d10, d11}, [COEF_BLOCK, :128]! - add COEF_BLOCK, COEF_BLOCK, #16 - vld1.16 {d12, d13}, [COEF_BLOCK, :128]! - add COEF_BLOCK, COEF_BLOCK, #16 - vld1.16 {d16, d17}, [COEF_BLOCK, :128]! - /* Dequantize */ - vld1.16 {d18, d19, d20, d21}, [DCT_TABLE, :128]! - vmul.s16 q2, q2, q9 - vmul.s16 q3, q3, q10 - add DCT_TABLE, DCT_TABLE, #16 - vld1.16 {d24, d25}, [DCT_TABLE, :128]! - vmul.s16 q5, q5, q12 - add DCT_TABLE, DCT_TABLE, #16 - vld1.16 {d26, d27}, [DCT_TABLE, :128]! - vmul.s16 q6, q6, q13 - add DCT_TABLE, DCT_TABLE, #16 - vld1.16 {d30, d31}, [DCT_TABLE, :128]! - vmul.s16 q8, q8, q15 - - /* Pass 1 */ -#if 0 - idct_helper d4, d6, d10, d12, d16, 13, d4, d6 - transpose_4x4 d4, d6, d8, d10 - idct_helper d5, d7, d11, d13, d17, 13, d5, d7 - transpose_4x4 d5, d7, d9, d11 -#else - vmull.s16 q13, d6, d0[3] - vmlal.s16 q13, d10, d0[2] - vmlal.s16 q13, d12, d0[1] - vmlal.s16 q13, d16, d0[0] - vmull.s16 q12, d7, d0[3] - vmlal.s16 q12, d11, d0[2] - vmlal.s16 q12, d13, d0[1] - vmlal.s16 q12, d17, d0[0] - vshll.s16 q14, d4, #15 - vshll.s16 q15, d5, #15 - vadd.s32 q10, q14, q13 - vsub.s32 q14, q14, q13 - vrshrn.s32 d4, q10, #13 - vrshrn.s32 d6, q14, #13 - vadd.s32 q10, q15, q12 - vsub.s32 q14, q15, q12 - vrshrn.s32 d5, q10, #13 - vrshrn.s32 d7, q14, #13 - vtrn.16 q2, q3 - vtrn.32 q3, q5 -#endif - - /* Pass 2 */ - idct_helper d4, d6, d10, d7, d11, 20, d26, d27 - - /* Range limit */ - vmov.u16 q15, #0x80 - vadd.s16 q13, q13, q15 - vqmovun.s16 d26, q13 - vqmovun.s16 d27, q13 - - /* Store results to the output buffer */ - ldmia OUTPUT_BUF, {TMP1, TMP2} - add TMP1, TMP1, OUTPUT_COL - add TMP2, TMP2, OUTPUT_COL - - vst1.8 {d26[0]}, [TMP1]! - vst1.8 {d27[4]}, [TMP1]! - vst1.8 {d26[1]}, [TMP2]! - vst1.8 {d27[5]}, [TMP2]! - - vpop {d8-d15} - bx lr - - .unreq DCT_TABLE - .unreq COEF_BLOCK - .unreq OUTPUT_BUF - .unreq OUTPUT_COL - .unreq TMP1 - .unreq TMP2 - -.purgem idct_helper - - -/*****************************************************************************/ - -/* - * jsimd_ycc_extrgb_convert_neon - * jsimd_ycc_extbgr_convert_neon - * jsimd_ycc_extrgbx_convert_neon - * jsimd_ycc_extbgrx_convert_neon - * jsimd_ycc_extxbgr_convert_neon - * jsimd_ycc_extxrgb_convert_neon - * - * Colorspace conversion YCbCr -> RGB - */ - - -.macro do_load size - .if \size == 8 - vld1.8 {d4}, [U, :64]! - vld1.8 {d5}, [V, :64]! - vld1.8 {d0}, [Y, :64]! - pld [U, #64] - pld [V, #64] - pld [Y, #64] - .elseif \size == 4 - vld1.8 {d4[0]}, [U]! - vld1.8 {d4[1]}, [U]! - vld1.8 {d4[2]}, [U]! - vld1.8 {d4[3]}, [U]! - vld1.8 {d5[0]}, [V]! - vld1.8 {d5[1]}, [V]! - vld1.8 {d5[2]}, [V]! - vld1.8 {d5[3]}, [V]! - vld1.8 {d0[0]}, [Y]! - vld1.8 {d0[1]}, [Y]! - vld1.8 {d0[2]}, [Y]! - vld1.8 {d0[3]}, [Y]! - .elseif \size == 2 - vld1.8 {d4[4]}, [U]! - vld1.8 {d4[5]}, [U]! - vld1.8 {d5[4]}, [V]! - vld1.8 {d5[5]}, [V]! - vld1.8 {d0[4]}, [Y]! - vld1.8 {d0[5]}, [Y]! - .elseif \size == 1 - vld1.8 {d4[6]}, [U]! - vld1.8 {d5[6]}, [V]! - vld1.8 {d0[6]}, [Y]! - .else - .error unsupported macroblock size - .endif -.endm - -.macro do_store bpp, size - .if \bpp == 24 - .if \size == 8 - vst3.8 {d10, d11, d12}, [RGB]! - .elseif \size == 4 - vst3.8 {d10[0], d11[0], d12[0]}, [RGB]! - vst3.8 {d10[1], d11[1], d12[1]}, [RGB]! - vst3.8 {d10[2], d11[2], d12[2]}, [RGB]! - vst3.8 {d10[3], d11[3], d12[3]}, [RGB]! - .elseif \size == 2 - vst3.8 {d10[4], d11[4], d12[4]}, [RGB]! - vst3.8 {d10[5], d11[5], d12[5]}, [RGB]! - .elseif \size == 1 - vst3.8 {d10[6], d11[6], d12[6]}, [RGB]! - .else - .error unsupported macroblock size - .endif - .elseif \bpp == 32 - .if \size == 8 - vst4.8 {d10, d11, d12, d13}, [RGB]! - .elseif \size == 4 - vst4.8 {d10[0], d11[0], d12[0], d13[0]}, [RGB]! - vst4.8 {d10[1], d11[1], d12[1], d13[1]}, [RGB]! - vst4.8 {d10[2], d11[2], d12[2], d13[2]}, [RGB]! - vst4.8 {d10[3], d11[3], d12[3], d13[3]}, [RGB]! - .elseif \size == 2 - vst4.8 {d10[4], d11[4], d12[4], d13[4]}, [RGB]! - vst4.8 {d10[5], d11[5], d12[5], d13[5]}, [RGB]! - .elseif \size == 1 - vst4.8 {d10[6], d11[6], d12[6], d13[6]}, [RGB]! - .else - .error unsupported macroblock size - .endif - .elseif \bpp == 16 - .if \size == 8 - vst1.16 {q15}, [RGB]! - .elseif \size == 4 - vst1.16 {d30}, [RGB]! - .elseif \size == 2 - vst1.16 {d31[0]}, [RGB]! - vst1.16 {d31[1]}, [RGB]! - .elseif \size == 1 - vst1.16 {d31[2]}, [RGB]! - .else - .error unsupported macroblock size - .endif - .else - .error unsupported bpp - .endif -.endm - -.macro generate_jsimd_ycc_rgb_convert_neon colorid, bpp, r_offs, g_offs, b_offs - -/* - * 2-stage pipelined YCbCr->RGB conversion - */ - -.macro do_yuv_to_rgb_stage1 - vaddw.u8 q3, q1, d4 /* q3 = u - 128 */ - vaddw.u8 q4, q1, d5 /* q2 = v - 128 */ - vmull.s16 q10, d6, d1[1] /* multiply by -11277 */ - vmlal.s16 q10, d8, d1[2] /* multiply by -23401 */ - vmull.s16 q11, d7, d1[1] /* multiply by -11277 */ - vmlal.s16 q11, d9, d1[2] /* multiply by -23401 */ - vmull.s16 q12, d8, d1[0] /* multiply by 22971 */ - vmull.s16 q13, d9, d1[0] /* multiply by 22971 */ - vmull.s16 q14, d6, d1[3] /* multiply by 29033 */ - vmull.s16 q15, d7, d1[3] /* multiply by 29033 */ -.endm - -.macro do_yuv_to_rgb_stage2 - vrshrn.s32 d20, q10, #15 - vrshrn.s32 d21, q11, #15 - vrshrn.s32 d24, q12, #14 - vrshrn.s32 d25, q13, #14 - vrshrn.s32 d28, q14, #14 - vrshrn.s32 d29, q15, #14 - vaddw.u8 q11, q10, d0 - vaddw.u8 q12, q12, d0 - vaddw.u8 q14, q14, d0 - .if \bpp != 16 - vqmovun.s16 d1\g_offs, q11 - vqmovun.s16 d1\r_offs, q12 - vqmovun.s16 d1\b_offs, q14 - .else /* rgb565 */ - vqshlu.s16 q13, q11, #8 - vqshlu.s16 q15, q12, #8 - vqshlu.s16 q14, q14, #8 - vsri.u16 q15, q13, #5 - vsri.u16 q15, q14, #11 - .endif -.endm - -.macro do_yuv_to_rgb_stage2_store_load_stage1 - /* "do_yuv_to_rgb_stage2" and "store" */ - vrshrn.s32 d20, q10, #15 - /* "load" and "do_yuv_to_rgb_stage1" */ - pld [U, #64] - vrshrn.s32 d21, q11, #15 - pld [V, #64] - vrshrn.s32 d24, q12, #14 - vrshrn.s32 d25, q13, #14 - vld1.8 {d4}, [U, :64]! - vrshrn.s32 d28, q14, #14 - vld1.8 {d5}, [V, :64]! - vrshrn.s32 d29, q15, #14 - vaddw.u8 q3, q1, d4 /* q3 = u - 128 */ - vaddw.u8 q4, q1, d5 /* q2 = v - 128 */ - vaddw.u8 q11, q10, d0 - vmull.s16 q10, d6, d1[1] /* multiply by -11277 */ - vmlal.s16 q10, d8, d1[2] /* multiply by -23401 */ - vaddw.u8 q12, q12, d0 - vaddw.u8 q14, q14, d0 - .if \bpp != 16 /**************** rgb24/rgb32 ******************************/ - vqmovun.s16 d1\g_offs, q11 - pld [Y, #64] - vqmovun.s16 d1\r_offs, q12 - vld1.8 {d0}, [Y, :64]! - vqmovun.s16 d1\b_offs, q14 - vmull.s16 q11, d7, d1[1] /* multiply by -11277 */ - vmlal.s16 q11, d9, d1[2] /* multiply by -23401 */ - do_store \bpp, 8 - vmull.s16 q12, d8, d1[0] /* multiply by 22971 */ - vmull.s16 q13, d9, d1[0] /* multiply by 22971 */ - vmull.s16 q14, d6, d1[3] /* multiply by 29033 */ - vmull.s16 q15, d7, d1[3] /* multiply by 29033 */ - .else /**************************** rgb565 ********************************/ - vqshlu.s16 q13, q11, #8 - pld [Y, #64] - vqshlu.s16 q15, q12, #8 - vqshlu.s16 q14, q14, #8 - vld1.8 {d0}, [Y, :64]! - vmull.s16 q11, d7, d1[1] - vmlal.s16 q11, d9, d1[2] - vsri.u16 q15, q13, #5 - vmull.s16 q12, d8, d1[0] - vsri.u16 q15, q14, #11 - vmull.s16 q13, d9, d1[0] - vmull.s16 q14, d6, d1[3] - do_store \bpp, 8 - vmull.s16 q15, d7, d1[3] - .endif -.endm - -.macro do_yuv_to_rgb - do_yuv_to_rgb_stage1 - do_yuv_to_rgb_stage2 -.endm - -/* Apple gas crashes on adrl, work around that by using adr. - * But this requires a copy of these constants for each function. - */ - -.balign 16 -jsimd_ycc_\colorid\()_neon_consts: - .short 0, 0, 0, 0 - .short 22971, -11277, -23401, 29033 - .short -128, -128, -128, -128 - .short -128, -128, -128, -128 - -asm_function jsimd_ycc_\colorid\()_convert_neon - OUTPUT_WIDTH .req r0 - INPUT_BUF .req r1 - INPUT_ROW .req r2 - OUTPUT_BUF .req r3 - NUM_ROWS .req r4 - - INPUT_BUF0 .req r5 - INPUT_BUF1 .req r6 - INPUT_BUF2 .req INPUT_BUF - - RGB .req r7 - Y .req r8 - U .req r9 - V .req r10 - N .req ip - - /* Load constants to d1, d2, d3 (d0 is just used for padding) */ - adr ip, jsimd_ycc_\colorid\()_neon_consts - vld1.16 {d0, d1, d2, d3}, [ip, :128] - - /* Save ARM registers and handle input arguments */ - push {r4, r5, r6, r7, r8, r9, r10, lr} - ldr NUM_ROWS, [sp, #(4 * 8)] - ldr INPUT_BUF0, [INPUT_BUF] - ldr INPUT_BUF1, [INPUT_BUF, #4] - ldr INPUT_BUF2, [INPUT_BUF, #8] - .unreq INPUT_BUF - - /* Save NEON registers */ - vpush {d8-d15} - - /* Initially set d10, d11, d12, d13 to 0xFF */ - vmov.u8 q5, #255 - vmov.u8 q6, #255 - - /* Outer loop over scanlines */ - cmp NUM_ROWS, #1 - blt 9f -0: - ldr Y, [INPUT_BUF0, INPUT_ROW, lsl #2] - ldr U, [INPUT_BUF1, INPUT_ROW, lsl #2] - mov N, OUTPUT_WIDTH - ldr V, [INPUT_BUF2, INPUT_ROW, lsl #2] - add INPUT_ROW, INPUT_ROW, #1 - ldr RGB, [OUTPUT_BUF], #4 - - /* Inner loop over pixels */ - subs N, N, #8 - blt 3f - do_load 8 - do_yuv_to_rgb_stage1 - subs N, N, #8 - blt 2f -1: - do_yuv_to_rgb_stage2_store_load_stage1 - subs N, N, #8 - bge 1b -2: - do_yuv_to_rgb_stage2 - do_store \bpp, 8 - tst N, #7 - beq 8f -3: - tst N, #4 - beq 3f - do_load 4 -3: - tst N, #2 - beq 4f - do_load 2 -4: - tst N, #1 - beq 5f - do_load 1 -5: - do_yuv_to_rgb - tst N, #4 - beq 6f - do_store \bpp, 4 -6: - tst N, #2 - beq 7f - do_store \bpp, 2 -7: - tst N, #1 - beq 8f - do_store \bpp, 1 -8: - subs NUM_ROWS, NUM_ROWS, #1 - bgt 0b -9: - /* Restore all registers and return */ - vpop {d8-d15} - pop {r4, r5, r6, r7, r8, r9, r10, pc} - - .unreq OUTPUT_WIDTH - .unreq INPUT_ROW - .unreq OUTPUT_BUF - .unreq NUM_ROWS - .unreq INPUT_BUF0 - .unreq INPUT_BUF1 - .unreq INPUT_BUF2 - .unreq RGB - .unreq Y - .unreq U - .unreq V - .unreq N - -.purgem do_yuv_to_rgb -.purgem do_yuv_to_rgb_stage1 -.purgem do_yuv_to_rgb_stage2 -.purgem do_yuv_to_rgb_stage2_store_load_stage1 - -.endm - -/*--------------------------------- id ----- bpp R G B */ -generate_jsimd_ycc_rgb_convert_neon extrgb, 24, 0, 1, 2 -generate_jsimd_ycc_rgb_convert_neon extbgr, 24, 2, 1, 0 -generate_jsimd_ycc_rgb_convert_neon extrgbx, 32, 0, 1, 2 -generate_jsimd_ycc_rgb_convert_neon extbgrx, 32, 2, 1, 0 -generate_jsimd_ycc_rgb_convert_neon extxbgr, 32, 3, 2, 1 -generate_jsimd_ycc_rgb_convert_neon extxrgb, 32, 1, 2, 3 -generate_jsimd_ycc_rgb_convert_neon rgb565, 16, 0, 0, 0 - -.purgem do_load -.purgem do_store - - -/*****************************************************************************/ - -/* - * jsimd_extrgb_ycc_convert_neon - * jsimd_extbgr_ycc_convert_neon - * jsimd_extrgbx_ycc_convert_neon - * jsimd_extbgrx_ycc_convert_neon - * jsimd_extxbgr_ycc_convert_neon - * jsimd_extxrgb_ycc_convert_neon - * - * Colorspace conversion RGB -> YCbCr - */ - -.macro do_store size - .if \size == 8 - vst1.8 {d20}, [Y]! - vst1.8 {d21}, [U]! - vst1.8 {d22}, [V]! - .elseif \size == 4 - vst1.8 {d20[0]}, [Y]! - vst1.8 {d20[1]}, [Y]! - vst1.8 {d20[2]}, [Y]! - vst1.8 {d20[3]}, [Y]! - vst1.8 {d21[0]}, [U]! - vst1.8 {d21[1]}, [U]! - vst1.8 {d21[2]}, [U]! - vst1.8 {d21[3]}, [U]! - vst1.8 {d22[0]}, [V]! - vst1.8 {d22[1]}, [V]! - vst1.8 {d22[2]}, [V]! - vst1.8 {d22[3]}, [V]! - .elseif \size == 2 - vst1.8 {d20[4]}, [Y]! - vst1.8 {d20[5]}, [Y]! - vst1.8 {d21[4]}, [U]! - vst1.8 {d21[5]}, [U]! - vst1.8 {d22[4]}, [V]! - vst1.8 {d22[5]}, [V]! - .elseif \size == 1 - vst1.8 {d20[6]}, [Y]! - vst1.8 {d21[6]}, [U]! - vst1.8 {d22[6]}, [V]! - .else - .error unsupported macroblock size - .endif -.endm - -.macro do_load bpp, size - .if \bpp == 24 - .if \size == 8 - vld3.8 {d10, d11, d12}, [RGB]! - pld [RGB, #128] - .elseif \size == 4 - vld3.8 {d10[0], d11[0], d12[0]}, [RGB]! - vld3.8 {d10[1], d11[1], d12[1]}, [RGB]! - vld3.8 {d10[2], d11[2], d12[2]}, [RGB]! - vld3.8 {d10[3], d11[3], d12[3]}, [RGB]! - .elseif \size == 2 - vld3.8 {d10[4], d11[4], d12[4]}, [RGB]! - vld3.8 {d10[5], d11[5], d12[5]}, [RGB]! - .elseif \size == 1 - vld3.8 {d10[6], d11[6], d12[6]}, [RGB]! - .else - .error unsupported macroblock size - .endif - .elseif \bpp == 32 - .if \size == 8 - vld4.8 {d10, d11, d12, d13}, [RGB]! - pld [RGB, #128] - .elseif \size == 4 - vld4.8 {d10[0], d11[0], d12[0], d13[0]}, [RGB]! - vld4.8 {d10[1], d11[1], d12[1], d13[1]}, [RGB]! - vld4.8 {d10[2], d11[2], d12[2], d13[2]}, [RGB]! - vld4.8 {d10[3], d11[3], d12[3], d13[3]}, [RGB]! - .elseif \size == 2 - vld4.8 {d10[4], d11[4], d12[4], d13[4]}, [RGB]! - vld4.8 {d10[5], d11[5], d12[5], d13[5]}, [RGB]! - .elseif \size == 1 - vld4.8 {d10[6], d11[6], d12[6], d13[6]}, [RGB]! - .else - .error unsupported macroblock size - .endif - .else - .error unsupported bpp - .endif -.endm - -.macro generate_jsimd_rgb_ycc_convert_neon colorid, bpp, r_offs, g_offs, b_offs - -/* - * 2-stage pipelined RGB->YCbCr conversion - */ - -.macro do_rgb_to_yuv_stage1 - vmovl.u8 q2, d1\r_offs /* r = { d4, d5 } */ - vmovl.u8 q3, d1\g_offs /* g = { d6, d7 } */ - vmovl.u8 q4, d1\b_offs /* b = { d8, d9 } */ - vmull.u16 q7, d4, d0[0] - vmlal.u16 q7, d6, d0[1] - vmlal.u16 q7, d8, d0[2] - vmull.u16 q8, d5, d0[0] - vmlal.u16 q8, d7, d0[1] - vmlal.u16 q8, d9, d0[2] - vrev64.32 q9, q1 - vrev64.32 q13, q1 - vmlsl.u16 q9, d4, d0[3] - vmlsl.u16 q9, d6, d1[0] - vmlal.u16 q9, d8, d1[1] - vmlsl.u16 q13, d5, d0[3] - vmlsl.u16 q13, d7, d1[0] - vmlal.u16 q13, d9, d1[1] - vrev64.32 q14, q1 - vrev64.32 q15, q1 - vmlal.u16 q14, d4, d1[1] - vmlsl.u16 q14, d6, d1[2] - vmlsl.u16 q14, d8, d1[3] - vmlal.u16 q15, d5, d1[1] - vmlsl.u16 q15, d7, d1[2] - vmlsl.u16 q15, d9, d1[3] -.endm - -.macro do_rgb_to_yuv_stage2 - vrshrn.u32 d20, q7, #16 - vrshrn.u32 d21, q8, #16 - vshrn.u32 d22, q9, #16 - vshrn.u32 d23, q13, #16 - vshrn.u32 d24, q14, #16 - vshrn.u32 d25, q15, #16 - vmovn.u16 d20, q10 /* d20 = y */ - vmovn.u16 d21, q11 /* d21 = u */ - vmovn.u16 d22, q12 /* d22 = v */ -.endm - -.macro do_rgb_to_yuv - do_rgb_to_yuv_stage1 - do_rgb_to_yuv_stage2 -.endm - -.macro do_rgb_to_yuv_stage2_store_load_stage1 - vrshrn.u32 d20, q7, #16 - vrshrn.u32 d21, q8, #16 - vshrn.u32 d22, q9, #16 - vrev64.32 q9, q1 - vshrn.u32 d23, q13, #16 - vrev64.32 q13, q1 - vshrn.u32 d24, q14, #16 - vshrn.u32 d25, q15, #16 - do_load \bpp, 8 - vmovn.u16 d20, q10 /* d20 = y */ - vmovl.u8 q2, d1\r_offs /* r = { d4, d5 } */ - vmovn.u16 d21, q11 /* d21 = u */ - vmovl.u8 q3, d1\g_offs /* g = { d6, d7 } */ - vmovn.u16 d22, q12 /* d22 = v */ - vmovl.u8 q4, d1\b_offs /* b = { d8, d9 } */ - vmull.u16 q7, d4, d0[0] - vmlal.u16 q7, d6, d0[1] - vmlal.u16 q7, d8, d0[2] - vst1.8 {d20}, [Y]! - vmull.u16 q8, d5, d0[0] - vmlal.u16 q8, d7, d0[1] - vmlal.u16 q8, d9, d0[2] - vmlsl.u16 q9, d4, d0[3] - vmlsl.u16 q9, d6, d1[0] - vmlal.u16 q9, d8, d1[1] - vst1.8 {d21}, [U]! - vmlsl.u16 q13, d5, d0[3] - vmlsl.u16 q13, d7, d1[0] - vmlal.u16 q13, d9, d1[1] - vrev64.32 q14, q1 - vrev64.32 q15, q1 - vmlal.u16 q14, d4, d1[1] - vmlsl.u16 q14, d6, d1[2] - vmlsl.u16 q14, d8, d1[3] - vst1.8 {d22}, [V]! - vmlal.u16 q15, d5, d1[1] - vmlsl.u16 q15, d7, d1[2] - vmlsl.u16 q15, d9, d1[3] -.endm - -.balign 16 -jsimd_\colorid\()_ycc_neon_consts: - .short 19595, 38470, 7471, 11059 - .short 21709, 32768, 27439, 5329 - .short 32767, 128, 32767, 128 - .short 32767, 128, 32767, 128 - -asm_function jsimd_\colorid\()_ycc_convert_neon - OUTPUT_WIDTH .req r0 - INPUT_BUF .req r1 - OUTPUT_BUF .req r2 - OUTPUT_ROW .req r3 - NUM_ROWS .req r4 - - OUTPUT_BUF0 .req r5 - OUTPUT_BUF1 .req r6 - OUTPUT_BUF2 .req OUTPUT_BUF - - RGB .req r7 - Y .req r8 - U .req r9 - V .req r10 - N .req ip - - /* Load constants to d0, d1, d2, d3 */ - adr ip, jsimd_\colorid\()_ycc_neon_consts - vld1.16 {d0, d1, d2, d3}, [ip, :128] - - /* Save ARM registers and handle input arguments */ - push {r4, r5, r6, r7, r8, r9, r10, lr} - ldr NUM_ROWS, [sp, #(4 * 8)] - ldr OUTPUT_BUF0, [OUTPUT_BUF] - ldr OUTPUT_BUF1, [OUTPUT_BUF, #4] - ldr OUTPUT_BUF2, [OUTPUT_BUF, #8] - .unreq OUTPUT_BUF - - /* Save NEON registers */ - vpush {d8-d15} - - /* Outer loop over scanlines */ - cmp NUM_ROWS, #1 - blt 9f -0: - ldr Y, [OUTPUT_BUF0, OUTPUT_ROW, lsl #2] - ldr U, [OUTPUT_BUF1, OUTPUT_ROW, lsl #2] - mov N, OUTPUT_WIDTH - ldr V, [OUTPUT_BUF2, OUTPUT_ROW, lsl #2] - add OUTPUT_ROW, OUTPUT_ROW, #1 - ldr RGB, [INPUT_BUF], #4 - - /* Inner loop over pixels */ - subs N, N, #8 - blt 3f - do_load \bpp, 8 - do_rgb_to_yuv_stage1 - subs N, N, #8 - blt 2f -1: - do_rgb_to_yuv_stage2_store_load_stage1 - subs N, N, #8 - bge 1b -2: - do_rgb_to_yuv_stage2 - do_store 8 - tst N, #7 - beq 8f -3: - tst N, #4 - beq 3f - do_load \bpp, 4 -3: - tst N, #2 - beq 4f - do_load \bpp, 2 -4: - tst N, #1 - beq 5f - do_load \bpp, 1 -5: - do_rgb_to_yuv - tst N, #4 - beq 6f - do_store 4 -6: - tst N, #2 - beq 7f - do_store 2 -7: - tst N, #1 - beq 8f - do_store 1 -8: - subs NUM_ROWS, NUM_ROWS, #1 - bgt 0b -9: - /* Restore all registers and return */ - vpop {d8-d15} - pop {r4, r5, r6, r7, r8, r9, r10, pc} - - .unreq OUTPUT_WIDTH - .unreq OUTPUT_ROW - .unreq INPUT_BUF - .unreq NUM_ROWS - .unreq OUTPUT_BUF0 - .unreq OUTPUT_BUF1 - .unreq OUTPUT_BUF2 - .unreq RGB - .unreq Y - .unreq U - .unreq V - .unreq N - -.purgem do_rgb_to_yuv -.purgem do_rgb_to_yuv_stage1 -.purgem do_rgb_to_yuv_stage2 -.purgem do_rgb_to_yuv_stage2_store_load_stage1 - -.endm - -/*--------------------------------- id ----- bpp R G B */ -generate_jsimd_rgb_ycc_convert_neon extrgb, 24, 0, 1, 2 -generate_jsimd_rgb_ycc_convert_neon extbgr, 24, 2, 1, 0 -generate_jsimd_rgb_ycc_convert_neon extrgbx, 32, 0, 1, 2 -generate_jsimd_rgb_ycc_convert_neon extbgrx, 32, 2, 1, 0 -generate_jsimd_rgb_ycc_convert_neon extxbgr, 32, 3, 2, 1 -generate_jsimd_rgb_ycc_convert_neon extxrgb, 32, 1, 2, 3 - -.purgem do_load -.purgem do_store - - -/*****************************************************************************/ - -/* - * Load data into workspace, applying unsigned->signed conversion - * - * TODO: can be combined with 'jsimd_fdct_ifast_neon' to get - * rid of VST1.16 instructions - */ - -asm_function jsimd_convsamp_neon - SAMPLE_DATA .req r0 - START_COL .req r1 - WORKSPACE .req r2 - TMP1 .req r3 - TMP2 .req r4 - TMP3 .req r5 - TMP4 .req ip - - push {r4, r5} - vmov.u8 d0, #128 - - ldmia SAMPLE_DATA!, {TMP1, TMP2, TMP3, TMP4} - add TMP1, TMP1, START_COL - add TMP2, TMP2, START_COL - add TMP3, TMP3, START_COL - add TMP4, TMP4, START_COL - vld1.8 {d16}, [TMP1] - vsubl.u8 q8, d16, d0 - vld1.8 {d18}, [TMP2] - vsubl.u8 q9, d18, d0 - vld1.8 {d20}, [TMP3] - vsubl.u8 q10, d20, d0 - vld1.8 {d22}, [TMP4] - ldmia SAMPLE_DATA!, {TMP1, TMP2, TMP3, TMP4} - vsubl.u8 q11, d22, d0 - vst1.16 {d16, d17, d18, d19}, [WORKSPACE, :128]! - add TMP1, TMP1, START_COL - add TMP2, TMP2, START_COL - vst1.16 {d20, d21, d22, d23}, [WORKSPACE, :128]! - add TMP3, TMP3, START_COL - add TMP4, TMP4, START_COL - vld1.8 {d24}, [TMP1] - vsubl.u8 q12, d24, d0 - vld1.8 {d26}, [TMP2] - vsubl.u8 q13, d26, d0 - vld1.8 {d28}, [TMP3] - vsubl.u8 q14, d28, d0 - vld1.8 {d30}, [TMP4] - vsubl.u8 q15, d30, d0 - vst1.16 {d24, d25, d26, d27}, [WORKSPACE, :128]! - vst1.16 {d28, d29, d30, d31}, [WORKSPACE, :128]! - pop {r4, r5} - bx lr - - .unreq SAMPLE_DATA - .unreq START_COL - .unreq WORKSPACE - .unreq TMP1 - .unreq TMP2 - .unreq TMP3 - .unreq TMP4 - - -/*****************************************************************************/ - -/* - * jsimd_fdct_ifast_neon - * - * This function contains a fast, not so accurate integer implementation of - * the forward DCT (Discrete Cosine Transform). It uses the same calculations - * and produces exactly the same output as IJG's original 'jpeg_fdct_ifast' - * function from jfdctfst.c - * - * TODO: can be combined with 'jsimd_convsamp_neon' to get - * rid of a bunch of VLD1.16 instructions - */ - -#define XFIX_0_382683433 d0[0] -#define XFIX_0_541196100 d0[1] -#define XFIX_0_707106781 d0[2] -#define XFIX_1_306562965 d0[3] - -.balign 16 -jsimd_fdct_ifast_neon_consts: - .short (98 * 128) /* XFIX_0_382683433 */ - .short (139 * 128) /* XFIX_0_541196100 */ - .short (181 * 128) /* XFIX_0_707106781 */ - .short (334 * 128 - 256 * 128) /* XFIX_1_306562965 */ - -asm_function jsimd_fdct_ifast_neon - - DATA .req r0 - TMP .req ip - - vpush {d8-d15} - - /* Load constants */ - adr TMP, jsimd_fdct_ifast_neon_consts - vld1.16 {d0}, [TMP, :64] - - /* Load all DATA into NEON registers with the following allocation: - * 0 1 2 3 | 4 5 6 7 - * ---------+-------- - * 0 | d16 | d17 | q8 - * 1 | d18 | d19 | q9 - * 2 | d20 | d21 | q10 - * 3 | d22 | d23 | q11 - * 4 | d24 | d25 | q12 - * 5 | d26 | d27 | q13 - * 6 | d28 | d29 | q14 - * 7 | d30 | d31 | q15 - */ - - vld1.16 {d16, d17, d18, d19}, [DATA, :128]! - vld1.16 {d20, d21, d22, d23}, [DATA, :128]! - vld1.16 {d24, d25, d26, d27}, [DATA, :128]! - vld1.16 {d28, d29, d30, d31}, [DATA, :128] - sub DATA, DATA, #(128 - 32) - - mov TMP, #2 -1: - /* Transpose */ - vtrn.16 q12, q13 - vtrn.16 q10, q11 - vtrn.16 q8, q9 - vtrn.16 q14, q15 - vtrn.32 q9, q11 - vtrn.32 q13, q15 - vtrn.32 q8, q10 - vtrn.32 q12, q14 - vswp d30, d23 - vswp d24, d17 - vswp d26, d19 - /* 1-D FDCT */ - vadd.s16 q2, q11, q12 - vswp d28, d21 - vsub.s16 q12, q11, q12 - vsub.s16 q6, q10, q13 - vadd.s16 q10, q10, q13 - vsub.s16 q7, q9, q14 - vadd.s16 q9, q9, q14 - vsub.s16 q1, q8, q15 - vadd.s16 q8, q8, q15 - vsub.s16 q4, q9, q10 - vsub.s16 q5, q8, q2 - vadd.s16 q3, q9, q10 - vadd.s16 q4, q4, q5 - vadd.s16 q2, q8, q2 - vqdmulh.s16 q4, q4, XFIX_0_707106781 - vadd.s16 q11, q12, q6 - vadd.s16 q8, q2, q3 - vsub.s16 q12, q2, q3 - vadd.s16 q3, q6, q7 - vadd.s16 q7, q7, q1 - vqdmulh.s16 q3, q3, XFIX_0_707106781 - vsub.s16 q6, q11, q7 - vadd.s16 q10, q5, q4 - vqdmulh.s16 q6, q6, XFIX_0_382683433 - vsub.s16 q14, q5, q4 - vqdmulh.s16 q11, q11, XFIX_0_541196100 - vqdmulh.s16 q5, q7, XFIX_1_306562965 - vadd.s16 q4, q1, q3 - vsub.s16 q3, q1, q3 - vadd.s16 q7, q7, q6 - vadd.s16 q11, q11, q6 - vadd.s16 q7, q7, q5 - vadd.s16 q13, q3, q11 - vsub.s16 q11, q3, q11 - vadd.s16 q9, q4, q7 - vsub.s16 q15, q4, q7 - subs TMP, TMP, #1 - bne 1b - - /* store results */ - vst1.16 {d16, d17, d18, d19}, [DATA, :128]! - vst1.16 {d20, d21, d22, d23}, [DATA, :128]! - vst1.16 {d24, d25, d26, d27}, [DATA, :128]! - vst1.16 {d28, d29, d30, d31}, [DATA, :128] - - vpop {d8-d15} - bx lr - - .unreq DATA - .unreq TMP - - -/*****************************************************************************/ - -/* - * GLOBAL(void) - * jsimd_quantize_neon(JCOEFPTR coef_block, DCTELEM *divisors, - * DCTELEM *workspace); - * - * Note: the code uses 2 stage pipelining in order to improve instructions - * scheduling and eliminate stalls (this provides ~15% better - * performance for this function on both ARM Cortex-A8 and - * ARM Cortex-A9 when compared to the non-pipelined variant). - * The instructions which belong to the second stage use different - * indentation for better readiability. - */ -asm_function jsimd_quantize_neon - - COEF_BLOCK .req r0 - DIVISORS .req r1 - WORKSPACE .req r2 - - RECIPROCAL .req DIVISORS - CORRECTION .req r3 - SHIFT .req ip - LOOP_COUNT .req r4 - - vld1.16 {d0, d1, d2, d3}, [WORKSPACE, :128]! - vabs.s16 q12, q0 - add CORRECTION, DIVISORS, #(64 * 2) - add SHIFT, DIVISORS, #(64 * 6) - vld1.16 {d20, d21, d22, d23}, [CORRECTION, :128]! - vabs.s16 q13, q1 - vld1.16 {d16, d17, d18, d19}, [RECIPROCAL, :128]! - vadd.u16 q12, q12, q10 /* add correction */ - vadd.u16 q13, q13, q11 - vmull.u16 q10, d24, d16 /* multiply by reciprocal */ - vmull.u16 q11, d25, d17 - vmull.u16 q8, d26, d18 - vmull.u16 q9, d27, d19 - vld1.16 {d24, d25, d26, d27}, [SHIFT, :128]! - vshrn.u32 d20, q10, #16 - vshrn.u32 d21, q11, #16 - vshrn.u32 d22, q8, #16 - vshrn.u32 d23, q9, #16 - vneg.s16 q12, q12 - vneg.s16 q13, q13 - vshr.s16 q2, q0, #15 /* extract sign */ - vshr.s16 q3, q1, #15 - vshl.u16 q14, q10, q12 /* shift */ - vshl.u16 q15, q11, q13 - - push {r4, r5} - mov LOOP_COUNT, #3 -1: - vld1.16 {d0, d1, d2, d3}, [WORKSPACE, :128]! - veor.u16 q14, q14, q2 /* restore sign */ - vabs.s16 q12, q0 - vld1.16 {d20, d21, d22, d23}, [CORRECTION, :128]! - vabs.s16 q13, q1 - veor.u16 q15, q15, q3 - vld1.16 {d16, d17, d18, d19}, [RECIPROCAL, :128]! - vadd.u16 q12, q12, q10 /* add correction */ - vadd.u16 q13, q13, q11 - vmull.u16 q10, d24, d16 /* multiply by reciprocal */ - vmull.u16 q11, d25, d17 - vmull.u16 q8, d26, d18 - vmull.u16 q9, d27, d19 - vsub.u16 q14, q14, q2 - vld1.16 {d24, d25, d26, d27}, [SHIFT, :128]! - vsub.u16 q15, q15, q3 - vshrn.u32 d20, q10, #16 - vshrn.u32 d21, q11, #16 - vst1.16 {d28, d29, d30, d31}, [COEF_BLOCK, :128]! - vshrn.u32 d22, q8, #16 - vshrn.u32 d23, q9, #16 - vneg.s16 q12, q12 - vneg.s16 q13, q13 - vshr.s16 q2, q0, #15 /* extract sign */ - vshr.s16 q3, q1, #15 - vshl.u16 q14, q10, q12 /* shift */ - vshl.u16 q15, q11, q13 - subs LOOP_COUNT, LOOP_COUNT, #1 - bne 1b - pop {r4, r5} - - veor.u16 q14, q14, q2 /* restore sign */ - veor.u16 q15, q15, q3 - vsub.u16 q14, q14, q2 - vsub.u16 q15, q15, q3 - vst1.16 {d28, d29, d30, d31}, [COEF_BLOCK, :128]! - - bx lr /* return */ - - .unreq COEF_BLOCK - .unreq DIVISORS - .unreq WORKSPACE - .unreq RECIPROCAL - .unreq CORRECTION - .unreq SHIFT - .unreq LOOP_COUNT - - -/*****************************************************************************/ - -/* - * GLOBAL(void) - * jsimd_h2v1_fancy_upsample_neon(int max_v_samp_factor, - * JDIMENSION downsampled_width, - * JSAMPARRAY input_data, - * JSAMPARRAY *output_data_ptr); - * - * Note: the use of unaligned writes is the main remaining bottleneck in - * this code, which can be potentially solved to get up to tens - * of percents performance improvement on Cortex-A8/Cortex-A9. - */ - -/* - * Upsample 16 source pixels to 32 destination pixels. The new 16 source - * pixels are loaded to q0. The previous 16 source pixels are in q1. The - * shifted-by-one source pixels are constructed in q2 by using q0 and q1. - * Register d28 is used for multiplication by 3. Register q15 is used - * for adding +1 bias. - */ -.macro upsample16 OUTPTR, INPTR - vld1.8 {q0}, [\INPTR]! - vmovl.u8 q8, d0 - vext.8 q2, q1, q0, #15 - vmovl.u8 q9, d1 - vaddw.u8 q10, q15, d4 - vaddw.u8 q11, q15, d5 - vmlal.u8 q8, d4, d28 - vmlal.u8 q9, d5, d28 - vmlal.u8 q10, d0, d28 - vmlal.u8 q11, d1, d28 - vmov q1, q0 /* backup source pixels to q1 */ - vrshrn.u16 d6, q8, #2 - vrshrn.u16 d7, q9, #2 - vshrn.u16 d8, q10, #2 - vshrn.u16 d9, q11, #2 - vst2.8 {d6, d7, d8, d9}, [\OUTPTR]! -.endm - -/* - * Upsample 32 source pixels to 64 destination pixels. Compared to 'usample16' - * macro, the roles of q0 and q1 registers are reversed for even and odd - * groups of 16 pixels, that's why "vmov q1, q0" instructions are not needed. - * Also this unrolling allows to reorder loads and stores to compensate - * multiplication latency and reduce stalls. - */ -.macro upsample32 OUTPTR, INPTR - /* even 16 pixels group */ - vld1.8 {q0}, [\INPTR]! - vmovl.u8 q8, d0 - vext.8 q2, q1, q0, #15 - vmovl.u8 q9, d1 - vaddw.u8 q10, q15, d4 - vaddw.u8 q11, q15, d5 - vmlal.u8 q8, d4, d28 - vmlal.u8 q9, d5, d28 - vmlal.u8 q10, d0, d28 - vmlal.u8 q11, d1, d28 - /* odd 16 pixels group */ - vld1.8 {q1}, [\INPTR]! - vrshrn.u16 d6, q8, #2 - vrshrn.u16 d7, q9, #2 - vshrn.u16 d8, q10, #2 - vshrn.u16 d9, q11, #2 - vmovl.u8 q8, d2 - vext.8 q2, q0, q1, #15 - vmovl.u8 q9, d3 - vaddw.u8 q10, q15, d4 - vaddw.u8 q11, q15, d5 - vmlal.u8 q8, d4, d28 - vmlal.u8 q9, d5, d28 - vmlal.u8 q10, d2, d28 - vmlal.u8 q11, d3, d28 - vst2.8 {d6, d7, d8, d9}, [\OUTPTR]! - vrshrn.u16 d6, q8, #2 - vrshrn.u16 d7, q9, #2 - vshrn.u16 d8, q10, #2 - vshrn.u16 d9, q11, #2 - vst2.8 {d6, d7, d8, d9}, [\OUTPTR]! -.endm - -/* - * Upsample a row of WIDTH pixels from INPTR to OUTPTR. - */ -.macro upsample_row OUTPTR, INPTR, WIDTH, TMP1 - /* special case for the first and last pixels */ - sub \WIDTH, \WIDTH, #1 - add \OUTPTR, \OUTPTR, #1 - ldrb \TMP1, [\INPTR, \WIDTH] - strb \TMP1, [\OUTPTR, \WIDTH, asl #1] - ldrb \TMP1, [\INPTR], #1 - strb \TMP1, [\OUTPTR, #-1] - vmov.8 d3[7], \TMP1 - - subs \WIDTH, \WIDTH, #32 - blt 5f -0: /* process 32 pixels per iteration */ - upsample32 \OUTPTR, \INPTR - subs \WIDTH, \WIDTH, #32 - bge 0b -5: - adds \WIDTH, \WIDTH, #16 - blt 1f -0: /* process 16 pixels if needed */ - upsample16 \OUTPTR, \INPTR - subs \WIDTH, \WIDTH, #16 -1: - adds \WIDTH, \WIDTH, #16 - beq 9f - - /* load the remaining 1-15 pixels */ - add \INPTR, \INPTR, \WIDTH - tst \WIDTH, #1 - beq 2f - sub \INPTR, \INPTR, #1 - vld1.8 {d0[0]}, [\INPTR] -2: - tst \WIDTH, #2 - beq 2f - vext.8 d0, d0, d0, #6 - sub \INPTR, \INPTR, #1 - vld1.8 {d0[1]}, [\INPTR] - sub \INPTR, \INPTR, #1 - vld1.8 {d0[0]}, [\INPTR] -2: - tst \WIDTH, #4 - beq 2f - vrev64.32 d0, d0 - sub \INPTR, \INPTR, #1 - vld1.8 {d0[3]}, [\INPTR] - sub \INPTR, \INPTR, #1 - vld1.8 {d0[2]}, [\INPTR] - sub \INPTR, \INPTR, #1 - vld1.8 {d0[1]}, [\INPTR] - sub \INPTR, \INPTR, #1 - vld1.8 {d0[0]}, [\INPTR] -2: - tst \WIDTH, #8 - beq 2f - vmov d1, d0 - sub \INPTR, \INPTR, #8 - vld1.8 {d0}, [\INPTR] -2: /* upsample the remaining pixels */ - vmovl.u8 q8, d0 - vext.8 q2, q1, q0, #15 - vmovl.u8 q9, d1 - vaddw.u8 q10, q15, d4 - vaddw.u8 q11, q15, d5 - vmlal.u8 q8, d4, d28 - vmlal.u8 q9, d5, d28 - vmlal.u8 q10, d0, d28 - vmlal.u8 q11, d1, d28 - vrshrn.u16 d10, q8, #2 - vrshrn.u16 d12, q9, #2 - vshrn.u16 d11, q10, #2 - vshrn.u16 d13, q11, #2 - vzip.8 d10, d11 - vzip.8 d12, d13 - /* store the remaining pixels */ - tst \WIDTH, #8 - beq 2f - vst1.8 {d10, d11}, [\OUTPTR]! - vmov q5, q6 -2: - tst \WIDTH, #4 - beq 2f - vst1.8 {d10}, [\OUTPTR]! - vmov d10, d11 -2: - tst \WIDTH, #2 - beq 2f - vst1.8 {d10[0]}, [\OUTPTR]! - vst1.8 {d10[1]}, [\OUTPTR]! - vst1.8 {d10[2]}, [\OUTPTR]! - vst1.8 {d10[3]}, [\OUTPTR]! - vext.8 d10, d10, d10, #4 -2: - tst \WIDTH, #1 - beq 2f - vst1.8 {d10[0]}, [\OUTPTR]! - vst1.8 {d10[1]}, [\OUTPTR]! -2: -9: -.endm - -asm_function jsimd_h2v1_fancy_upsample_neon - - MAX_V_SAMP_FACTOR .req r0 - DOWNSAMPLED_WIDTH .req r1 - INPUT_DATA .req r2 - OUTPUT_DATA_PTR .req r3 - OUTPUT_DATA .req OUTPUT_DATA_PTR - - OUTPTR .req r4 - INPTR .req r5 - WIDTH .req ip - TMP .req lr - - push {r4, r5, r6, lr} - vpush {d8-d15} - - ldr OUTPUT_DATA, [OUTPUT_DATA_PTR] - cmp MAX_V_SAMP_FACTOR, #0 - ble 99f - - /* initialize constants */ - vmov.u8 d28, #3 - vmov.u16 q15, #1 -11: - ldr INPTR, [INPUT_DATA], #4 - ldr OUTPTR, [OUTPUT_DATA], #4 - mov WIDTH, DOWNSAMPLED_WIDTH - upsample_row OUTPTR, INPTR, WIDTH, TMP - subs MAX_V_SAMP_FACTOR, MAX_V_SAMP_FACTOR, #1 - bgt 11b - -99: - vpop {d8-d15} - pop {r4, r5, r6, pc} - - .unreq MAX_V_SAMP_FACTOR - .unreq DOWNSAMPLED_WIDTH - .unreq INPUT_DATA - .unreq OUTPUT_DATA_PTR - .unreq OUTPUT_DATA - - .unreq OUTPTR - .unreq INPTR - .unreq WIDTH - .unreq TMP - -.purgem upsample16 -.purgem upsample32 -.purgem upsample_row - - -/*****************************************************************************/ - -/* - * GLOBAL(JOCTET *) - * jsimd_huff_encode_one_block(working_state *state, JOCTET *buffer, - * JCOEFPTR block, int last_dc_val, - * c_derived_tbl *dctbl, c_derived_tbl *actbl) - * - */ - -.macro emit_byte BUFFER, PUT_BUFFER, PUT_BITS, ZERO, TMP - sub \PUT_BITS, \PUT_BITS, #0x8 - lsr \TMP, \PUT_BUFFER, \PUT_BITS - uxtb \TMP, \TMP - strb \TMP, [\BUFFER, #1]! - cmp \TMP, #0xff - /*it eq*/ - strbeq \ZERO, [\BUFFER, #1]! -.endm - -.macro put_bits PUT_BUFFER, PUT_BITS, CODE, SIZE - /*lsl \PUT_BUFFER, \PUT_BUFFER, \SIZE*/ - add \PUT_BITS, \SIZE - /*orr \PUT_BUFFER, \PUT_BUFFER, \CODE*/ - orr \PUT_BUFFER, \CODE, \PUT_BUFFER, lsl \SIZE -.endm - -.macro checkbuf15 BUFFER, PUT_BUFFER, PUT_BITS, ZERO, TMP - cmp \PUT_BITS, #0x10 - blt 15f - eor \ZERO, \ZERO, \ZERO - emit_byte \BUFFER, \PUT_BUFFER, \PUT_BITS, \ZERO, \TMP - emit_byte \BUFFER, \PUT_BUFFER, \PUT_BITS, \ZERO, \TMP -15: -.endm - -.balign 16 -jsimd_huff_encode_one_block_neon_consts: - .byte 0x01 - .byte 0x02 - .byte 0x04 - .byte 0x08 - .byte 0x10 - .byte 0x20 - .byte 0x40 - .byte 0x80 - -asm_function jsimd_huff_encode_one_block_neon - push {r4, r5, r6, r7, r8, r9, r10, r11, lr} - add r7, sp, #0x1c - sub r4, sp, #0x40 - bfc r4, #0, #5 - mov sp, r4 /* align sp on 32 bytes */ - vst1.64 {d8, d9, d10, d11}, [r4, :128]! - vst1.64 {d12, d13, d14, d15}, [r4, :128] - sub sp, #0x140 /* reserve 320 bytes */ - str r0, [sp, #0x18] /* working state > sp + Ox18 */ - add r4, sp, #0x20 /* r4 = t1 */ - ldr lr, [r7, #0x8] /* lr = dctbl */ - sub r10, r1, #0x1 /* r10=buffer-- */ - ldrsh r1, [r2] - mov r9, #0x10 - mov r8, #0x1 - adr r5, jsimd_huff_encode_one_block_neon_consts - /* prepare data */ - vld1.8 {d26}, [r5, :64] - veor q8, q8, q8 - veor q9, q9, q9 - vdup.16 q14, r9 - vdup.16 q15, r8 - veor q10, q10, q10 - veor q11, q11, q11 - sub r1, r1, r3 - add r9, r2, #0x22 - add r8, r2, #0x18 - add r3, r2, #0x36 - vmov.16 d0[0], r1 - vld1.16 {d2[0]}, [r9, :16] - vld1.16 {d4[0]}, [r8, :16] - vld1.16 {d6[0]}, [r3, :16] - add r1, r2, #0x2 - add r9, r2, #0x30 - add r8, r2, #0x26 - add r3, r2, #0x28 - vld1.16 {d0[1]}, [r1, :16] - vld1.16 {d2[1]}, [r9, :16] - vld1.16 {d4[1]}, [r8, :16] - vld1.16 {d6[1]}, [r3, :16] - add r1, r2, #0x10 - add r9, r2, #0x40 - add r8, r2, #0x34 - add r3, r2, #0x1a - vld1.16 {d0[2]}, [r1, :16] - vld1.16 {d2[2]}, [r9, :16] - vld1.16 {d4[2]}, [r8, :16] - vld1.16 {d6[2]}, [r3, :16] - add r1, r2, #0x20 - add r9, r2, #0x32 - add r8, r2, #0x42 - add r3, r2, #0xc - vld1.16 {d0[3]}, [r1, :16] - vld1.16 {d2[3]}, [r9, :16] - vld1.16 {d4[3]}, [r8, :16] - vld1.16 {d6[3]}, [r3, :16] - add r1, r2, #0x12 - add r9, r2, #0x24 - add r8, r2, #0x50 - add r3, r2, #0xe - vld1.16 {d1[0]}, [r1, :16] - vld1.16 {d3[0]}, [r9, :16] - vld1.16 {d5[0]}, [r8, :16] - vld1.16 {d7[0]}, [r3, :16] - add r1, r2, #0x4 - add r9, r2, #0x16 - add r8, r2, #0x60 - add r3, r2, #0x1c - vld1.16 {d1[1]}, [r1, :16] - vld1.16 {d3[1]}, [r9, :16] - vld1.16 {d5[1]}, [r8, :16] - vld1.16 {d7[1]}, [r3, :16] - add r1, r2, #0x6 - add r9, r2, #0x8 - add r8, r2, #0x52 - add r3, r2, #0x2a - vld1.16 {d1[2]}, [r1, :16] - vld1.16 {d3[2]}, [r9, :16] - vld1.16 {d5[2]}, [r8, :16] - vld1.16 {d7[2]}, [r3, :16] - add r1, r2, #0x14 - add r9, r2, #0xa - add r8, r2, #0x44 - add r3, r2, #0x38 - vld1.16 {d1[3]}, [r1, :16] - vld1.16 {d3[3]}, [r9, :16] - vld1.16 {d5[3]}, [r8, :16] - vld1.16 {d7[3]}, [r3, :16] - vcgt.s16 q8, q8, q0 - vcgt.s16 q9, q9, q1 - vcgt.s16 q10, q10, q2 - vcgt.s16 q11, q11, q3 - vabs.s16 q0, q0 - vabs.s16 q1, q1 - vabs.s16 q2, q2 - vabs.s16 q3, q3 - veor q8, q8, q0 - veor q9, q9, q1 - veor q10, q10, q2 - veor q11, q11, q3 - add r9, r4, #0x20 - add r8, r4, #0x80 - add r3, r4, #0xa0 - vclz.i16 q0, q0 - vclz.i16 q1, q1 - vclz.i16 q2, q2 - vclz.i16 q3, q3 - vsub.i16 q0, q14, q0 - vsub.i16 q1, q14, q1 - vsub.i16 q2, q14, q2 - vsub.i16 q3, q14, q3 - vst1.16 {d0, d1, d2, d3}, [r4, :256] - vst1.16 {d4, d5, d6, d7}, [r9, :256] - vshl.s16 q0, q15, q0 - vshl.s16 q1, q15, q1 - vshl.s16 q2, q15, q2 - vshl.s16 q3, q15, q3 - vsub.i16 q0, q0, q15 - vsub.i16 q1, q1, q15 - vsub.i16 q2, q2, q15 - vsub.i16 q3, q3, q15 - vand q8, q8, q0 - vand q9, q9, q1 - vand q10, q10, q2 - vand q11, q11, q3 - vst1.16 {d16, d17, d18, d19}, [r8, :256] - vst1.16 {d20, d21, d22, d23}, [r3, :256] - add r1, r2, #0x46 - add r9, r2, #0x3a - add r8, r2, #0x74 - add r3, r2, #0x6a - vld1.16 {d8[0]}, [r1, :16] - vld1.16 {d10[0]}, [r9, :16] - vld1.16 {d12[0]}, [r8, :16] - vld1.16 {d14[0]}, [r3, :16] - veor q8, q8, q8 - veor q9, q9, q9 - veor q10, q10, q10 - veor q11, q11, q11 - add r1, r2, #0x54 - add r9, r2, #0x2c - add r8, r2, #0x76 - add r3, r2, #0x78 - vld1.16 {d8[1]}, [r1, :16] - vld1.16 {d10[1]}, [r9, :16] - vld1.16 {d12[1]}, [r8, :16] - vld1.16 {d14[1]}, [r3, :16] - add r1, r2, #0x62 - add r9, r2, #0x1e - add r8, r2, #0x68 - add r3, r2, #0x7a - vld1.16 {d8[2]}, [r1, :16] - vld1.16 {d10[2]}, [r9, :16] - vld1.16 {d12[2]}, [r8, :16] - vld1.16 {d14[2]}, [r3, :16] - add r1, r2, #0x70 - add r9, r2, #0x2e - add r8, r2, #0x5a - add r3, r2, #0x6c - vld1.16 {d8[3]}, [r1, :16] - vld1.16 {d10[3]}, [r9, :16] - vld1.16 {d12[3]}, [r8, :16] - vld1.16 {d14[3]}, [r3, :16] - add r1, r2, #0x72 - add r9, r2, #0x3c - add r8, r2, #0x4c - add r3, r2, #0x5e - vld1.16 {d9[0]}, [r1, :16] - vld1.16 {d11[0]}, [r9, :16] - vld1.16 {d13[0]}, [r8, :16] - vld1.16 {d15[0]}, [r3, :16] - add r1, r2, #0x64 - add r9, r2, #0x4a - add r8, r2, #0x3e - add r3, r2, #0x6e - vld1.16 {d9[1]}, [r1, :16] - vld1.16 {d11[1]}, [r9, :16] - vld1.16 {d13[1]}, [r8, :16] - vld1.16 {d15[1]}, [r3, :16] - add r1, r2, #0x56 - add r9, r2, #0x58 - add r8, r2, #0x4e - add r3, r2, #0x7c - vld1.16 {d9[2]}, [r1, :16] - vld1.16 {d11[2]}, [r9, :16] - vld1.16 {d13[2]}, [r8, :16] - vld1.16 {d15[2]}, [r3, :16] - add r1, r2, #0x48 - add r9, r2, #0x66 - add r8, r2, #0x5c - add r3, r2, #0x7e - vld1.16 {d9[3]}, [r1, :16] - vld1.16 {d11[3]}, [r9, :16] - vld1.16 {d13[3]}, [r8, :16] - vld1.16 {d15[3]}, [r3, :16] - vcgt.s16 q8, q8, q4 - vcgt.s16 q9, q9, q5 - vcgt.s16 q10, q10, q6 - vcgt.s16 q11, q11, q7 - vabs.s16 q4, q4 - vabs.s16 q5, q5 - vabs.s16 q6, q6 - vabs.s16 q7, q7 - veor q8, q8, q4 - veor q9, q9, q5 - veor q10, q10, q6 - veor q11, q11, q7 - add r1, r4, #0x40 - add r9, r4, #0x60 - add r8, r4, #0xc0 - add r3, r4, #0xe0 - vclz.i16 q4, q4 - vclz.i16 q5, q5 - vclz.i16 q6, q6 - vclz.i16 q7, q7 - vsub.i16 q4, q14, q4 - vsub.i16 q5, q14, q5 - vsub.i16 q6, q14, q6 - vsub.i16 q7, q14, q7 - vst1.16 {d8, d9, d10, d11}, [r1, :256] - vst1.16 {d12, d13, d14, d15}, [r9, :256] - vshl.s16 q4, q15, q4 - vshl.s16 q5, q15, q5 - vshl.s16 q6, q15, q6 - vshl.s16 q7, q15, q7 - vsub.i16 q4, q4, q15 - vsub.i16 q5, q5, q15 - vsub.i16 q6, q6, q15 - vsub.i16 q7, q7, q15 - vand q8, q8, q4 - vand q9, q9, q5 - vand q10, q10, q6 - vand q11, q11, q7 - vst1.16 {d16, d17, d18, d19}, [r8, :256] - vst1.16 {d20, d21, d22, d23}, [r3, :256] - ldr r12, [r7, #0xc] /* r12 = actbl */ - add r1, lr, #0x400 /* r1 = dctbl->ehufsi */ - mov r9, r12 /* r9 = actbl */ - add r6, r4, #0x80 /* r6 = t2 */ - ldr r11, [r0, #0x8] /* r11 = put_buffer */ - ldr r4, [r0, #0xc] /* r4 = put_bits */ - ldrh r2, [r6, #-128] /* r2 = nbits */ - ldrh r3, [r6] /* r3 = temp2 & (((JLONG)1)<ehufsi */ - ldrsb r6, [r5, #0xf0] /* r6 = actbl->ehufsi[0xf0] */ - veor q8, q8, q8 - vceq.i16 q0, q0, q8 - vceq.i16 q1, q1, q8 - vceq.i16 q2, q2, q8 - vceq.i16 q3, q3, q8 - vceq.i16 q4, q4, q8 - vceq.i16 q5, q5, q8 - vceq.i16 q6, q6, q8 - vceq.i16 q7, q7, q8 - vmovn.i16 d0, q0 - vmovn.i16 d2, q1 - vmovn.i16 d4, q2 - vmovn.i16 d6, q3 - vmovn.i16 d8, q4 - vmovn.i16 d10, q5 - vmovn.i16 d12, q6 - vmovn.i16 d14, q7 - vand d0, d0, d26 - vand d2, d2, d26 - vand d4, d4, d26 - vand d6, d6, d26 - vand d8, d8, d26 - vand d10, d10, d26 - vand d12, d12, d26 - vand d14, d14, d26 - vpadd.i8 d0, d0, d2 - vpadd.i8 d4, d4, d6 - vpadd.i8 d8, d8, d10 - vpadd.i8 d12, d12, d14 - vpadd.i8 d0, d0, d4 - vpadd.i8 d8, d8, d12 - vpadd.i8 d0, d0, d8 - vmov.32 r1, d0[1] - vmov.32 r8, d0[0] - mvn r1, r1 - mvn r8, r8 - lsrs r1, r1, #0x1 - rrx r8, r8 /* shift in last r1 bit while shifting out DC bit */ - rbit r1, r1 /* r1 = index1 */ - rbit r8, r8 /* r8 = index0 */ - ldr r0, [r9, #0x3c0] /* r0 = actbl->ehufco[0xf0] */ - str r1, [sp, #0x14] /* index1 > sp + 0x14 */ - cmp r8, #0x0 - beq 6f -1: - clz r2, r8 - add lr, lr, r2, lsl #1 - lsl r8, r8, r2 - ldrh r1, [lr, #-126] -2: - cmp r2, #0x10 - blt 3f - sub r2, r2, #0x10 - put_bits r11, r4, r0, r6 - cmp r4, #0x10 - blt 2b - eor r3, r3, r3 - emit_byte r10, r11, r4, r3, r12 - emit_byte r10, r11, r4, r3, r12 - b 2b -3: - add r2, r1, r2, lsl #4 - ldrh r3, [lr, #2]! - ldr r12, [r9, r2, lsl #2] - ldrb r2, [r5, r2] - put_bits r11, r4, r12, r2 - checkbuf15 r10, r11, r4, r2, r12 - put_bits r11, r4, r3, r1 - checkbuf15 r10, r11, r4, r2, r12 - lsls r8, r8, #0x1 - bne 1b -6: - add r12, sp, #0x20 /* r12 = t1 */ - ldr r8, [sp, #0x14] /* r8 = index1 */ - adds r12, #0xc0 /* r12 = t2 + (DCTSIZE2/2) */ - cmp r8, #0x0 - beq 6f - clz r2, r8 - sub r12, r12, lr - lsl r8, r8, r2 - add r2, r2, r12, lsr #1 - add lr, lr, r2, lsl #1 - b 7f -1: - clz r2, r8 - add lr, lr, r2, lsl #1 - lsl r8, r8, r2 -7: - ldrh r1, [lr, #-126] -2: - cmp r2, #0x10 - blt 3f - sub r2, r2, #0x10 - put_bits r11, r4, r0, r6 - cmp r4, #0x10 - blt 2b - eor r3, r3, r3 - emit_byte r10, r11, r4, r3, r12 - emit_byte r10, r11, r4, r3, r12 - b 2b -3: - add r2, r1, r2, lsl #4 - ldrh r3, [lr, #2]! - ldr r12, [r9, r2, lsl #2] - ldrb r2, [r5, r2] - put_bits r11, r4, r12, r2 - checkbuf15 r10, r11, r4, r2, r12 - put_bits r11, r4, r3, r1 - checkbuf15 r10, r11, r4, r2, r12 - lsls r8, r8, #0x1 - bne 1b -6: - add r0, sp, #0x20 - add r0, #0xfe - cmp lr, r0 - bhs 1f - ldr r1, [r9] - ldrb r0, [r5] - put_bits r11, r4, r1, r0 - checkbuf15 r10, r11, r4, r0, r1 -1: - ldr r12, [sp, #0x18] - str r11, [r12, #0x8] - str r4, [r12, #0xc] - add r0, r10, #0x1 - add r4, sp, #0x140 - vld1.64 {d8, d9, d10, d11}, [r4, :128]! - vld1.64 {d12, d13, d14, d15}, [r4, :128] - sub r4, r7, #0x1c - mov sp, r4 - pop {r4, r5, r6, r7, r8, r9, r10, r11, pc} - -.purgem emit_byte -.purgem put_bits -.purgem checkbuf15 diff --git a/third-party/mozjpeg/mozjpeg/simd/arm/neon-compat.h.in b/third-party/mozjpeg/mozjpeg/simd/arm/neon-compat.h.in new file mode 100644 index 00000000000..d403f2289fc --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/arm/neon-compat.h.in @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020, D. R. Commander. All Rights Reserved. + * Copyright (C) 2020-2021, Arm Limited. All Rights Reserved. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#cmakedefine HAVE_VLD1_S16_X3 +#cmakedefine HAVE_VLD1_U16_X2 +#cmakedefine HAVE_VLD1Q_U8_X4 + +/* Define compiler-independent count-leading-zeros and byte-swap macros */ +#if defined(_MSC_VER) && !defined(__clang__) +#define BUILTIN_CLZ(x) _CountLeadingZeros(x) +#define BUILTIN_CLZLL(x) _CountLeadingZeros64(x) +#define BUILTIN_BSWAP64(x) _byteswap_uint64(x) +#elif defined(__clang__) || defined(__GNUC__) +#define BUILTIN_CLZ(x) __builtin_clz(x) +#define BUILTIN_CLZLL(x) __builtin_clzll(x) +#define BUILTIN_BSWAP64(x) __builtin_bswap64(x) +#else +#error "Unknown compiler" +#endif diff --git a/third-party/mozjpeg/mozjpeg/simd/gas-preprocessor.in b/third-party/mozjpeg/mozjpeg/simd/gas-preprocessor.in deleted file mode 100755 index 560f788b554..00000000000 --- a/third-party/mozjpeg/mozjpeg/simd/gas-preprocessor.in +++ /dev/null @@ -1 +0,0 @@ -gas-preprocessor.pl @CMAKE_ASM_COMPILER@ ${1+"$@"} diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jchuff-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jchuff-sse2.asm index 79f0ca52cc7..278cf5e83af 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jchuff-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jchuff-sse2.asm @@ -1,8 +1,9 @@ ; ; jchuff-sse2.asm - Huffman entropy encoding (SSE2) ; -; Copyright (C) 2009-2011, 2014-2017, D. R. Commander. +; Copyright (C) 2009-2011, 2014-2017, 2019, D. R. Commander. ; Copyright (C) 2015, Matthieu Darbois. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -15,134 +16,255 @@ ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; ; This file contains an SSE2 implementation for Huffman coding of one block. -; The following code is based directly on jchuff.c; see jchuff.c for more -; details. +; The following code is based on jchuff.c; see jchuff.c for more details. %include "jsimdext.inc" +struc working_state +.next_output_byte: resp 1 ; => next byte to write in buffer +.free_in_buffer: resp 1 ; # of byte spaces remaining in buffer +.cur.put_buffer.simd resq 1 ; current bit accumulation buffer +.cur.free_bits resd 1 ; # of bits available in it +.cur.last_dc_val resd 4 ; last DC coef for each component +.cinfo: resp 1 ; dump_buffer needs access to this +endstruc + +struc c_derived_tbl +.ehufco: resd 256 ; code for each symbol +.ehufsi: resb 256 ; length of code for each symbol +; If no code has been allocated for a symbol S, ehufsi[S] contains 0 +endstruc + ; -------------------------------------------------------------------------- SECTION SEG_CONST - alignz 32 GLOBAL_DATA(jconst_huff_encode_one_block) EXTN(jconst_huff_encode_one_block): -%include "jpeg_nbits_table.inc" + alignz 32 + +jpeg_mask_bits dq 0x0000, 0x0001, 0x0003, 0x0007 + dq 0x000f, 0x001f, 0x003f, 0x007f + dq 0x00ff, 0x01ff, 0x03ff, 0x07ff + dq 0x0fff, 0x1fff, 0x3fff, 0x7fff + +times 1 << 14 db 15 +times 1 << 13 db 14 +times 1 << 12 db 13 +times 1 << 11 db 12 +times 1 << 10 db 11 +times 1 << 9 db 10 +times 1 << 8 db 9 +times 1 << 7 db 8 +times 1 << 6 db 7 +times 1 << 5 db 6 +times 1 << 4 db 5 +times 1 << 3 db 4 +times 1 << 2 db 3 +times 1 << 1 db 2 +times 1 << 0 db 1 +times 1 db 0 +jpeg_nbits_table: +times 1 db 0 +times 1 << 0 db 1 +times 1 << 1 db 2 +times 1 << 2 db 3 +times 1 << 3 db 4 +times 1 << 4 db 5 +times 1 << 5 db 6 +times 1 << 6 db 7 +times 1 << 7 db 8 +times 1 << 8 db 9 +times 1 << 9 db 10 +times 1 << 10 db 11 +times 1 << 11 db 12 +times 1 << 12 db 13 +times 1 << 13 db 14 +times 1 << 14 db 15 alignz 32 +%ifdef PIC +%define NBITS(x) nbits_base + x +%else +%define NBITS(x) jpeg_nbits_table + x +%endif +%define MASK_BITS(x) NBITS((x) * 8) + (jpeg_mask_bits - jpeg_nbits_table) + ; -------------------------------------------------------------------------- SECTION SEG_TEXT BITS 32 -; These macros perform the same task as the emit_bits() function in the -; original libjpeg code. In addition to reducing overhead by explicitly -; inlining the code, additional performance is achieved by taking into -; account the size of the bit buffer and waiting until it is almost full -; before emptying it. This mostly benefits 64-bit platforms, since 6 -; bytes can be stored in a 64-bit bit buffer before it has to be emptied. - -%macro EMIT_BYTE 0 - sub put_bits, 8 ; put_bits -= 8; - mov edx, put_buffer - mov ecx, put_bits - shr edx, cl ; c = (JOCTET)GETJOCTET(put_buffer >> put_bits); - mov byte [eax], dl ; *buffer++ = c; - add eax, 1 - cmp dl, 0xFF ; need to stuff a zero byte? - jne %%.EMIT_BYTE_END - mov byte [eax], 0 ; *buffer++ = 0; - add eax, 1 -%%.EMIT_BYTE_END: -%endmacro - -%macro PUT_BITS 1 - add put_bits, ecx ; put_bits += size; - shl put_buffer, cl ; put_buffer = (put_buffer << size); - or put_buffer, %1 +%define mm_put_buffer mm0 +%define mm_all_0xff mm1 +%define mm_temp mm2 +%define mm_nbits mm3 +%define mm_code_bits mm3 +%define mm_code mm4 +%define mm_overflow_bits mm5 +%define mm_save_nbits mm6 + +; Shorthand used to describe SIMD operations: +; wN: xmmN treated as eight signed 16-bit values +; wN[i]: perform the same operation on all eight signed 16-bit values, i=0..7 +; bN: xmmN treated as 16 unsigned 8-bit values, or +; mmN treated as eight unsigned 8-bit values +; bN[i]: perform the same operation on all unsigned 8-bit values, +; i=0..15 (SSE register) or i=0..7 (MMX register) +; Contents of SIMD registers are shown in memory order. + +; Fill the bit buffer to capacity with the leading bits from code, then output +; the bit buffer and put the remaining bits from code into the bit buffer. +; +; Usage: +; code - contains the bits to shift into the bit buffer (LSB-aligned) +; %1 - temp register +; %2 - low byte of temp register +; %3 - second byte of temp register +; %4-%8 (optional) - extra instructions to execute before the macro completes +; %9 - the label to which to jump when the macro completes +; +; Upon completion, free_bits will be set to the number of remaining bits from +; code, and put_buffer will contain those remaining bits. temp and code will +; be clobbered. +; +; This macro encodes any 0xFF bytes as 0xFF 0x00, as does the EMIT_BYTE() +; macro in jchuff.c. + +%macro EMIT_QWORD 9 +%define %%temp %1 +%define %%tempb %2 +%define %%temph %3 + add nbits, free_bits ; nbits += free_bits; + neg free_bits ; free_bits = -free_bits; + movq mm_temp, mm_code ; temp = code; + movd mm_nbits, nbits ; nbits --> MMX register + movd mm_overflow_bits, free_bits ; overflow_bits (temp register) = free_bits; + neg free_bits ; free_bits = -free_bits; + psllq mm_put_buffer, mm_nbits ; put_buffer <<= nbits; + psrlq mm_temp, mm_overflow_bits ; temp >>= overflow_bits; + add free_bits, 64 ; free_bits += 64; + por mm_temp, mm_put_buffer ; temp |= put_buffer; +%ifidn %%temp, nbits_base + movd mm_save_nbits, nbits_base ; save nbits_base +%endif + movq mm_code_bits, mm_temp ; code_bits (temp register) = temp; + movq mm_put_buffer, mm_code ; put_buffer = code; + pcmpeqb mm_temp, mm_all_0xff ; b_temp[i] = (b_temp[i] == 0xFF ? 0xFF : 0); + movq mm_code, mm_code_bits ; code = code_bits; + psrlq mm_code_bits, 32 ; code_bits >>= 32; + pmovmskb nbits, mm_temp ; nbits = 0; nbits |= ((b_temp[i] >> 7) << i); + movd %%temp, mm_code_bits ; temp = code_bits; + bswap %%temp ; temp = htonl(temp); + test nbits, nbits ; if (nbits != 0) /* Some 0xFF bytes */ + jnz %%.SLOW ; goto %%.SLOW + mov dword [buffer], %%temp ; *(uint32_t)buffer = temp; +%ifidn %%temp, nbits_base + movd nbits_base, mm_save_nbits ; restore nbits_base +%endif + %4 + movd nbits, mm_code ; nbits = (uint32_t)(code); + %5 + bswap nbits ; nbits = htonl(nbits); + mov dword [buffer + 4], nbits ; *(uint32_t)(buffer + 4) = nbits; + lea buffer, [buffer + 8] ; buffer += 8; + %6 + %7 + %8 + jmp %9 ; return +%%.SLOW: + ; Execute the equivalent of the EMIT_BYTE() macro in jchuff.c for all 8 + ; bytes in the qword. + mov byte [buffer], %%tempb ; buffer[0] = temp[0]; + cmp %%tempb, 0xFF ; Set CF if temp[0] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (temp[0] < 0xFF ? 1 : 0)); + mov byte [buffer], %%temph ; buffer[0] = temp[1]; + cmp %%temph, 0xFF ; Set CF if temp[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (temp[1] < 0xFF ? 1 : 0)); + shr %%temp, 16 ; temp >>= 16; + mov byte [buffer], %%tempb ; buffer[0] = temp[0]; + cmp %%tempb, 0xFF ; Set CF if temp[0] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (temp[0] < 0xFF ? 1 : 0)); + mov byte [buffer], %%temph ; buffer[0] = temp[1]; + cmp %%temph, 0xFF ; Set CF if temp[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (temp[1] < 0xFF ? 1 : 0)); + movd nbits, mm_code ; nbits (temp register) = (uint32_t)(code) +%ifidn %%temp, nbits_base + movd nbits_base, mm_save_nbits ; restore nbits_base +%endif + bswap nbits ; nbits = htonl(nbits) + mov byte [buffer], nbitsb ; buffer[0] = nbits[0]; + cmp nbitsb, 0xFF ; Set CF if nbits[0] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (nbits[0] < 0xFF ? 1 : 0)); + mov byte [buffer], nbitsh ; buffer[0] = nbits[1]; + cmp nbitsh, 0xFF ; Set CF if nbits[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (nbits[1] < 0xFF ? 1 : 0)); + shr nbits, 16 ; nbits >>= 16; + mov byte [buffer], nbitsb ; buffer[0] = nbits[0]; + cmp nbitsb, 0xFF ; Set CF if nbits[0] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (nbits[0] < 0xFF ? 1 : 0)); + mov byte [buffer], nbitsh ; buffer[0] = nbits[1]; + %4 + cmp nbitsh, 0xFF ; Set CF if nbits[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb buffer, -2 ; buffer -= (-2 + (nbits[1] < 0xFF ? 1 : 0)); + %5 + %6 + %7 + %8 + jmp %9 ; return; %endmacro -%macro CHECKBUF15 0 - cmp put_bits, 16 ; if (put_bits > 31) { - jl %%.CHECKBUF15_END - mov eax, POINTER [esp+buffer] - EMIT_BYTE - EMIT_BYTE - mov POINTER [esp+buffer], eax -%%.CHECKBUF15_END: +%macro PUSH 1 + push %1 +%assign stack_offset stack_offset + 4 %endmacro -%macro EMIT_BITS 1 - PUT_BITS %1 - CHECKBUF15 +%macro POP 1 + pop %1 +%assign stack_offset stack_offset - 4 %endmacro -%macro kloop_prepare 37 ;(ko, jno0, ..., jno31, xmm0, xmm1, xmm2, xmm3) - pxor xmm4, xmm4 ; __m128i neg = _mm_setzero_si128(); - pxor xmm5, xmm5 ; __m128i neg = _mm_setzero_si128(); - pxor xmm6, xmm6 ; __m128i neg = _mm_setzero_si128(); - pxor xmm7, xmm7 ; __m128i neg = _mm_setzero_si128(); - pinsrw %34, word [esi + %2 * SIZEOF_WORD], 0 ; xmm_shadow[0] = block[jno0]; - pinsrw %35, word [esi + %10 * SIZEOF_WORD], 0 ; xmm_shadow[8] = block[jno8]; - pinsrw %36, word [esi + %18 * SIZEOF_WORD], 0 ; xmm_shadow[16] = block[jno16]; - pinsrw %37, word [esi + %26 * SIZEOF_WORD], 0 ; xmm_shadow[24] = block[jno24]; - pinsrw %34, word [esi + %3 * SIZEOF_WORD], 1 ; xmm_shadow[1] = block[jno1]; - pinsrw %35, word [esi + %11 * SIZEOF_WORD], 1 ; xmm_shadow[9] = block[jno9]; - pinsrw %36, word [esi + %19 * SIZEOF_WORD], 1 ; xmm_shadow[17] = block[jno17]; - pinsrw %37, word [esi + %27 * SIZEOF_WORD], 1 ; xmm_shadow[25] = block[jno25]; - pinsrw %34, word [esi + %4 * SIZEOF_WORD], 2 ; xmm_shadow[2] = block[jno2]; - pinsrw %35, word [esi + %12 * SIZEOF_WORD], 2 ; xmm_shadow[10] = block[jno10]; - pinsrw %36, word [esi + %20 * SIZEOF_WORD], 2 ; xmm_shadow[18] = block[jno18]; - pinsrw %37, word [esi + %28 * SIZEOF_WORD], 2 ; xmm_shadow[26] = block[jno26]; - pinsrw %34, word [esi + %5 * SIZEOF_WORD], 3 ; xmm_shadow[3] = block[jno3]; - pinsrw %35, word [esi + %13 * SIZEOF_WORD], 3 ; xmm_shadow[11] = block[jno11]; - pinsrw %36, word [esi + %21 * SIZEOF_WORD], 3 ; xmm_shadow[19] = block[jno19]; - pinsrw %37, word [esi + %29 * SIZEOF_WORD], 3 ; xmm_shadow[27] = block[jno27]; - pinsrw %34, word [esi + %6 * SIZEOF_WORD], 4 ; xmm_shadow[4] = block[jno4]; - pinsrw %35, word [esi + %14 * SIZEOF_WORD], 4 ; xmm_shadow[12] = block[jno12]; - pinsrw %36, word [esi + %22 * SIZEOF_WORD], 4 ; xmm_shadow[20] = block[jno20]; - pinsrw %37, word [esi + %30 * SIZEOF_WORD], 4 ; xmm_shadow[28] = block[jno28]; - pinsrw %34, word [esi + %7 * SIZEOF_WORD], 5 ; xmm_shadow[5] = block[jno5]; - pinsrw %35, word [esi + %15 * SIZEOF_WORD], 5 ; xmm_shadow[13] = block[jno13]; - pinsrw %36, word [esi + %23 * SIZEOF_WORD], 5 ; xmm_shadow[21] = block[jno21]; - pinsrw %37, word [esi + %31 * SIZEOF_WORD], 5 ; xmm_shadow[29] = block[jno29]; - pinsrw %34, word [esi + %8 * SIZEOF_WORD], 6 ; xmm_shadow[6] = block[jno6]; - pinsrw %35, word [esi + %16 * SIZEOF_WORD], 6 ; xmm_shadow[14] = block[jno14]; - pinsrw %36, word [esi + %24 * SIZEOF_WORD], 6 ; xmm_shadow[22] = block[jno22]; - pinsrw %37, word [esi + %32 * SIZEOF_WORD], 6 ; xmm_shadow[30] = block[jno30]; - pinsrw %34, word [esi + %9 * SIZEOF_WORD], 7 ; xmm_shadow[7] = block[jno7]; - pinsrw %35, word [esi + %17 * SIZEOF_WORD], 7 ; xmm_shadow[15] = block[jno15]; - pinsrw %36, word [esi + %25 * SIZEOF_WORD], 7 ; xmm_shadow[23] = block[jno23]; -%if %1 != 32 - pinsrw %37, word [esi + %33 * SIZEOF_WORD], 7 ; xmm_shadow[31] = block[jno31]; +; If PIC is defined, load the address of a symbol defined in this file into a +; register. Equivalent to +; get_GOT %1 +; lea %1, [GOTOFF(%1, %2)] +; without using the GOT. +; +; Usage: +; %1 - register into which to load the address of the symbol +; %2 - symbol whose address should be loaded +; %3 - optional multi-line macro to execute before the symbol address is loaded +; %4 - optional multi-line macro to execute after the symbol address is loaded +; +; If PIC is not defined, then %3 and %4 are executed in order. + +%macro GET_SYM 2-4 +%ifdef PIC + call %%.geteip +%%.ref: + %4 + add %1, %2 - %%.ref + jmp short %%.done + align 32 +%%.geteip: + %3 4 ; must adjust stack pointer because of call + mov %1, POINTER [esp] + ret + align 32 +%%.done: %else - pinsrw %37, ecx, 7 ; xmm_shadow[31] = block[jno31]; + %3 0 + %4 %endif - pcmpgtw xmm4, %34 ; neg = _mm_cmpgt_epi16(neg, x1); - pcmpgtw xmm5, %35 ; neg = _mm_cmpgt_epi16(neg, x1); - pcmpgtw xmm6, %36 ; neg = _mm_cmpgt_epi16(neg, x1); - pcmpgtw xmm7, %37 ; neg = _mm_cmpgt_epi16(neg, x1); - paddw %34, xmm4 ; x1 = _mm_add_epi16(x1, neg); - paddw %35, xmm5 ; x1 = _mm_add_epi16(x1, neg); - paddw %36, xmm6 ; x1 = _mm_add_epi16(x1, neg); - paddw %37, xmm7 ; x1 = _mm_add_epi16(x1, neg); - pxor %34, xmm4 ; x1 = _mm_xor_si128(x1, neg); - pxor %35, xmm5 ; x1 = _mm_xor_si128(x1, neg); - pxor %36, xmm6 ; x1 = _mm_xor_si128(x1, neg); - pxor %37, xmm7 ; x1 = _mm_xor_si128(x1, neg); - pxor xmm4, %34 ; neg = _mm_xor_si128(neg, x1); - pxor xmm5, %35 ; neg = _mm_xor_si128(neg, x1); - pxor xmm6, %36 ; neg = _mm_xor_si128(neg, x1); - pxor xmm7, %37 ; neg = _mm_xor_si128(neg, x1); - movdqa XMMWORD [esp + t1 + %1 * SIZEOF_WORD], %34 ; _mm_storeu_si128((__m128i *)(t1 + ko), x1); - movdqa XMMWORD [esp + t1 + (%1 + 8) * SIZEOF_WORD], %35 ; _mm_storeu_si128((__m128i *)(t1 + ko + 8), x1); - movdqa XMMWORD [esp + t1 + (%1 + 16) * SIZEOF_WORD], %36 ; _mm_storeu_si128((__m128i *)(t1 + ko + 16), x1); - movdqa XMMWORD [esp + t1 + (%1 + 24) * SIZEOF_WORD], %37 ; _mm_storeu_si128((__m128i *)(t1 + ko + 24), x1); - movdqa XMMWORD [esp + t2 + %1 * SIZEOF_WORD], xmm4 ; _mm_storeu_si128((__m128i *)(t2 + ko), neg); - movdqa XMMWORD [esp + t2 + (%1 + 8) * SIZEOF_WORD], xmm5 ; _mm_storeu_si128((__m128i *)(t2 + ko + 8), neg); - movdqa XMMWORD [esp + t2 + (%1 + 16) * SIZEOF_WORD], xmm6 ; _mm_storeu_si128((__m128i *)(t2 + ko + 16), neg); - movdqa XMMWORD [esp + t2 + (%1 + 24) * SIZEOF_WORD], xmm7 ; _mm_storeu_si128((__m128i *)(t2 + ko + 24), neg); %endmacro ; @@ -153,272 +275,487 @@ EXTN(jconst_huff_encode_one_block): ; JCOEFPTR block, int last_dc_val, ; c_derived_tbl *dctbl, c_derived_tbl *actbl) ; - -; eax + 8 = working_state *state -; eax + 12 = JOCTET *buffer -; eax + 16 = JCOEFPTR block -; eax + 20 = int last_dc_val -; eax + 24 = c_derived_tbl *dctbl -; eax + 28 = c_derived_tbl *actbl - -%define pad 6 * SIZEOF_DWORD ; Align to 16 bytes -%define t1 pad -%define t2 t1 + (DCTSIZE2 * SIZEOF_WORD) -%define block t2 + (DCTSIZE2 * SIZEOF_WORD) -%define actbl block + SIZEOF_DWORD -%define buffer actbl + SIZEOF_DWORD -%define temp buffer + SIZEOF_DWORD -%define temp2 temp + SIZEOF_DWORD -%define temp3 temp2 + SIZEOF_DWORD -%define temp4 temp3 + SIZEOF_DWORD -%define temp5 temp4 + SIZEOF_DWORD -%define gotptr temp5 + SIZEOF_DWORD ; void *gotptr -%define put_buffer ebx -%define put_bits edi +; Stack layout: +; Function args +; Return address +; Saved ebx +; Saved ebp +; Saved esi +; Saved edi <-- esp_save +; ... +; esp_save +; t_ 64*2 bytes (aligned to 128 bytes) +; +; esp is used (as t) to point into t_ (data in lower indices is not used once +; esp passes over them, so this is signal-safe.) Aligning to 128 bytes allows +; us to find the rest of the data again. +; +; NOTES: +; When shuffling data, we try to avoid pinsrw as much as possible, since it is +; slow on many CPUs. Its reciprocal throughput (issue latency) is 1 even on +; modern CPUs, so chains of pinsrw instructions (even with different outputs) +; can limit performance. pinsrw is a VectorPath instruction on AMD K8 and +; requires 2 µops (with memory operand) on Intel. In either case, only one +; pinsrw instruction can be decoded per cycle (and nothing else if they are +; back-to-back), so out-of-order execution cannot be used to work around long +; pinsrw chains (though for Sandy Bridge and later, this may be less of a +; problem if the code runs from the µop cache.) +; +; We use tzcnt instead of bsf without checking for support. The instruction is +; executed as bsf on CPUs that don't support tzcnt (encoding is equivalent to +; rep bsf.) The destination (first) operand of bsf (and tzcnt on some CPUs) is +; an input dependency (although the behavior is not formally defined, Intel +; CPUs usually leave the destination unmodified if the source is zero.) This +; can prevent out-of-order execution, so we clear the destination before +; invoking tzcnt. +; +; Initial register allocation +; eax - frame --> buffer +; ebx - nbits_base (PIC) / emit_temp +; ecx - dctbl --> size --> state +; edx - block --> nbits +; esi - code_temp --> state --> actbl +; edi - index_temp --> free_bits +; esp - t +; ebp - index + +%define frame eax +%ifdef PIC +%define nbits_base ebx +%endif +%define emit_temp ebx +%define emit_tempb bl +%define emit_temph bh +%define dctbl ecx +%define block edx +%define code_temp esi +%define index_temp edi +%define t esp +%define index ebp + +%assign save_frame DCTSIZE2 * SIZEOF_WORD + +; Step 1: Re-arrange input data according to jpeg_natural_order +; xx 01 02 03 04 05 06 07 xx 01 08 16 09 02 03 10 +; 08 09 10 11 12 13 14 15 17 24 32 25 18 11 04 05 +; 16 17 18 19 20 21 22 23 12 19 26 33 40 48 41 34 +; 24 25 26 27 28 29 30 31 ==> 27 20 13 06 07 14 21 28 +; 32 33 34 35 36 37 38 39 35 42 49 56 57 50 43 36 +; 40 41 42 43 44 45 46 47 29 22 15 23 30 37 44 51 +; 48 49 50 51 52 53 54 55 58 59 52 45 38 31 39 46 +; 56 57 58 59 60 61 62 63 53 60 61 54 47 55 62 63 align 32 GLOBAL_FUNCTION(jsimd_huff_encode_one_block_sse2) EXTN(jsimd_huff_encode_one_block_sse2): - push ebp - mov eax, esp ; eax = original ebp - sub esp, byte 4 - and esp, byte (-SIZEOF_XMMWORD) ; align to 128 bits - mov [esp], eax - mov ebp, esp ; ebp = aligned ebp - sub esp, temp5+9*SIZEOF_DWORD-pad - push ebx - push ecx -; push edx ; need not be preserved - push esi - push edi - push ebp - - mov esi, POINTER [eax+8] ; (working_state *state) - mov put_buffer, dword [esi+8] ; put_buffer = state->cur.put_buffer; - mov put_bits, dword [esi+12] ; put_bits = state->cur.put_bits; - push esi ; esi is now scratch - - get_GOT edx ; get GOT address - movpic POINTER [esp+gotptr], edx ; save GOT address - - mov ecx, POINTER [eax+28] - mov edx, POINTER [eax+16] - mov esi, POINTER [eax+12] - mov POINTER [esp+actbl], ecx - mov POINTER [esp+block], edx - mov POINTER [esp+buffer], esi - - ; Encode the DC coefficient difference per section F.1.2.1 - mov esi, POINTER [esp+block] ; block - movsx ecx, word [esi] ; temp = temp2 = block[0] - last_dc_val; - sub ecx, dword [eax+20] - mov esi, ecx - - ; This is a well-known technique for obtaining the absolute value - ; with out a branch. It is derived from an assembly language technique - ; presented in "How to Optimize for the Pentium Processors", - ; Copyright (c) 1996, 1997 by Agner Fog. - mov edx, ecx - sar edx, 31 ; temp3 = temp >> (CHAR_BIT * sizeof(int) - 1); - xor ecx, edx ; temp ^= temp3; - sub ecx, edx ; temp -= temp3; - - ; For a negative input, want temp2 = bitwise complement of abs(input) - ; This code assumes we are on a two's complement machine - add esi, edx ; temp2 += temp3; - mov dword [esp+temp], esi ; backup temp2 in temp - - ; Find the number of bits needed for the magnitude of the coefficient - movpic ebp, POINTER [esp+gotptr] ; load GOT address (ebp) - movzx edx, byte [GOTOFF(ebp, jpeg_nbits_table + ecx)] ; nbits = JPEG_NBITS(temp); - mov dword [esp+temp2], edx ; backup nbits in temp2 - - ; Emit the Huffman-coded symbol for the number of bits - mov ebp, POINTER [eax+24] ; After this point, arguments are not accessible anymore - mov eax, INT [ebp + edx * 4] ; code = dctbl->ehufco[nbits]; - movzx ecx, byte [ebp + edx + 1024] ; size = dctbl->ehufsi[nbits]; - EMIT_BITS eax ; EMIT_BITS(code, size) - - mov ecx, dword [esp+temp2] ; restore nbits - - ; Mask off any extra bits in code - mov eax, 1 - shl eax, cl - dec eax - and eax, dword [esp+temp] ; temp2 &= (((JLONG)1)<>= r; - mov dword [esp+temp3], edx -.BRLOOP: - cmp ecx, 16 ; while (r > 15) { - jl near .ERLOOP - sub ecx, 16 ; r -= 16; - mov dword [esp+temp], ecx - mov eax, INT [ebp + 240 * 4] ; code_0xf0 = actbl->ehufco[0xf0]; - movzx ecx, byte [ebp + 1024 + 240] ; size_0xf0 = actbl->ehufsi[0xf0]; - EMIT_BITS eax ; EMIT_BITS(code_0xf0, size_0xf0) - mov ecx, dword [esp+temp] - jmp .BRLOOP -.ERLOOP: - movsx eax, word [esi] ; temp = t1[k]; - movpic edx, POINTER [esp+gotptr] ; load GOT address (edx) - movzx eax, byte [GOTOFF(edx, jpeg_nbits_table + eax)] ; nbits = JPEG_NBITS(temp); - mov dword [esp+temp2], eax - ; Emit Huffman symbol for run length / number of bits - shl ecx, 4 ; temp3 = (r << 4) + nbits; - add ecx, eax - mov eax, INT [ebp + ecx * 4] ; code = actbl->ehufco[temp3]; - movzx ecx, byte [ebp + ecx + 1024] ; size = actbl->ehufsi[temp3]; - EMIT_BITS eax - - movsx edx, word [esi+DCTSIZE2*2] ; temp2 = t2[k]; - ; Mask off any extra bits in code - mov ecx, dword [esp+temp2] - mov eax, 1 - shl eax, cl - dec eax - and eax, edx ; temp2 &= (((JLONG)1)<>= 1; - - jmp .BLOOP -.ELOOP: - movdqa xmm0, XMMWORD [esp + t1 + 32 * SIZEOF_WORD] ; __m128i tmp0 = _mm_loadu_si128((__m128i *)(t1 + 0)); - movdqa xmm1, XMMWORD [esp + t1 + 40 * SIZEOF_WORD] ; __m128i tmp1 = _mm_loadu_si128((__m128i *)(t1 + 8)); - movdqa xmm2, XMMWORD [esp + t1 + 48 * SIZEOF_WORD] ; __m128i tmp2 = _mm_loadu_si128((__m128i *)(t1 + 16)); - movdqa xmm3, XMMWORD [esp + t1 + 56 * SIZEOF_WORD] ; __m128i tmp3 = _mm_loadu_si128((__m128i *)(t1 + 24)); - pcmpeqw xmm0, xmm7 ; tmp0 = _mm_cmpeq_epi16(tmp0, zero); - pcmpeqw xmm1, xmm7 ; tmp1 = _mm_cmpeq_epi16(tmp1, zero); - pcmpeqw xmm2, xmm7 ; tmp2 = _mm_cmpeq_epi16(tmp2, zero); - pcmpeqw xmm3, xmm7 ; tmp3 = _mm_cmpeq_epi16(tmp3, zero); - packsswb xmm0, xmm1 ; tmp0 = _mm_packs_epi16(tmp0, tmp1); - packsswb xmm2, xmm3 ; tmp2 = _mm_packs_epi16(tmp2, tmp3); - pmovmskb edx, xmm0 ; index = ((uint64_t)_mm_movemask_epi8(tmp0)) << 0; - pmovmskb ecx, xmm2 ; index = ((uint64_t)_mm_movemask_epi8(tmp2)) << 16; - shl ecx, 16 - or edx, ecx - not edx ; index = ~index; - - lea eax, [esp + t1 + (DCTSIZE2/2) * 2] - sub eax, esi - shr eax, 1 - bsf ecx, edx ; r = __builtin_ctzl(index); - jz near .ELOOP2 - shr edx, cl ; index >>= r; - add ecx, eax - lea esi, [esi+ecx*2] ; k += r; - mov dword [esp+temp3], edx - jmp .BRLOOP2 -.BLOOP2: - bsf ecx, edx ; r = __builtin_ctzl(index); - jz near .ELOOP2 - lea esi, [esi+ecx*2] ; k += r; - shr edx, cl ; index >>= r; - mov dword [esp+temp3], edx -.BRLOOP2: - cmp ecx, 16 ; while (r > 15) { - jl near .ERLOOP2 - sub ecx, 16 ; r -= 16; - mov dword [esp+temp], ecx - mov eax, INT [ebp + 240 * 4] ; code_0xf0 = actbl->ehufco[0xf0]; - movzx ecx, byte [ebp + 1024 + 240] ; size_0xf0 = actbl->ehufsi[0xf0]; - EMIT_BITS eax ; EMIT_BITS(code_0xf0, size_0xf0) - mov ecx, dword [esp+temp] - jmp .BRLOOP2 -.ERLOOP2: - movsx eax, word [esi] ; temp = t1[k]; - bsr eax, eax ; nbits = 32 - __builtin_clz(temp); - inc eax - mov dword [esp+temp2], eax - ; Emit Huffman symbol for run length / number of bits - shl ecx, 4 ; temp3 = (r << 4) + nbits; - add ecx, eax - mov eax, INT [ebp + ecx * 4] ; code = actbl->ehufco[temp3]; - movzx ecx, byte [ebp + ecx + 1024] ; size = actbl->ehufsi[temp3]; - EMIT_BITS eax - - movsx edx, word [esi+DCTSIZE2*2] ; temp2 = t2[k]; - ; Mask off any extra bits in code - mov ecx, dword [esp+temp2] - mov eax, 1 - shl eax, cl - dec eax - and eax, edx ; temp2 &= (((JLONG)1)<>= 1; - - jmp .BLOOP2 -.ELOOP2: - ; If the last coef(s) were zero, emit an end-of-block code - lea edx, [esp + t1 + (DCTSIZE2-1) * 2] ; r = DCTSIZE2-1-k; - cmp edx, esi ; if (r > 0) { - je .EFN - mov eax, INT [ebp] ; code = actbl->ehufco[0]; - movzx ecx, byte [ebp + 1024] ; size = actbl->ehufsi[0]; - EMIT_BITS eax -.EFN: - mov eax, [esp+buffer] - pop esi - ; Save put_buffer & put_bits - mov dword [esi+8], put_buffer ; state->cur.put_buffer = put_buffer; - mov dword [esi+12], put_bits ; state->cur.put_bits = put_bits; - - pop ebp - pop edi - pop esi -; pop edx ; need not be preserved - pop ecx - pop ebx - mov esp, ebp ; esp <- aligned ebp - pop esp ; esp <- original ebp - pop ebp + +%assign stack_offset 0 +%define arg_state 4 + stack_offset +%define arg_buffer 8 + stack_offset +%define arg_block 12 + stack_offset +%define arg_last_dc_val 16 + stack_offset +%define arg_dctbl 20 + stack_offset +%define arg_actbl 24 + stack_offset + + ;X: X = code stream + mov block, [esp + arg_block] + PUSH ebx + PUSH ebp + movups xmm3, XMMWORD [block + 0 * SIZEOF_WORD] ;D: w3 = xx 01 02 03 04 05 06 07 + PUSH esi + PUSH edi + movdqa xmm0, xmm3 ;A: w0 = xx 01 02 03 04 05 06 07 + mov frame, esp + lea t, [frame - (save_frame + 4)] + movups xmm1, XMMWORD [block + 8 * SIZEOF_WORD] ;B: w1 = 08 09 10 11 12 13 14 15 + and t, -DCTSIZE2 * SIZEOF_WORD ; t = &t_[0] + mov [t + save_frame], frame + pxor xmm4, xmm4 ;A: w4[i] = 0; + punpckldq xmm0, xmm1 ;A: w0 = xx 01 08 09 02 03 10 11 + pshuflw xmm0, xmm0, 11001001b ;A: w0 = 01 08 xx 09 02 03 10 11 + pinsrw xmm0, word [block + 16 * SIZEOF_WORD], 2 ;A: w0 = 01 08 16 09 02 03 10 11 + punpckhdq xmm3, xmm1 ;D: w3 = 04 05 12 13 06 07 14 15 + punpcklqdq xmm1, xmm3 ;B: w1 = 08 09 10 11 04 05 12 13 + pinsrw xmm0, word [block + 17 * SIZEOF_WORD], 7 ;A: w0 = 01 08 16 09 02 03 10 17 + ;A: (Row 0, offset 1) + pcmpgtw xmm4, xmm0 ;A: w4[i] = (w0[i] < 0 ? -1 : 0); + paddw xmm0, xmm4 ;A: w0[i] += w4[i]; + movaps XMMWORD [t + 0 * SIZEOF_WORD], xmm0 ;A: t[i] = w0[i]; + + movq xmm2, qword [block + 24 * SIZEOF_WORD] ;B: w2 = 24 25 26 27 -- -- -- -- + pshuflw xmm2, xmm2, 11011000b ;B: w2 = 24 26 25 27 -- -- -- -- + pslldq xmm1, 1 * SIZEOF_WORD ;B: w1 = -- 08 09 10 11 04 05 12 + movups xmm5, XMMWORD [block + 48 * SIZEOF_WORD] ;H: w5 = 48 49 50 51 52 53 54 55 + movsd xmm1, xmm2 ;B: w1 = 24 26 25 27 11 04 05 12 + punpcklqdq xmm2, xmm5 ;C: w2 = 24 26 25 27 48 49 50 51 + pinsrw xmm1, word [block + 32 * SIZEOF_WORD], 1 ;B: w1 = 24 32 25 27 11 04 05 12 + pxor xmm4, xmm4 ;A: w4[i] = 0; + psrldq xmm3, 2 * SIZEOF_WORD ;D: w3 = 12 13 06 07 14 15 -- -- + pcmpeqw xmm0, xmm4 ;A: w0[i] = (w0[i] == 0 ? -1 : 0); + pinsrw xmm1, word [block + 18 * SIZEOF_WORD], 3 ;B: w1 = 24 32 25 18 11 04 05 12 + ; (Row 1, offset 1) + pcmpgtw xmm4, xmm1 ;B: w4[i] = (w1[i] < 0 ? -1 : 0); + paddw xmm1, xmm4 ;B: w1[i] += w4[i]; + movaps XMMWORD [t + 8 * SIZEOF_WORD], xmm1 ;B: t[i+8] = w1[i]; + pxor xmm4, xmm4 ;B: w4[i] = 0; + pcmpeqw xmm1, xmm4 ;B: w1[i] = (w1[i] == 0 ? -1 : 0); + + packsswb xmm0, xmm1 ;AB: b0[i] = w0[i], b0[i+8] = w1[i] + ; w/ signed saturation + + pinsrw xmm3, word [block + 20 * SIZEOF_WORD], 0 ;D: w3 = 20 13 06 07 14 15 -- -- + pinsrw xmm3, word [block + 21 * SIZEOF_WORD], 5 ;D: w3 = 20 13 06 07 14 21 -- -- + pinsrw xmm3, word [block + 28 * SIZEOF_WORD], 6 ;D: w3 = 20 13 06 07 14 21 28 -- + pinsrw xmm3, word [block + 35 * SIZEOF_WORD], 7 ;D: w3 = 20 13 06 07 14 21 28 35 + ; (Row 3, offset 1) + pcmpgtw xmm4, xmm3 ;D: w4[i] = (w3[i] < 0 ? -1 : 0); + paddw xmm3, xmm4 ;D: w3[i] += w4[i]; + movaps XMMWORD [t + 24 * SIZEOF_WORD], xmm3 ;D: t[i+24] = w3[i]; + pxor xmm4, xmm4 ;D: w4[i] = 0; + pcmpeqw xmm3, xmm4 ;D: w3[i] = (w3[i] == 0 ? -1 : 0); + + pinsrw xmm2, word [block + 19 * SIZEOF_WORD], 0 ;C: w2 = 19 26 25 27 48 49 50 51 + pinsrw xmm2, word [block + 33 * SIZEOF_WORD], 2 ;C: w2 = 19 26 33 27 48 49 50 51 + pinsrw xmm2, word [block + 40 * SIZEOF_WORD], 3 ;C: w2 = 19 26 33 40 48 49 50 51 + pinsrw xmm2, word [block + 41 * SIZEOF_WORD], 5 ;C: w2 = 19 26 33 40 48 41 50 51 + pinsrw xmm2, word [block + 34 * SIZEOF_WORD], 6 ;C: w2 = 19 26 33 40 48 41 34 51 + pinsrw xmm2, word [block + 27 * SIZEOF_WORD], 7 ;C: w2 = 19 26 33 40 48 41 34 27 + ; (Row 2, offset 1) + pcmpgtw xmm4, xmm2 ;C: w4[i] = (w2[i] < 0 ? -1 : 0); + paddw xmm2, xmm4 ;C: w2[i] += w4[i]; + movsx code_temp, word [block] ;Z: code_temp = block[0]; + +; %1 - stack pointer adjustment +%macro GET_SYM_BEFORE 1 + movaps XMMWORD [t + 16 * SIZEOF_WORD + %1], xmm2 + ;C: t[i+16] = w2[i]; + pxor xmm4, xmm4 ;C: w4[i] = 0; + pcmpeqw xmm2, xmm4 ;C: w2[i] = (w2[i] == 0 ? -1 : 0); + sub code_temp, [frame + arg_last_dc_val] ;Z: code_temp -= last_dc_val; + + packsswb xmm2, xmm3 ;CD: b2[i] = w2[i], b2[i+8] = w3[i] + ; w/ signed saturation + + movdqa xmm3, xmm5 ;H: w3 = 48 49 50 51 52 53 54 55 + pmovmskb index_temp, xmm2 ;Z: index_temp = 0; index_temp |= ((b2[i] >> 7) << i); + pmovmskb index, xmm0 ;Z: index = 0; index |= ((b0[i] >> 7) << i); + movups xmm0, XMMWORD [block + 56 * SIZEOF_WORD] ;H: w0 = 56 57 58 59 60 61 62 63 + punpckhdq xmm3, xmm0 ;H: w3 = 52 53 60 61 54 55 62 63 + shl index_temp, 16 ;Z: index_temp <<= 16; + psrldq xmm3, 1 * SIZEOF_WORD ;H: w3 = 53 60 61 54 55 62 63 -- + pxor xmm2, xmm2 ;H: w2[i] = 0; + pshuflw xmm3, xmm3, 00111001b ;H: w3 = 60 61 54 53 55 62 63 -- + or index, index_temp ;Z: index |= index_temp; +%undef index_temp +%define free_bits edi +%endmacro + +%macro GET_SYM_AFTER 0 + movq xmm1, qword [block + 44 * SIZEOF_WORD] ;G: w1 = 44 45 46 47 -- -- -- -- + unpcklps xmm5, xmm0 ;E: w5 = 48 49 56 57 50 51 58 59 + pxor xmm0, xmm0 ;H: w0[i] = 0; + not index ;Z: index = ~index; + pinsrw xmm3, word [block + 47 * SIZEOF_WORD], 3 ;H: w3 = 60 61 54 47 55 62 63 -- + ; (Row 7, offset 1) + pcmpgtw xmm2, xmm3 ;H: w2[i] = (w3[i] < 0 ? -1 : 0); + mov dctbl, [frame + arg_dctbl] + paddw xmm3, xmm2 ;H: w3[i] += w2[i]; + movaps XMMWORD [t + 56 * SIZEOF_WORD], xmm3 ;H: t[i+56] = w3[i]; + movq xmm4, qword [block + 36 * SIZEOF_WORD] ;G: w4 = 36 37 38 39 -- -- -- -- + pcmpeqw xmm3, xmm0 ;H: w3[i] = (w3[i] == 0 ? -1 : 0); + punpckldq xmm4, xmm1 ;G: w4 = 36 37 44 45 38 39 46 47 + movdqa xmm1, xmm4 ;F: w1 = 36 37 44 45 38 39 46 47 + pcmpeqw mm_all_0xff, mm_all_0xff ;Z: all_0xff[i] = 0xFF; +%endmacro + + GET_SYM nbits_base, jpeg_nbits_table, GET_SYM_BEFORE, GET_SYM_AFTER + + psrldq xmm4, 1 * SIZEOF_WORD ;G: w4 = 37 44 45 38 39 46 47 -- + shufpd xmm1, xmm5, 10b ;F: w1 = 36 37 44 45 50 51 58 59 + pshufhw xmm4, xmm4, 11010011b ;G: w4 = 37 44 45 38 -- 39 46 -- + pslldq xmm1, 1 * SIZEOF_WORD ;F: w1 = -- 36 37 44 45 50 51 58 + pinsrw xmm4, word [block + 59 * SIZEOF_WORD], 0 ;G: w4 = 59 44 45 38 -- 39 46 -- + pshufd xmm1, xmm1, 11011000b ;F: w1 = -- 36 45 50 37 44 51 58 + cmp code_temp, 1 << 31 ;Z: Set CF if code_temp < 0x80000000, + ;Z: i.e. if code_temp is positive + pinsrw xmm4, word [block + 52 * SIZEOF_WORD], 1 ;G: w4 = 59 52 45 38 -- 39 46 -- + movlps xmm1, qword [block + 20 * SIZEOF_WORD] ;F: w1 = 20 21 22 23 37 44 51 58 + pinsrw xmm4, word [block + 31 * SIZEOF_WORD], 4 ;G: w4 = 59 52 45 38 31 39 46 -- + pshuflw xmm1, xmm1, 01110010b ;F: w1 = 22 20 23 21 37 44 51 58 + pinsrw xmm4, word [block + 53 * SIZEOF_WORD], 7 ;G: w4 = 59 52 45 38 31 39 46 53 + ; (Row 6, offset 1) + adc code_temp, -1 ;Z: code_temp += -1 + (code_temp >= 0 ? 1 : 0); + pxor xmm2, xmm2 ;G: w2[i] = 0; + pcmpgtw xmm0, xmm4 ;G: w0[i] = (w4[i] < 0 ? -1 : 0); + pinsrw xmm1, word [block + 15 * SIZEOF_WORD], 1 ;F: w1 = 22 15 23 21 37 44 51 58 + paddw xmm4, xmm0 ;G: w4[i] += w0[i]; + movaps XMMWORD [t + 48 * SIZEOF_WORD], xmm4 ;G: t[48+i] = w4[i]; + movd mm_temp, code_temp ;Z: temp = code_temp + pinsrw xmm1, word [block + 30 * SIZEOF_WORD], 3 ;F: w1 = 22 15 23 30 37 44 51 58 + ; (Row 5, offset 1) + pcmpeqw xmm4, xmm2 ;G: w4[i] = (w4[i] == 0 ? -1 : 0); + + packsswb xmm4, xmm3 ;GH: b4[i] = w4[i], b4[i+8] = w3[i] + ; w/ signed saturation + + lea t, [t - SIZEOF_WORD] ;Z: t = &t[-1] + pxor xmm0, xmm0 ;F: w0[i] = 0; + pcmpgtw xmm2, xmm1 ;F: w2[i] = (w1[i] < 0 ? -1 : 0); + paddw xmm1, xmm2 ;F: w1[i] += w2[i]; + movaps XMMWORD [t + (40+1) * SIZEOF_WORD], xmm1 ;F: t[40+i] = w1[i]; + pcmpeqw xmm1, xmm0 ;F: w1[i] = (w1[i] == 0 ? -1 : 0); + pinsrw xmm5, word [block + 42 * SIZEOF_WORD], 0 ;E: w5 = 42 49 56 57 50 51 58 59 + pinsrw xmm5, word [block + 43 * SIZEOF_WORD], 5 ;E: w5 = 42 49 56 57 50 43 58 59 + pinsrw xmm5, word [block + 36 * SIZEOF_WORD], 6 ;E: w5 = 42 49 56 57 50 43 36 59 + pinsrw xmm5, word [block + 29 * SIZEOF_WORD], 7 ;E: w5 = 42 49 56 57 50 43 36 29 + ; (Row 4, offset 1) +%undef block +%define nbits edx +%define nbitsb dl +%define nbitsh dh + movzx nbits, byte [NBITS(code_temp)] ;Z: nbits = JPEG_NBITS(code_temp); +%undef code_temp +%define state esi + pxor xmm2, xmm2 ;E: w2[i] = 0; + mov state, [frame + arg_state] + movd mm_nbits, nbits ;Z: nbits --> MMX register + pcmpgtw xmm0, xmm5 ;E: w0[i] = (w5[i] < 0 ? -1 : 0); + movd mm_code, dword [dctbl + c_derived_tbl.ehufco + nbits * 4] + ;Z: code = dctbl->ehufco[nbits]; +%define size ecx +%define sizeb cl +%define sizeh ch + paddw xmm5, xmm0 ;E: w5[i] += w0[i]; + movaps XMMWORD [t + (32+1) * SIZEOF_WORD], xmm5 ;E: t[32+i] = w5[i]; + movzx size, byte [dctbl + c_derived_tbl.ehufsi + nbits] + ;Z: size = dctbl->ehufsi[nbits]; +%undef dctbl + pcmpeqw xmm5, xmm2 ;E: w5[i] = (w5[i] == 0 ? -1 : 0); + + packsswb xmm5, xmm1 ;EF: b5[i] = w5[i], b5[i+8] = w1[i] + ; w/ signed saturation + + movq mm_put_buffer, [state + working_state.cur.put_buffer.simd] + ;Z: put_buffer = state->cur.put_buffer.simd; + mov free_bits, [state + working_state.cur.free_bits] + ;Z: free_bits = state->cur.free_bits; +%undef state +%define actbl esi + mov actbl, [frame + arg_actbl] +%define buffer eax + mov buffer, [frame + arg_buffer] +%undef frame + jmp .BEGIN + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +; size <= 32, so this is not really a loop +.BRLOOP1: ; .BRLOOP1: + movzx nbits, byte [actbl + c_derived_tbl.ehufsi + 0xf0] + ; nbits = actbl->ehufsi[0xf0]; + movd mm_code, dword [actbl + c_derived_tbl.ehufco + 0xf0 * 4] + ; code = actbl->ehufco[0xf0]; + and index, 0x7ffffff ; clear index if size == 32 + sub size, 16 ; size -= 16; + sub free_bits, nbits ; if ((free_bits -= nbits) <= 0) + jle .EMIT_BRLOOP1 ; goto .EMIT_BRLOOP1; + movd mm_nbits, nbits ; nbits --> MMX register + psllq mm_put_buffer, mm_nbits ; put_buffer <<= nbits; + por mm_put_buffer, mm_code ; put_buffer |= code; + jmp .ERLOOP1 ; goto .ERLOOP1; + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +%ifdef PIC + times 6 nop +%else + times 2 nop +%endif +.BLOOP1: ; do { /* size = # of zero bits/elements to skip */ +; if size == 32, index remains unchanged. Correct in .BRLOOP. + shr index, sizeb ; index >>= size; + lea t, [t + size * SIZEOF_WORD] ; t += size; + cmp size, 16 ; if (size > 16) + jg .BRLOOP1 ; goto .BRLOOP1; +.ERLOOP1: ; .ERLOOP1: + movsx nbits, word [t] ; nbits = *t; +%ifdef PIC + add size, size ; size += size; +%else + lea size, [size * 2] ; size += size; +%endif + movd mm_temp, nbits ; temp = nbits; + movzx nbits, byte [NBITS(nbits)] ; nbits = JPEG_NBITS(nbits); + lea size, [size * 8 + nbits] ; size = size * 8 + nbits; + movd mm_nbits, nbits ; nbits --> MMX register + movd mm_code, dword [actbl + c_derived_tbl.ehufco + (size - 16) * 4] + ; code = actbl->ehufco[size-16]; + movzx size, byte [actbl + c_derived_tbl.ehufsi + (size - 16)] + ; size = actbl->ehufsi[size-16]; +.BEGIN: ; .BEGIN: + pand mm_temp, [MASK_BITS(nbits)] ; temp &= (1 << nbits) - 1; + psllq mm_code, mm_nbits ; code <<= nbits; + add nbits, size ; nbits += size; + por mm_code, mm_temp ; code |= temp; + sub free_bits, nbits ; if ((free_bits -= nbits) <= 0) + jle .EMIT_ERLOOP1 ; insert code, flush buffer, init size, goto .BLOOP1 + xor size, size ; size = 0; /* kill tzcnt input dependency */ + tzcnt size, index ; size = # of trailing 0 bits in index + movd mm_nbits, nbits ; nbits --> MMX register + psllq mm_put_buffer, mm_nbits ; put_buffer <<= nbits; + inc size ; ++size; + por mm_put_buffer, mm_code ; put_buffer |= code; + test index, index + jnz .BLOOP1 ; } while (index != 0); +; Round 2 +; t points to the last used word, possibly below t_ if the previous index had 32 zero bits. +.ELOOP1: ; .ELOOP1: + pmovmskb size, xmm4 ; size = 0; size |= ((b4[i] >> 7) << i); + pmovmskb index, xmm5 ; index = 0; index |= ((b5[i] >> 7) << i); + shl size, 16 ; size <<= 16; + or index, size ; index |= size; + not index ; index = ~index; + lea nbits, [t + (1 + DCTSIZE2) * SIZEOF_WORD] + ; nbits = t + 1 + 64; + and nbits, -DCTSIZE2 * SIZEOF_WORD ; nbits &= -128; /* now points to &t_[64] */ + sub nbits, t ; nbits -= t; + shr nbits, 1 ; nbits >>= 1; /* # of leading 0 bits in old index + 33 */ + tzcnt size, index ; size = # of trailing 0 bits in index + inc size ; ++size; + test index, index ; if (index == 0) + jz .ELOOP2 ; goto .ELOOP2; +; NOTE: size == 32 cannot happen, since the last element is always 0. + shr index, sizeb ; index >>= size; + lea size, [size + nbits - 33] ; size = size + nbits - 33; + lea t, [t + size * SIZEOF_WORD] ; t += size; + cmp size, 16 ; if (size <= 16) + jle .ERLOOP2 ; goto .ERLOOP2; +.BRLOOP2: ; do { + movzx nbits, byte [actbl + c_derived_tbl.ehufsi + 0xf0] + ; nbits = actbl->ehufsi[0xf0]; + sub size, 16 ; size -= 16; + movd mm_code, dword [actbl + c_derived_tbl.ehufco + 0xf0 * 4] + ; code = actbl->ehufco[0xf0]; + sub free_bits, nbits ; if ((free_bits -= nbits) <= 0) + jle .EMIT_BRLOOP2 ; insert code and flush put_buffer + movd mm_nbits, nbits ; else { nbits --> MMX register + psllq mm_put_buffer, mm_nbits ; put_buffer <<= nbits; + por mm_put_buffer, mm_code ; put_buffer |= code; + cmp size, 16 ; if (size <= 16) + jle .ERLOOP2 ; goto .ERLOOP2; + jmp .BRLOOP2 ; } while (1); + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +.BLOOP2: ; do { /* size = # of zero bits/elements to skip */ + shr index, sizeb ; index >>= size; + lea t, [t + size * SIZEOF_WORD] ; t += size; + cmp size, 16 ; if (size > 16) + jg .BRLOOP2 ; goto .BRLOOP2; +.ERLOOP2: ; .ERLOOP2: + movsx nbits, word [t] ; nbits = *t; + add size, size ; size += size; + movd mm_temp, nbits ; temp = nbits; + movzx nbits, byte [NBITS(nbits)] ; nbits = JPEG_NBITS(nbits); + movd mm_nbits, nbits ; nbits --> MMX register + lea size, [size * 8 + nbits] ; size = size * 8 + nbits; + movd mm_code, dword [actbl + c_derived_tbl.ehufco + (size - 16) * 4] + ; code = actbl->ehufco[size-16]; + movzx size, byte [actbl + c_derived_tbl.ehufsi + (size - 16)] + ; size = actbl->ehufsi[size-16]; + psllq mm_code, mm_nbits ; code <<= nbits; + pand mm_temp, [MASK_BITS(nbits)] ; temp &= (1 << nbits) - 1; + lea nbits, [nbits + size] ; nbits += size; + por mm_code, mm_temp ; code |= temp; + xor size, size ; size = 0; /* kill tzcnt input dependency */ + sub free_bits, nbits ; if ((free_bits -= nbits) <= 0) + jle .EMIT_ERLOOP2 ; insert code, flush buffer, init size, goto .BLOOP2 + tzcnt size, index ; size = # of trailing 0 bits in index + movd mm_nbits, nbits ; nbits --> MMX register + psllq mm_put_buffer, mm_nbits ; put_buffer <<= nbits; + inc size ; ++size; + por mm_put_buffer, mm_code ; put_buffer |= code; + test index, index + jnz .BLOOP2 ; } while (index != 0); +.ELOOP2: ; .ELOOP2: + mov nbits, t ; nbits = t; + lea t, [t + SIZEOF_WORD] ; t = &t[1]; + and nbits, DCTSIZE2 * SIZEOF_WORD - 1 ; nbits &= 127; + and t, -DCTSIZE2 * SIZEOF_WORD ; t &= -128; /* t = &t_[0]; */ + cmp nbits, (DCTSIZE2 - 2) * SIZEOF_WORD ; if (nbits != 62 * 2) + je .EFN ; { + movd mm_code, dword [actbl + c_derived_tbl.ehufco + 0] + ; code = actbl->ehufco[0]; + movzx nbits, byte [actbl + c_derived_tbl.ehufsi + 0] + ; nbits = actbl->ehufsi[0]; + sub free_bits, nbits ; if ((free_bits -= nbits) <= 0) + jg .EFN_SKIP_EMIT_CODE ; { + EMIT_QWORD size, sizeb, sizeh, , , , , , .EFN ; insert code, flush put_buffer + align 16 +.EFN_SKIP_EMIT_CODE: ; } else { + movd mm_nbits, nbits ; nbits --> MMX register + psllq mm_put_buffer, mm_nbits ; put_buffer <<= nbits; + por mm_put_buffer, mm_code ; put_buffer |= code; +.EFN: ; } } +%define frame esp + mov frame, [t + save_frame] +%define state ecx + mov state, [frame + arg_state] + movq [state + working_state.cur.put_buffer.simd], mm_put_buffer + ; state->cur.put_buffer.simd = put_buffer; + emms + mov [state + working_state.cur.free_bits], free_bits + ; state->cur.free_bits = free_bits; + POP edi + POP esi + POP ebp + POP ebx ret +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +.EMIT_BRLOOP1: + EMIT_QWORD emit_temp, emit_tempb, emit_temph, , , , , , \ + .ERLOOP1 + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +.EMIT_ERLOOP1: + EMIT_QWORD size, sizeb, sizeh, \ + { xor size, size }, \ + { tzcnt size, index }, \ + { inc size }, \ + { test index, index }, \ + { jnz .BLOOP1 }, \ + .ELOOP1 + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +.EMIT_BRLOOP2: + EMIT_QWORD emit_temp, emit_tempb, emit_temph, , , , \ + { cmp size, 16 }, \ + { jle .ERLOOP2 }, \ + .BRLOOP2 + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +.EMIT_ERLOOP2: + EMIT_QWORD size, sizeb, sizeh, \ + { xor size, size }, \ + { tzcnt size, index }, \ + { inc size }, \ + { test index, index }, \ + { jnz .BLOOP2 }, \ + .ELOOP2 + ; For some reason, the OS X linker does not honor the request to align the ; segment unless we do this. align 32 diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jcphuff-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jcphuff-sse2.asm index 8b731783760..c26b48a47d8 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jcphuff-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jcphuff-sse2.asm @@ -523,6 +523,8 @@ EXTN(jsimd_encode_mcu_AC_refine_prepare_sse2): add KK, 2 dec K jnz .BLOOPR16 + test LEN, 15 + je .PADDINGR .ELOOPR16: mov LENEND, LEN diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-avx2.asm index 97de2302b54..23cf733135b 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-avx2.asm @@ -2,7 +2,7 @@ ; jfdctint.asm - accurate integer FDCT (AVX2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2009, 2016, 2018, D. R. Commander. +; Copyright (C) 2009, 2016, 2018, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; forward DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jfdctint.c; see the jfdctint.c for ; more details. @@ -103,7 +103,7 @@ F_3_072 equ DESCALE(3299298341, 30 - CONST_BITS) ; FIX(3.072711026) %endmacro ; -------------------------------------------------------------------------- -; In-place 8x8x16-bit slow integer forward DCT using AVX2 instructions +; In-place 8x8x16-bit accurate integer forward DCT using AVX2 instructions ; %1-%4: Input/output registers ; %5-%8: Temp registers ; %9: Pass (1 or 2) diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-mmx.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-mmx.asm index 3ade9d4921d..34a43b9e5ef 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-mmx.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-mmx.asm @@ -2,7 +2,7 @@ ; jfdctint.asm - accurate integer FDCT (MMX) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2016, D. R. Commander. +; Copyright (C) 2016, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; forward DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jfdctint.c; see the jfdctint.c for ; more details. diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-sse2.asm index 71b684c4fb4..6f8e18cb9d0 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jfdctint-sse2.asm @@ -2,7 +2,7 @@ ; jfdctint.asm - accurate integer FDCT (SSE2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2016, D. R. Commander. +; Copyright (C) 2016, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; forward DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jfdctint.c; see the jfdctint.c for ; more details. diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-avx2.asm index c371985c76a..199c7df3b69 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-avx2.asm @@ -2,7 +2,7 @@ ; jidctint.asm - accurate integer IDCT (AVX2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2009, 2016, 2018, D. R. Commander. +; Copyright (C) 2009, 2016, 2018, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; inverse DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jidctint.c; see the jidctint.c for ; more details. @@ -113,7 +113,7 @@ F_3_072 equ DESCALE(3299298341, 30 - CONST_BITS) ; FIX(3.072711026) %endmacro ; -------------------------------------------------------------------------- -; In-place 8x8x16-bit slow integer inverse DCT using AVX2 instructions +; In-place 8x8x16-bit accurate integer inverse DCT using AVX2 instructions ; %1-%4: Input/output registers ; %5-%12: Temp registers ; %9: Pass (1 or 2) diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-mmx.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-mmx.asm index 4f07f567f55..f15c8d34bcb 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-mmx.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-mmx.asm @@ -2,7 +2,7 @@ ; jidctint.asm - accurate integer IDCT (MMX) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2016, D. R. Commander. +; Copyright (C) 2016, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; inverse DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jidctint.c; see the jidctint.c for ; more details. diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-sse2.asm index e442fdd2ddb..43e320189b4 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jidctint-sse2.asm @@ -2,7 +2,7 @@ ; jidctint.asm - accurate integer IDCT (SSE2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2016, D. R. Commander. +; Copyright (C) 2016, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; inverse DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jidctint.c; see the jidctint.c for ; more details. diff --git a/third-party/mozjpeg/mozjpeg/simd/i386/jsimd.c b/third-party/mozjpeg/mozjpeg/simd/i386/jsimd.c index 563949a02b8..b429b0a5320 100644 --- a/third-party/mozjpeg/mozjpeg/simd/i386/jsimd.c +++ b/third-party/mozjpeg/mozjpeg/simd/i386/jsimd.c @@ -2,8 +2,8 @@ * jsimd_i386.c * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009-2011, 2013-2014, 2016, 2018, D. R. Commander. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2009-2011, 2013-2014, 2016, 2018, 2022-2023, D. R. Commander. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -21,7 +21,6 @@ #include "../../jdct.h" #include "../../jsimddct.h" #include "../jsimd.h" -#include "jconfigint.h" /* * In the PIC cases, we have no guarantee that constants will keep @@ -32,19 +31,17 @@ #define IS_ALIGNED_SSE(ptr) (IS_ALIGNED(ptr, 4)) /* 16 byte alignment */ #define IS_ALIGNED_AVX(ptr) (IS_ALIGNED(ptr, 5)) /* 32 byte alignment */ -static unsigned int simd_support = (unsigned int)(~0); -static unsigned int simd_huffman = 1; +static THREAD_LOCAL unsigned int simd_support = (unsigned int)(~0); +static THREAD_LOCAL unsigned int simd_huffman = 1; /* * Check what SIMD accelerations are supported. - * - * FIXME: This code is racy under a multi-threaded environment. */ LOCAL(void) init_simd(void) { #ifndef NO_GETENV - char *env = NULL; + char env[2] = { 0 }; #endif if (simd_support != ~0U) @@ -54,26 +51,19 @@ init_simd(void) #ifndef NO_GETENV /* Force different settings through environment variables */ - env = getenv("JSIMD_FORCEMMX"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCEMMX") && !strcmp(env, "1")) simd_support &= JSIMD_MMX; - env = getenv("JSIMD_FORCE3DNOW"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCE3DNOW") && !strcmp(env, "1")) simd_support &= JSIMD_3DNOW | JSIMD_MMX; - env = getenv("JSIMD_FORCESSE"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCESSE") && !strcmp(env, "1")) simd_support &= JSIMD_SSE | JSIMD_MMX; - env = getenv("JSIMD_FORCESSE2"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCESSE2") && !strcmp(env, "1")) simd_support &= JSIMD_SSE2; - env = getenv("JSIMD_FORCEAVX2"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCEAVX2") && !strcmp(env, "1")) simd_support &= JSIMD_AVX2; - env = getenv("JSIMD_FORCENONE"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCENONE") && !strcmp(env, "1")) simd_support = 0; - env = getenv("JSIMD_NOHUFFENC"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_NOHUFFENC") && !strcmp(env, "1")) simd_huffman = 0; #endif } @@ -168,6 +158,9 @@ jsimd_rgb_ycc_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, void (*sse2fct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); void (*mmxfct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->in_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_extrgb_ycc_convert_avx2; @@ -227,6 +220,9 @@ jsimd_rgb_gray_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, void (*sse2fct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); void (*mmxfct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->in_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_extrgb_gray_convert_avx2; @@ -286,6 +282,9 @@ jsimd_ycc_rgb_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, void (*sse2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY, int); void (*mmxfct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY, int); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->out_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_ycc_extrgb_convert_avx2; @@ -389,6 +388,9 @@ GLOBAL(void) jsimd_h2v2_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY output_data) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v2_downsample_avx2(cinfo->image_width, cinfo->max_v_samp_factor, compptr->v_samp_factor, @@ -409,6 +411,9 @@ GLOBAL(void) jsimd_h2v1_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY output_data) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v1_downsample_avx2(cinfo->image_width, cinfo->max_v_samp_factor, compptr->v_samp_factor, @@ -471,6 +476,9 @@ GLOBAL(void) jsimd_h2v2_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v2_upsample_avx2(cinfo->max_v_samp_factor, cinfo->output_width, input_data, output_data_ptr); @@ -486,6 +494,9 @@ GLOBAL(void) jsimd_h2v1_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v1_upsample_avx2(cinfo->max_v_samp_factor, cinfo->output_width, input_data, output_data_ptr); @@ -547,6 +558,9 @@ GLOBAL(void) jsimd_h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v2_fancy_upsample_avx2(cinfo->max_v_samp_factor, compptr->downsampled_width, input_data, @@ -565,6 +579,9 @@ GLOBAL(void) jsimd_h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v1_fancy_upsample_avx2(cinfo->max_v_samp_factor, compptr->downsampled_width, input_data, @@ -633,6 +650,9 @@ jsimd_h2v2_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, void (*sse2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); void (*mmxfct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->out_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_h2v2_extrgb_merged_upsample_avx2; @@ -691,6 +711,9 @@ jsimd_h2v1_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, void (*sse2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); void (*mmxfct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->out_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_h2v1_extrgb_merged_upsample_avx2; @@ -795,6 +818,9 @@ GLOBAL(void) jsimd_convsamp(JSAMPARRAY sample_data, JDIMENSION start_col, DCTELEM *workspace) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_convsamp_avx2(sample_data, start_col, workspace); else if (simd_support & JSIMD_SSE2) @@ -807,6 +833,9 @@ GLOBAL(void) jsimd_convsamp_float(JSAMPARRAY sample_data, JDIMENSION start_col, FAST_FLOAT *workspace) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_SSE2) jsimd_convsamp_float_sse2(sample_data, start_col, workspace); else if (simd_support & JSIMD_SSE) @@ -877,6 +906,9 @@ jsimd_can_fdct_float(void) GLOBAL(void) jsimd_fdct_islow(DCTELEM *data) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_fdct_islow_avx2(data); else if (simd_support & JSIMD_SSE2) @@ -888,6 +920,9 @@ jsimd_fdct_islow(DCTELEM *data) GLOBAL(void) jsimd_fdct_ifast(DCTELEM *data) { + if (simd_support == ~0U) + init_simd(); + if ((simd_support & JSIMD_SSE2) && IS_ALIGNED_SSE(jconst_fdct_islow_sse2)) jsimd_fdct_ifast_sse2(data); else @@ -897,6 +932,9 @@ jsimd_fdct_ifast(DCTELEM *data) GLOBAL(void) jsimd_fdct_float(FAST_FLOAT *data) { + if (simd_support == ~0U) + init_simd(); + if ((simd_support & JSIMD_SSE) && IS_ALIGNED_SSE(jconst_fdct_float_sse)) jsimd_fdct_float_sse(data); else if (simd_support & JSIMD_3DNOW) @@ -952,6 +990,9 @@ jsimd_can_quantize_float(void) GLOBAL(void) jsimd_quantize(JCOEFPTR coef_block, DCTELEM *divisors, DCTELEM *workspace) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_quantize_avx2(coef_block, divisors, workspace); else if (simd_support & JSIMD_SSE2) @@ -964,6 +1005,9 @@ GLOBAL(void) jsimd_quantize_float(JCOEFPTR coef_block, FAST_FLOAT *divisors, FAST_FLOAT *workspace) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_SSE2) jsimd_quantize_float_sse2(coef_block, divisors, workspace); else if (simd_support & JSIMD_SSE) @@ -1027,6 +1071,9 @@ jsimd_idct_2x2(j_decompress_ptr cinfo, jpeg_component_info *compptr, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col) { + if (simd_support == ~0U) + init_simd(); + if ((simd_support & JSIMD_SSE2) && IS_ALIGNED_SSE(jconst_idct_red_sse2)) jsimd_idct_2x2_sse2(compptr->dct_table, coef_block, output_buf, output_col); @@ -1039,6 +1086,9 @@ jsimd_idct_4x4(j_decompress_ptr cinfo, jpeg_component_info *compptr, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col) { + if (simd_support == ~0U) + init_simd(); + if ((simd_support & JSIMD_SSE2) && IS_ALIGNED_SSE(jconst_idct_red_sse2)) jsimd_idct_4x4_sse2(compptr->dct_table, coef_block, output_buf, output_col); @@ -1133,6 +1183,9 @@ jsimd_idct_islow(j_decompress_ptr cinfo, jpeg_component_info *compptr, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_idct_islow_avx2(compptr->dct_table, coef_block, output_buf, output_col); @@ -1149,6 +1202,9 @@ jsimd_idct_ifast(j_decompress_ptr cinfo, jpeg_component_info *compptr, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col) { + if (simd_support == ~0U) + init_simd(); + if ((simd_support & JSIMD_SSE2) && IS_ALIGNED_SSE(jconst_idct_ifast_sse2)) jsimd_idct_ifast_sse2(compptr->dct_table, coef_block, output_buf, output_col); @@ -1162,6 +1218,9 @@ jsimd_idct_float(j_decompress_ptr cinfo, jpeg_component_info *compptr, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col) { + if (simd_support == ~0U) + init_simd(); + if ((simd_support & JSIMD_SSE2) && IS_ALIGNED_SSE(jconst_idct_float_sse2)) jsimd_idct_float_sse2(compptr->dct_table, coef_block, output_buf, output_col); @@ -1219,7 +1278,7 @@ jsimd_can_encode_mcu_AC_first_prepare(void) GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { jsimd_encode_mcu_AC_first_prepare_sse2(block, jpeg_natural_order_start, Sl, Al, values, zerobits); @@ -1245,7 +1304,7 @@ jsimd_can_encode_mcu_AC_refine_prepare(void) GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { return jsimd_encode_mcu_AC_refine_prepare_sse2(block, jpeg_natural_order_start, diff --git a/third-party/mozjpeg/mozjpeg/simd/jsimd.h b/third-party/mozjpeg/mozjpeg/simd/jsimd.h index a9fc81280d2..a28754adb9d 100644 --- a/third-party/mozjpeg/mozjpeg/simd/jsimd.h +++ b/third-party/mozjpeg/mozjpeg/simd/jsimd.h @@ -2,11 +2,12 @@ * simd/jsimd.h * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2011, 2014-2016, 2018, D. R. Commander. + * Copyright (C) 2011, 2014-2016, 2018, 2020, 2022, D. R. Commander. * Copyright (C) 2013-2014, MIPS Technologies, Inc., California. * Copyright (C) 2014, Linaro Limited. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. - * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * Copyright (C) 2020, Arm Limited. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -121,6 +122,8 @@ EXTERN(void) jsimd_extxrgb_ycc_convert_neon (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); +#ifndef NEON_INTRINSICS + EXTERN(void) jsimd_extrgb_ycc_convert_neon_slowld3 (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); @@ -128,6 +131,8 @@ EXTERN(void) jsimd_extbgr_ycc_convert_neon_slowld3 (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); +#endif + EXTERN(void) jsimd_rgb_ycc_convert_dspr2 (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); @@ -263,6 +268,28 @@ EXTERN(void) jsimd_extxrgb_gray_convert_avx2 (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_rgb_gray_convert_neon + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extrgb_gray_convert_neon + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extrgbx_gray_convert_neon + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extbgr_gray_convert_neon + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extbgrx_gray_convert_neon + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extxbgr_gray_convert_neon + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extxrgb_gray_convert_neon + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); + EXTERN(void) jsimd_rgb_gray_convert_dspr2 (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); @@ -285,6 +312,28 @@ EXTERN(void) jsimd_extxrgb_gray_convert_dspr2 (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_rgb_gray_convert_mmi + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extrgb_gray_convert_mmi + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extrgbx_gray_convert_mmi + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extbgr_gray_convert_mmi + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extbgrx_gray_convert_mmi + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extxbgr_gray_convert_mmi + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); +EXTERN(void) jsimd_extxrgb_gray_convert_mmi + (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, + JDIMENSION output_row, int num_rows); + EXTERN(void) jsimd_rgb_gray_convert_altivec (JDIMENSION img_width, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows); @@ -401,6 +450,8 @@ EXTERN(void) jsimd_ycc_rgb565_convert_neon (JDIMENSION out_width, JSAMPIMAGE input_buf, JDIMENSION input_row, JSAMPARRAY output_buf, int num_rows); +#ifndef NEON_INTRINSICS + EXTERN(void) jsimd_ycc_extrgb_convert_neon_slowst3 (JDIMENSION out_width, JSAMPIMAGE input_buf, JDIMENSION input_row, JSAMPARRAY output_buf, int num_rows); @@ -408,6 +459,8 @@ EXTERN(void) jsimd_ycc_extbgr_convert_neon_slowst3 (JDIMENSION out_width, JSAMPIMAGE input_buf, JDIMENSION input_row, JSAMPARRAY output_buf, int num_rows); +#endif + EXTERN(void) jsimd_ycc_rgb_convert_dspr2 (JDIMENSION out_width, JSAMPIMAGE input_buf, JDIMENSION input_row, JSAMPARRAY output_buf, int num_rows); @@ -562,6 +615,13 @@ EXTERN(void) jsimd_h2v2_upsample_avx2 (int max_v_samp_factor, JDIMENSION output_width, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr); +EXTERN(void) jsimd_h2v1_upsample_neon + (int max_v_samp_factor, JDIMENSION output_width, JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr); +EXTERN(void) jsimd_h2v2_upsample_neon + (int max_v_samp_factor, JDIMENSION output_width, JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr); + EXTERN(void) jsimd_h2v1_upsample_dspr2 (int max_v_samp_factor, JDIMENSION output_width, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr); @@ -608,6 +668,12 @@ EXTERN(void) jsimd_h2v2_fancy_upsample_avx2 EXTERN(void) jsimd_h2v1_fancy_upsample_neon (int max_v_samp_factor, JDIMENSION downsampled_width, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr); +EXTERN(void) jsimd_h2v2_fancy_upsample_neon + (int max_v_samp_factor, JDIMENSION downsampled_width, JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr); +EXTERN(void) jsimd_h1v2_fancy_upsample_neon + (int max_v_samp_factor, JDIMENSION downsampled_width, JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr); EXTERN(void) jsimd_h2v1_fancy_upsample_dspr2 (int max_v_samp_factor, JDIMENSION downsampled_width, JSAMPARRAY input_data, @@ -616,6 +682,9 @@ EXTERN(void) jsimd_h2v2_fancy_upsample_dspr2 (int max_v_samp_factor, JDIMENSION downsampled_width, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr); +EXTERN(void) jsimd_h2v1_fancy_upsample_mmi + (int max_v_samp_factor, JDIMENSION downsampled_width, JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr); EXTERN(void) jsimd_h2v2_fancy_upsample_mmi (int max_v_samp_factor, JDIMENSION downsampled_width, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr); @@ -762,6 +831,50 @@ EXTERN(void) jsimd_h2v2_extxrgb_merged_upsample_avx2 (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extrgb_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extrgbx_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extbgr_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extbgrx_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extxbgr_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extxrgb_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); + +EXTERN(void) jsimd_h2v2_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extrgb_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extrgbx_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extbgr_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extbgrx_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extxbgr_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extxrgb_merged_upsample_neon + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); + EXTERN(void) jsimd_h2v1_merged_upsample_dspr2 (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf, JSAMPLE *range); @@ -806,6 +919,50 @@ EXTERN(void) jsimd_h2v2_extxrgb_merged_upsample_dspr2 (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf, JSAMPLE *range); +EXTERN(void) jsimd_h2v1_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extrgb_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extrgbx_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extbgr_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extbgrx_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extxbgr_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v1_extxrgb_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); + +EXTERN(void) jsimd_h2v2_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extrgb_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extrgbx_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extbgr_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extbgrx_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extxbgr_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); +EXTERN(void) jsimd_h2v2_extxrgb_merged_upsample_mmi + (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf); + EXTERN(void) jsimd_h2v1_merged_upsample_altivec (JDIMENSION output_width, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf); @@ -882,7 +1039,7 @@ EXTERN(void) jsimd_convsamp_float_sse2 EXTERN(void) jsimd_convsamp_float_dspr2 (JSAMPARRAY sample_data, JDIMENSION start_col, FAST_FLOAT *workspace); -/* Slow Integer Forward DCT */ +/* Accurate Integer Forward DCT */ EXTERN(void) jsimd_fdct_islow_mmx(DCTELEM *data); extern const int jconst_fdct_islow_sse2[]; @@ -909,6 +1066,8 @@ EXTERN(void) jsimd_fdct_ifast_neon(DCTELEM *data); EXTERN(void) jsimd_fdct_ifast_dspr2(DCTELEM *data); +EXTERN(void) jsimd_fdct_ifast_mmi(DCTELEM *data); + EXTERN(void) jsimd_fdct_ifast_altivec(DCTELEM *data); /* Floating Point Forward DCT */ @@ -989,7 +1148,7 @@ EXTERN(void) jsimd_idct_12x12_pass1_dspr2 EXTERN(void) jsimd_idct_12x12_pass2_dspr2 (int *workspace, int *output); -/* Slow Integer Inverse DCT */ +/* Accurate Integer Inverse DCT */ EXTERN(void) jsimd_idct_islow_mmx (void *dct_table, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col); @@ -1040,6 +1199,10 @@ EXTERN(void) jsimd_idct_ifast_rows_dspr2 (DCTELEM *wsptr, JSAMPARRAY output_buf, JDIMENSION output_col, const int *idct_coefs); +EXTERN(void) jsimd_idct_ifast_mmi + (void *dct_table, JCOEFPTR coef_block, JSAMPARRAY output_buf, + JDIMENSION output_col); + EXTERN(void) jsimd_idct_ifast_altivec (void *dct_table, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col); @@ -1069,15 +1232,27 @@ EXTERN(JOCTET *) jsimd_huff_encode_one_block_neon (void *state, JOCTET *buffer, JCOEFPTR block, int last_dc_val, c_derived_tbl *dctbl, c_derived_tbl *actbl); +#ifndef NEON_INTRINSICS + EXTERN(JOCTET *) jsimd_huff_encode_one_block_neon_slowtbl (void *state, JOCTET *buffer, JCOEFPTR block, int last_dc_val, c_derived_tbl *dctbl, c_derived_tbl *actbl); +#endif + /* Progressive Huffman encoding */ EXTERN(void) jsimd_encode_mcu_AC_first_prepare_sse2 (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, - JCOEF *values, size_t *zerobits); + UJCOEF *values, size_t *zerobits); + +EXTERN(void) jsimd_encode_mcu_AC_first_prepare_neon + (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, + UJCOEF *values, size_t *zerobits); EXTERN(int) jsimd_encode_mcu_AC_refine_prepare_sse2 (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, - JCOEF *absvalues, size_t *bits); + UJCOEF *absvalues, size_t *bits); + +EXTERN(int) jsimd_encode_mcu_AC_refine_prepare_neon + (const JCOEF *block, const int *jpeg_natural_order_start, int Sl, int Al, + UJCOEF *absvalues, size_t *bits); diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jccolext-mmi.c b/third-party/mozjpeg/mozjpeg/simd/loongson/jccolext-mmi.c deleted file mode 100644 index 6cdeb5e09a6..00000000000 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jccolext-mmi.c +++ /dev/null @@ -1,483 +0,0 @@ -/* - * Loongson MMI optimizations for libjpeg-turbo - * - * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2014-2015, 2019, D. R. Commander. All Rights Reserved. - * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. - * All Rights Reserved. - * Authors: ZhuChen - * SunZhangzhi - * CaiWanwei - * ZhangLixia - * - * Based on the x86 SIMD extension for IJG JPEG library - * Copyright (C) 1999-2006, MIYASAKA Masaru. - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ - -/* This file is included by jccolor-mmi.c */ - - -#if RGB_RED == 0 -#define mmA mm0 -#define mmB mm1 -#elif RGB_GREEN == 0 -#define mmA mm2 -#define mmB mm3 -#elif RGB_BLUE == 0 -#define mmA mm4 -#define mmB mm5 -#else -#define mmA mm6 -#define mmB mm7 -#endif - -#if RGB_RED == 1 -#define mmC mm0 -#define mmD mm1 -#elif RGB_GREEN == 1 -#define mmC mm2 -#define mmD mm3 -#elif RGB_BLUE == 1 -#define mmC mm4 -#define mmD mm5 -#else -#define mmC mm6 -#define mmD mm7 -#endif - -#if RGB_RED == 2 -#define mmE mm0 -#define mmF mm1 -#elif RGB_GREEN == 2 -#define mmE mm2 -#define mmF mm3 -#elif RGB_BLUE == 2 -#define mmE mm4 -#define mmF mm5 -#else -#define mmE mm6 -#define mmF mm7 -#endif - -#if RGB_RED == 3 -#define mmG mm0 -#define mmH mm1 -#elif RGB_GREEN == 3 -#define mmG mm2 -#define mmH mm3 -#elif RGB_BLUE == 3 -#define mmG mm4 -#define mmH mm5 -#else -#define mmG mm6 -#define mmH mm7 -#endif - - -void jsimd_rgb_ycc_convert_mmi(JDIMENSION image_width, JSAMPARRAY input_buf, - JSAMPIMAGE output_buf, JDIMENSION output_row, - int num_rows) -{ - JSAMPROW inptr, outptr0, outptr1, outptr2; - int num_cols, col; - __m64 mm0, mm1, mm2, mm3, mm4, mm5, mm6, mm7; - __m64 wk[7]; - __m64 Y_BG, Cb_RG, Cr_BG; - - while (--num_rows >= 0) { - inptr = *input_buf++; - outptr0 = output_buf[0][output_row]; - outptr1 = output_buf[1][output_row]; - outptr2 = output_buf[2][output_row]; - output_row++; - - for (num_cols = image_width; num_cols > 0; num_cols -= 8, - outptr0 += 8, outptr1 += 8, outptr2 += 8) { - -#if RGB_PIXELSIZE == 3 - - if (num_cols < 8) { - col = num_cols * 3; - asm(".set noreorder\r\n" - - "li $8, 1\r\n" - "move $9, %3\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 1f\r\n" - "nop \r\n" - "subu $9, $9, 1\r\n" - "xor $12, $12, $12\r\n" - "move $13, %5\r\n" - "dadd $13, $13, $9\r\n" - "lbu $12, 0($13)\r\n" - - "1: \r\n" - "li $8, 2\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 2f\r\n" - "nop \r\n" - "subu $9, $9, 2\r\n" - "xor $11, $11, $11\r\n" - "move $13, %5\r\n" - "dadd $13, $13, $9\r\n" - "lhu $11, 0($13)\r\n" - "sll $12, $12, 16\r\n" - "or $12, $12, $11\r\n" - - "2: \r\n" - "dmtc1 $12, %0\r\n" - "li $8, 4\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 3f\r\n" - "nop \r\n" - "subu $9, $9, 4\r\n" - "move $13, %5\r\n" - "dadd $13, $13, $9\r\n" - "lwu $14, 0($13)\r\n" - "dmtc1 $14, %1\r\n" - "dsll32 $12, $12, 0\r\n" - "or $12, $12, $14\r\n" - "dmtc1 $12, %0\r\n" - - "3: \r\n" - "li $8, 8\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 4f\r\n" - "nop \r\n" - "mov.s %1, %0\r\n" - "ldc1 %0, 0(%5)\r\n" - "li $9, 8\r\n" - "j 5f\r\n" - "nop \r\n" - - "4: \r\n" - "li $8, 16\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 5f\r\n" - "nop \r\n" - "mov.s %2, %0\r\n" - "ldc1 %0, 0(%5)\r\n" - "ldc1 %1, 8(%5)\r\n" - - "5: \r\n" - "nop \r\n" - ".set reorder\r\n" - - : "=f" (mmA), "=f" (mmG), "=f" (mmF) - : "r" (col), "r" (num_rows), "r" (inptr) - : "$f0", "$f2", "$f4", "$8", "$9", "$10", "$11", "$12", "$13", - "$14", "memory" - ); - } else { - if (!(((long)inptr) & 7)) { - mmA = _mm_load_si64((__m64 *)&inptr[0]); - mmG = _mm_load_si64((__m64 *)&inptr[8]); - mmF = _mm_load_si64((__m64 *)&inptr[16]); - } else { - mmA = _mm_loadu_si64((__m64 *)&inptr[0]); - mmG = _mm_loadu_si64((__m64 *)&inptr[8]); - mmF = _mm_loadu_si64((__m64 *)&inptr[16]); - } - inptr += RGB_PIXELSIZE * 8; - } - mmD = mmA; - mmA = _mm_slli_si64(mmA, 4 * BYTE_BIT); - mmD = _mm_srli_si64(mmD, 4 * BYTE_BIT); - - mmA = _mm_unpackhi_pi8(mmA, mmG); - mmG = _mm_slli_si64(mmG, 4 * BYTE_BIT); - - mmD = _mm_unpacklo_pi8(mmD, mmF); - mmG = _mm_unpackhi_pi8(mmG, mmF); - - mmE = mmA; - mmA = _mm_slli_si64(mmA, 4 * BYTE_BIT); - mmE = _mm_srli_si64(mmE, 4 * BYTE_BIT); - - mmA = _mm_unpackhi_pi8(mmA, mmD); - mmD = _mm_slli_si64(mmD, 4 * BYTE_BIT); - - mmE = _mm_unpacklo_pi8(mmE, mmG); - mmD = _mm_unpackhi_pi8(mmD, mmG); - mmC = mmA; - mmA = _mm_loadlo_pi8_f(mmA); - mmC = _mm_loadhi_pi8_f(mmC); - - mmB = mmE; - mmE = _mm_loadlo_pi8_f(mmE); - mmB = _mm_loadhi_pi8_f(mmB); - - mmF = mmD; - mmD = _mm_loadlo_pi8_f(mmD); - mmF = _mm_loadhi_pi8_f(mmF); - -#else /* RGB_PIXELSIZE == 4 */ - - if (num_cols < 8) { - col = num_cols; - asm(".set noreorder\r\n" - - "li $8, 1\r\n" - "move $9, %4\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 1f\r\n" - "nop \r\n" - "subu $9, $9, 1\r\n" - "dsll $11, $9, 2\r\n" - "move $13, %5\r\n" - "daddu $13, $13, $11\r\n" - "lwc1 %0, 0($13)\r\n" - - "1: \r\n" - "li $8, 2\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 2f\r\n" - "nop \r\n" - "subu $9, $9, 2\r\n" - "dsll $11, $9, 2\r\n" - "move $13, %5\r\n" - "daddu $13, $13, $11\r\n" - "mov.s %1, %0\r\n" - "ldc1 %0, 0($13)\r\n" - - "2: \r\n" - "li $8, 4\r\n" - "and $10, $9, $8\r\n" - "beqz $10, 3f\r\n" - "nop \r\n" - "mov.s %2, %0\r\n" - "mov.s %3, %1\r\n" - "ldc1 %0, 0(%5)\r\n" - "ldc1 %1, 8(%5)\r\n" - - "3: \r\n" - "nop \r\n" - ".set reorder\r\n" - - : "=f" (mmA), "=f" (mmF), "=f" (mmD), "=f" (mmC) - : "r" (col), "r" (inptr) - : "$f0", "$f2", "$8", "$9", "$10", "$11", "$13", "memory" - ); - } else { - if (!(((long)inptr) & 7)) { - mmA = _mm_load_si64((__m64 *)&inptr[0]); - mmF = _mm_load_si64((__m64 *)&inptr[8]); - mmD = _mm_load_si64((__m64 *)&inptr[16]); - mmC = _mm_load_si64((__m64 *)&inptr[24]); - } else { - mmA = _mm_loadu_si64((__m64 *)&inptr[0]); - mmF = _mm_loadu_si64((__m64 *)&inptr[8]); - mmD = _mm_loadu_si64((__m64 *)&inptr[16]); - mmC = _mm_loadu_si64((__m64 *)&inptr[24]); - } - inptr += RGB_PIXELSIZE * 8; - } - mmB = mmA; - mmA = _mm_unpacklo_pi8(mmA, mmF); - mmB = _mm_unpackhi_pi8(mmB, mmF); - - mmG = mmD; - mmD = _mm_unpacklo_pi8(mmD, mmC); - mmG = _mm_unpackhi_pi8(mmG, mmC); - - mmE = mmA; - mmA = _mm_unpacklo_pi16(mmA, mmD); - mmE = _mm_unpackhi_pi16(mmE, mmD); - - mmH = mmB; - mmB = _mm_unpacklo_pi16(mmB, mmG); - mmH = _mm_unpackhi_pi16(mmH, mmG); - - mmC = mmA; - mmA = _mm_loadlo_pi8_f(mmA); - mmC = _mm_loadhi_pi8_f(mmC); - - mmD = mmB; - mmB = _mm_loadlo_pi8_f(mmB); - mmD = _mm_loadhi_pi8_f(mmD); - - mmG = mmE; - mmE = _mm_loadlo_pi8_f(mmE); - mmG = _mm_loadhi_pi8_f(mmG); - - mmF = mmH; - mmF = _mm_unpacklo_pi8(mmF, mmH); - mmH = _mm_unpackhi_pi8(mmH, mmH); - mmF = _mm_srli_pi16(mmF, BYTE_BIT); - mmH = _mm_srli_pi16(mmH, BYTE_BIT); - -#endif - - wk[0] = mm0; - wk[1] = mm1; - wk[2] = mm4; - wk[3] = mm5; - - mm6 = mm1; - mm1 = _mm_unpacklo_pi16(mm1, mm3); - mm6 = _mm_unpackhi_pi16(mm6, mm3); - mm7 = mm1; - mm4 = mm6; - mm1 = _mm_madd_pi16(mm1, PW_F0299_F0337); - mm6 = _mm_madd_pi16(mm6, PW_F0299_F0337); - mm7 = _mm_madd_pi16(mm7, PW_MF016_MF033); - mm4 = _mm_madd_pi16(mm4, PW_MF016_MF033); - - wk[4] = mm1; - wk[5] = mm6; - - mm1 = _mm_loadlo_pi16_f(mm5); - mm6 = _mm_loadhi_pi16_f(mm5); - mm1 = _mm_srli_pi32(mm1, 1); - mm6 = _mm_srli_pi32(mm6, 1); - - mm5 = PD_ONEHALFM1_CJ; - mm7 = _mm_add_pi32(mm7, mm1); - mm4 = _mm_add_pi32(mm4, mm6); - mm7 = _mm_add_pi32(mm7, mm5); - mm4 = _mm_add_pi32(mm4, mm5); - mm7 = _mm_srli_pi32(mm7, SCALEBITS); - mm4 = _mm_srli_pi32(mm4, SCALEBITS); - mm7 = _mm_packs_pi32(mm7, mm4); - - mm1 = wk[2]; - mm6 = mm0; - mm0 = _mm_unpacklo_pi16(mm0, mm2); - mm6 = _mm_unpackhi_pi16(mm6, mm2); - mm5 = mm0; - mm4 = mm6; - mm0 = _mm_madd_pi16(mm0, PW_F0299_F0337); - mm6 = _mm_madd_pi16(mm6, PW_F0299_F0337); - mm5 = _mm_madd_pi16(mm5, PW_MF016_MF033); - mm4 = _mm_madd_pi16(mm4, PW_MF016_MF033); - - wk[6] = mm0; - wk[7] = mm6; - mm0 = _mm_loadlo_pi16_f(mm1); - mm6 = _mm_loadhi_pi16_f(mm1); - mm0 = _mm_srli_pi32(mm0, 1); - mm6 = _mm_srli_pi32(mm6, 1); - - mm1 = PD_ONEHALFM1_CJ; - mm5 = _mm_add_pi32(mm5, mm0); - mm4 = _mm_add_pi32(mm4, mm6); - mm5 = _mm_add_pi32(mm5, mm1); - mm4 = _mm_add_pi32(mm4, mm1); - mm5 = _mm_srli_pi32(mm5, SCALEBITS); - mm4 = _mm_srli_pi32(mm4, SCALEBITS); - mm5 = _mm_packs_pi32(mm5, mm4); - - mm7 = _mm_slli_pi16(mm7, BYTE_BIT); - mm5 = _mm_or_si64(mm5, mm7); - Cb_RG = mm5; - - mm0 = wk[3]; - mm6 = wk[2]; - mm1 = wk[1]; - - mm4 = mm0; - mm0 = _mm_unpacklo_pi16(mm0, mm3); - mm4 = _mm_unpackhi_pi16(mm4, mm3); - mm7 = mm0; - mm5 = mm4; - mm0 = _mm_madd_pi16(mm0, PW_F0114_F0250); - mm4 = _mm_madd_pi16(mm4, PW_F0114_F0250); - mm7 = _mm_madd_pi16(mm7, PW_MF008_MF041); - mm5 = _mm_madd_pi16(mm5, PW_MF008_MF041); - - mm3 = PD_ONEHALF; - mm0 = _mm_add_pi32(mm0, wk[4]); - mm4 = _mm_add_pi32(mm4, wk[5]); - mm0 = _mm_add_pi32(mm0, mm3); - mm4 = _mm_add_pi32(mm4, mm3); - mm0 = _mm_srli_pi32(mm0, SCALEBITS); - mm4 = _mm_srli_pi32(mm4, SCALEBITS); - mm0 = _mm_packs_pi32(mm0, mm4); - - mm3 = _mm_loadlo_pi16_f(mm1); - mm4 = _mm_loadhi_pi16_f(mm1); - mm3 = _mm_srli_pi32(mm3, 1); - mm4 = _mm_srli_pi32(mm4, 1); - - mm1 = PD_ONEHALFM1_CJ; - mm7 = _mm_add_pi32(mm7, mm3); - mm5 = _mm_add_pi32(mm5, mm4); - mm7 = _mm_add_pi32(mm7, mm1); - mm5 = _mm_add_pi32(mm5, mm1); - mm7 = _mm_srli_pi32(mm7, SCALEBITS); - mm5 = _mm_srli_pi32(mm5, SCALEBITS); - mm7 = _mm_packs_pi32(mm7, mm5); - - mm3 = wk[0]; - mm4 = mm6; - mm6 = _mm_unpacklo_pi16(mm6, mm2); - mm4 = _mm_unpackhi_pi16(mm4, mm2); - mm1 = mm6; - mm5 = mm4; - mm6 = _mm_madd_pi16(mm6, PW_F0114_F0250); - mm4 = _mm_madd_pi16(mm4, PW_F0114_F0250); - mm1 = _mm_madd_pi16(mm1, PW_MF008_MF041); - mm5 = _mm_madd_pi16(mm5, PW_MF008_MF041); - - mm2 = PD_ONEHALF; - mm6 = _mm_add_pi32(mm6, wk[6]); - mm4 = _mm_add_pi32(mm4, wk[7]); - mm6 = _mm_add_pi32(mm6, mm2); - mm4 = _mm_add_pi32(mm4, mm2); - mm6 = _mm_srli_pi32(mm6, SCALEBITS); - mm4 = _mm_srli_pi32(mm4, SCALEBITS); - mm6 = _mm_packs_pi32(mm6, mm4); - - mm0 = _mm_slli_pi16(mm0, BYTE_BIT); - mm6 = _mm_or_si64(mm6, mm0); - Y_BG = mm6; - - mm2 = _mm_loadlo_pi16_f(mm3); - mm4 = _mm_loadhi_pi16_f(mm3); - mm2 = _mm_srli_pi32(mm2, 1); - mm4 = _mm_srli_pi32(mm4, 1); - - mm0 = PD_ONEHALFM1_CJ; - mm1 = _mm_add_pi32(mm1, mm2); - mm5 = _mm_add_pi32(mm5, mm4); - mm1 = _mm_add_pi32(mm1, mm0); - mm5 = _mm_add_pi32(mm5, mm0); - mm1 = _mm_srli_pi32(mm1, SCALEBITS); - mm5 = _mm_srli_pi32(mm5, SCALEBITS); - mm1 = _mm_packs_pi32(mm1, mm5); - - mm7 = _mm_slli_pi16(mm7, BYTE_BIT); - mm1 = _mm_or_si64(mm1, mm7); - Cr_BG = mm1; - - _mm_store_si64((__m64 *)&outptr0[0], Y_BG); - _mm_store_si64((__m64 *)&outptr1[0], Cb_RG); - _mm_store_si64((__m64 *)&outptr2[0], Cr_BG); - } - } -} - -#undef mmA -#undef mmB -#undef mmC -#undef mmD -#undef mmE -#undef mmF -#undef mmG -#undef mmH diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jdcolext-mmi.c b/third-party/mozjpeg/mozjpeg/simd/loongson/jdcolext-mmi.c deleted file mode 100644 index 560d9b02278..00000000000 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jdcolext-mmi.c +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Loongson MMI optimizations for libjpeg-turbo - * - * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2015, D. R. Commander. All Rights Reserved. - * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. - * All Rights Reserved. - * Authors: ZhuChen - * SunZhangzhi - * CaiWanwei - * - * Based on the x86 SIMD extension for IJG JPEG library - * Copyright (C) 1999-2006, MIYASAKA Masaru. - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ - -/* This file is included by jdcolor-mmi.c */ - - -#if RGB_RED == 0 -#define mmA mm0 -#define mmB mm1 -#elif RGB_GREEN == 0 -#define mmA mm2 -#define mmB mm3 -#elif RGB_BLUE == 0 -#define mmA mm4 -#define mmB mm5 -#else -#define mmA mm6 -#define mmB mm7 -#endif - -#if RGB_RED == 1 -#define mmC mm0 -#define mmD mm1 -#elif RGB_GREEN == 1 -#define mmC mm2 -#define mmD mm3 -#elif RGB_BLUE == 1 -#define mmC mm4 -#define mmD mm5 -#else -#define mmC mm6 -#define mmD mm7 -#endif - -#if RGB_RED == 2 -#define mmE mm0 -#define mmF mm1 -#elif RGB_GREEN == 2 -#define mmE mm2 -#define mmF mm3 -#elif RGB_BLUE == 2 -#define mmE mm4 -#define mmF mm5 -#else -#define mmE mm6 -#define mmF mm7 -#endif - -#if RGB_RED == 3 -#define mmG mm0 -#define mmH mm1 -#elif RGB_GREEN == 3 -#define mmG mm2 -#define mmH mm3 -#elif RGB_BLUE == 3 -#define mmG mm4 -#define mmH mm5 -#else -#define mmG mm6 -#define mmH mm7 -#endif - - -void jsimd_ycc_rgb_convert_mmi(JDIMENSION out_width, JSAMPIMAGE input_buf, - JDIMENSION input_row, JSAMPARRAY output_buf, - int num_rows) -{ - JSAMPROW outptr, inptr0, inptr1, inptr2; - int num_cols, col; - __m64 mm0, mm1, mm2, mm3, mm4, mm5, mm6, mm7; - __m64 mm8, wk[2]; - - while (--num_rows >= 0) { - inptr0 = input_buf[0][input_row]; - inptr1 = input_buf[1][input_row]; - inptr2 = input_buf[2][input_row]; - input_row++; - outptr = *output_buf++; - - for (num_cols = out_width; num_cols > 0; num_cols -= 8, - inptr0 += 8, inptr1 += 8, inptr2 += 8) { - - mm5 = _mm_load_si64((__m64 *)inptr1); - mm1 = _mm_load_si64((__m64 *)inptr2); - mm8 = _mm_load_si64((__m64 *)inptr0); - mm4 = 0; - mm7 = 0; - mm4 = _mm_cmpeq_pi16(mm4, mm4); - mm7 = _mm_cmpeq_pi16(mm7, mm7); - mm4 = _mm_srli_pi16(mm4, BYTE_BIT); - mm7 = _mm_slli_pi16(mm7, 7); /* mm7={0xFF80 0xFF80 0xFF80 0xFF80} */ - mm0 = mm4; /* mm0=mm4={0xFF 0x00 0xFF 0x00 ..} */ - - mm4 = _mm_and_si64(mm4, mm5); /* mm4=Cb(0246)=CbE */ - mm5 = _mm_srli_pi16(mm5, BYTE_BIT); /* mm5=Cb(1357)=CbO */ - mm0 = _mm_and_si64(mm0, mm1); /* mm0=Cr(0246)=CrE */ - mm1 = _mm_srli_pi16(mm1, BYTE_BIT); /* mm1=Cr(1357)=CrO */ - mm4 = _mm_add_pi16(mm4, mm7); - mm5 = _mm_add_pi16(mm5, mm7); - mm0 = _mm_add_pi16(mm0, mm7); - mm1 = _mm_add_pi16(mm1, mm7); - - /* (Original) - * R = Y + 1.40200 * Cr - * G = Y - 0.34414 * Cb - 0.71414 * Cr - * B = Y + 1.77200 * Cb - * - * (This implementation) - * R = Y + 0.40200 * Cr + Cr - * G = Y - 0.34414 * Cb + 0.28586 * Cr - Cr - * B = Y - 0.22800 * Cb + Cb + Cb - */ - - mm2 = mm4; /* mm2 = CbE */ - mm3 = mm5; /* mm3 = CbO */ - mm4 = _mm_add_pi16(mm4, mm4); /* mm4 = 2*CbE */ - mm5 = _mm_add_pi16(mm5, mm5); /* mm5 = 2*CbO */ - mm6 = mm0; /* mm6 = CrE */ - mm7 = mm1; /* mm7 = CrO */ - mm0 = _mm_add_pi16(mm0, mm0); /* mm0 = 2*CrE */ - mm1 = _mm_add_pi16(mm1, mm1); /* mm1 = 2*CrO */ - - mm4 = _mm_mulhi_pi16(mm4, PW_MF0228); /* mm4=(2*CbE * -FIX(0.22800) */ - mm5 = _mm_mulhi_pi16(mm5, PW_MF0228); /* mm5=(2*CbO * -FIX(0.22800) */ - mm0 = _mm_mulhi_pi16(mm0, PW_F0402); /* mm0=(2*CrE * FIX(0.40200)) */ - mm1 = _mm_mulhi_pi16(mm1, PW_F0402); /* mm1=(2*CrO * FIX(0.40200)) */ - - mm4 = _mm_add_pi16(mm4, PW_ONE); - mm5 = _mm_add_pi16(mm5, PW_ONE); - mm4 = _mm_srai_pi16(mm4, 1); /* mm4=(CbE * -FIX(0.22800)) */ - mm5 = _mm_srai_pi16(mm5, 1); /* mm5=(CbO * -FIX(0.22800)) */ - mm0 = _mm_add_pi16(mm0, PW_ONE); - mm1 = _mm_add_pi16(mm1, PW_ONE); - mm0 = _mm_srai_pi16(mm0, 1); /* mm0=(CrE * FIX(0.40200)) */ - mm1 = _mm_srai_pi16(mm1, 1); /* mm1=(CrO * FIX(0.40200)) */ - - mm4 = _mm_add_pi16(mm4, mm2); - mm5 = _mm_add_pi16(mm5, mm3); - mm4 = _mm_add_pi16(mm4, mm2); /* mm4=(CbE * FIX(1.77200))=(B-Y)E */ - mm5 = _mm_add_pi16(mm5, mm3); /* mm5=(CbO * FIX(1.77200))=(B-Y)O */ - mm0 = _mm_add_pi16(mm0, mm6); /* mm0=(CrE * FIX(1.40200))=(R-Y)E */ - mm1 = _mm_add_pi16(mm1, mm7); /* mm1=(CrO * FIX(1.40200))=(R-Y)O */ - - wk[0] = mm4; /* wk(0)=(B-Y)E */ - wk[1] = mm5; /* wk(1)=(B-Y)O */ - - mm4 = mm2; - mm5 = mm3; - mm2 = _mm_unpacklo_pi16(mm2, mm6); - mm4 = _mm_unpackhi_pi16(mm4, mm6); - mm2 = _mm_madd_pi16(mm2, PW_MF0344_F0285); - mm4 = _mm_madd_pi16(mm4, PW_MF0344_F0285); - mm3 = _mm_unpacklo_pi16(mm3, mm7); - mm5 = _mm_unpackhi_pi16(mm5, mm7); - mm3 = _mm_madd_pi16(mm3, PW_MF0344_F0285); - mm5 = _mm_madd_pi16(mm5, PW_MF0344_F0285); - - mm2 = _mm_add_pi32(mm2, PD_ONEHALF); - mm4 = _mm_add_pi32(mm4, PD_ONEHALF); - mm2 = _mm_srai_pi32(mm2, SCALEBITS); - mm4 = _mm_srai_pi32(mm4, SCALEBITS); - mm3 = _mm_add_pi32(mm3, PD_ONEHALF); - mm5 = _mm_add_pi32(mm5, PD_ONEHALF); - mm3 = _mm_srai_pi32(mm3, SCALEBITS); - mm5 = _mm_srai_pi32(mm5, SCALEBITS); - - mm2 = _mm_packs_pi32(mm2, mm4); /* mm2=CbE*-FIX(0.344)+CrE*FIX(0.285) */ - mm3 = _mm_packs_pi32(mm3, mm5); /* mm3=CbO*-FIX(0.344)+CrO*FIX(0.285) */ - mm2 = _mm_sub_pi16(mm2, mm6); /* mm2=CbE*-FIX(0.344)+CrE*-FIX(0.714)=(G-Y)E */ - mm3 = _mm_sub_pi16(mm3, mm7); /* mm3=CbO*-FIX(0.344)+CrO*-FIX(0.714)=(G-Y)O */ - - mm5 = mm8; /* mm5=Y(01234567) */ - - mm4 = _mm_cmpeq_pi16(mm4, mm4); - mm4 = _mm_srli_pi16(mm4, BYTE_BIT); /* mm4={0xFF 0x00 0xFF 0x00 ..} */ - mm4 = _mm_and_si64(mm4, mm5); /* mm4=Y(0246)=YE */ - mm5 = _mm_srli_pi16(mm5, BYTE_BIT); /* mm5=Y(1357)=YO */ - - mm0 = _mm_add_pi16(mm0, mm4); /* mm0=((R-Y)E+YE)=RE=(R0 R2 R4 R6) */ - mm1 = _mm_add_pi16(mm1, mm5); /* mm1=((R-Y)O+YO)=RO=(R1 R3 R5 R7) */ - mm0 = _mm_packs_pu16(mm0, mm0); /* mm0=(R0 R2 R4 R6 ** ** ** **) */ - mm1 = _mm_packs_pu16(mm1, mm1); /* mm1=(R1 R3 R5 R7 ** ** ** **) */ - - mm2 = _mm_add_pi16(mm2, mm4); /* mm2=((G-Y)E+YE)=GE=(G0 G2 G4 G6) */ - mm3 = _mm_add_pi16(mm3, mm5); /* mm3=((G-Y)O+YO)=GO=(G1 G3 G5 G7) */ - mm2 = _mm_packs_pu16(mm2, mm2); /* mm2=(G0 G2 G4 G6 ** ** ** **) */ - mm3 = _mm_packs_pu16(mm3, mm3); /* mm3=(G1 G3 G5 G7 ** ** ** **) */ - - mm4 = _mm_add_pi16(mm4, wk[0]); /* mm4=(YE+(B-Y)E)=BE=(B0 B2 B4 B6) */ - mm5 = _mm_add_pi16(mm5, wk[1]); /* mm5=(YO+(B-Y)O)=BO=(B1 B3 B5 B7) */ - mm4 = _mm_packs_pu16(mm4, mm4); /* mm4=(B0 B2 B4 B6 ** ** ** **) */ - mm5 = _mm_packs_pu16(mm5, mm5); /* mm5=(B1 B3 B5 B7 ** ** ** **) */ - -#if RGB_PIXELSIZE == 3 - - /* mmA=(00 02 04 06 ** ** ** **), mmB=(01 03 05 07 ** ** ** **) */ - /* mmC=(10 12 14 16 ** ** ** **), mmD=(11 13 15 17 ** ** ** **) */ - mmA = _mm_unpacklo_pi8(mmA, mmC); /* mmA=(00 10 02 12 04 14 06 16) */ - mmE = _mm_unpacklo_pi8(mmE, mmB); /* mmE=(20 01 22 03 24 05 26 07) */ - mmD = _mm_unpacklo_pi8(mmD, mmF); /* mmD=(11 21 13 23 15 25 17 27) */ - - mmG = mmA; - mmH = mmA; - mmA = _mm_unpacklo_pi16(mmA, mmE); /* mmA=(00 10 20 01 02 12 22 03) */ - mmG = _mm_unpackhi_pi16(mmG, mmE); /* mmG=(04 14 24 05 06 16 26 07) */ - - mmH = _mm_srli_si64(mmH, 2 * BYTE_BIT); - mmE = _mm_srli_si64(mmE, 2 * BYTE_BIT); - - mmC = mmD; - mmB = mmD; - mmD = _mm_unpacklo_pi16(mmD, mmH); /* mmD=(11 21 02 12 13 23 04 14) */ - mmC = _mm_unpackhi_pi16(mmC, mmH); /* mmC=(15 25 06 16 17 27 -- --) */ - - mmB = _mm_srli_si64(mmB, 2 * BYTE_BIT); /* mmB=(13 23 15 25 17 27 -- --) */ - - mmF = mmE; - mmE = _mm_unpacklo_pi16(mmE, mmB); /* mmE=(22 03 13 23 24 05 15 25) */ - mmF = _mm_unpackhi_pi16(mmF, mmB); /* mmF=(26 07 17 27 -- -- -- --) */ - - mmA = _mm_unpacklo_pi32(mmA, mmD); /* mmA=(00 10 20 01 11 21 02 12) */ - mmE = _mm_unpacklo_pi32(mmE, mmG); /* mmE=(22 03 13 23 04 14 24 05) */ - mmC = _mm_unpacklo_pi32(mmC, mmF); /* mmC=(15 25 06 16 26 07 17 27) */ - - if (num_cols >= 8) { - _mm_store_si64((__m64 *)outptr, mmA); - _mm_store_si64((__m64 *)(outptr + 8), mmE); - _mm_store_si64((__m64 *)(outptr + 16), mmC); - outptr += RGB_PIXELSIZE * 8; - } else { - col = num_cols * 3; - asm(".set noreorder\r\n" - - "li $8, 16\r\n" - "move $9, %4\r\n" - "mov.s $f4, %1\r\n" - "mov.s $f6, %3\r\n" - "move $10, %5\r\n" - "bltu $9, $8, 1f\r\n" - "nop \r\n" - "gssdlc1 $f4, 7($10)\r\n" - "gssdrc1 $f4, 0($10)\r\n" - "gssdlc1 $f6, 7+8($10)\r\n" - "gssdrc1 $f6, 8($10)\r\n" - "mov.s $f4, %2\r\n" - "subu $9, $9, 16\r\n" - "daddu $10, $10, 16\r\n" - "b 2f\r\n" - "nop \r\n" - - "1: \r\n" - "li $8, 8\r\n" /* st8 */ - "bltu $9, $8, 2f\r\n" - "nop \r\n" - "gssdlc1 $f4, 7($10)\r\n" - "gssdrc1 $f4, ($10)\r\n" - "mov.s $f4, %3\r\n" - "subu $9, $9, 8\r\n" - "daddu $10, $10, 8\r\n" - - "2: \r\n" - "li $8, 4\r\n" /* st4 */ - "mfc1 $11, $f4\r\n" - "bltu $9, $8, 3f\r\n" - "nop \r\n" - "swl $11, 3($10)\r\n" - "swr $11, 0($10)\r\n" - "li $8, 32\r\n" - "mtc1 $8, $f6\r\n" - "dsrl $f4, $f4, $f6\r\n" - "mfc1 $11, $f4\r\n" - "subu $9, $9, 4\r\n" - "daddu $10, $10, 4\r\n" - - "3: \r\n" - "li $8, 2\r\n" /* st2 */ - "bltu $9, $8, 4f\r\n" - "nop \r\n" - "ush $11, 0($10)\r\n" - "srl $11, 16\r\n" - "subu $9, $9, 2\r\n" - "daddu $10, $10, 2\r\n" - - "4: \r\n" - "li $8, 1\r\n" /* st1 */ - "bltu $9, $8, 5f\r\n" - "nop \r\n" - "sb $11, 0($10)\r\n" - - "5: \r\n" - "nop \r\n" /* end */ - : "=m" (*outptr) - : "f" (mmA), "f" (mmC), "f" (mmE), "r" (col), "r" (outptr) - : "$f4", "$f6", "$8", "$9", "$10", "$11", "memory" - ); - } - -#else /* RGB_PIXELSIZE == 4 */ - -#ifdef RGBX_FILLER_0XFF - mm6 = _mm_cmpeq_pi8(mm6, mm6); - mm7 = _mm_cmpeq_pi8(mm7, mm7); -#else - mm6 = _mm_xor_si64(mm6, mm6); - mm7 = _mm_xor_si64(mm7, mm7); -#endif - /* mmA=(00 02 04 06 ** ** ** **), mmB=(01 03 05 07 ** ** ** **) */ - /* mmC=(10 12 14 16 ** ** ** **), mmD=(11 13 15 17 ** ** ** **) */ - /* mmE=(20 22 24 26 ** ** ** **), mmF=(21 23 25 27 ** ** ** **) */ - /* mmG=(30 32 34 36 ** ** ** **), mmH=(31 33 35 37 ** ** ** **) */ - - mmA = _mm_unpacklo_pi8(mmA, mmC); /* mmA=(00 10 02 12 04 14 06 16) */ - mmE = _mm_unpacklo_pi8(mmE, mmG); /* mmE=(20 30 22 32 24 34 26 36) */ - mmB = _mm_unpacklo_pi8(mmB, mmD); /* mmB=(01 11 03 13 05 15 07 17) */ - mmF = _mm_unpacklo_pi8(mmF, mmH); /* mmF=(21 31 23 33 25 35 27 37) */ - - mmC = mmA; - mmA = _mm_unpacklo_pi16(mmA, mmE); /* mmA=(00 10 20 30 02 12 22 32) */ - mmC = _mm_unpackhi_pi16(mmC, mmE); /* mmC=(04 14 24 34 06 16 26 36) */ - mmG = mmB; - mmB = _mm_unpacklo_pi16(mmB, mmF); /* mmB=(01 11 21 31 03 13 23 33) */ - mmG = _mm_unpackhi_pi16(mmG, mmF); /* mmG=(05 15 25 35 07 17 27 37) */ - - mmD = mmA; - mmA = _mm_unpacklo_pi32(mmA, mmB); /* mmA=(00 10 20 30 01 11 21 31) */ - mmD = _mm_unpackhi_pi32(mmD, mmB); /* mmD=(02 12 22 32 03 13 23 33) */ - mmH = mmC; - mmC = _mm_unpacklo_pi32(mmC, mmG); /* mmC=(04 14 24 34 05 15 25 35) */ - mmH = _mm_unpackhi_pi32(mmH, mmG); /* mmH=(06 16 26 36 07 17 27 37) */ - - if (num_cols >= 8) { - _mm_store_si64((__m64 *)outptr, mmA); - _mm_store_si64((__m64 *)(outptr + 8), mmD); - _mm_store_si64((__m64 *)(outptr + 16), mmC); - _mm_store_si64((__m64 *)(outptr + 24), mmH); - outptr += RGB_PIXELSIZE * 8; - } else { - col = num_cols; - asm(".set noreorder\r\n" /* st16 */ - - "li $8, 4\r\n" - "move $9, %6\r\n" - "move $10, %7\r\n" - "mov.s $f4, %2\r\n" - "mov.s $f6, %4\r\n" - "bltu $9, $8, 1f\r\n" - "nop \r\n" - "gssdlc1 $f4, 7($10)\r\n" - "gssdrc1 $f4, ($10)\r\n" - "gssdlc1 $f6, 7+8($10)\r\n" - "gssdrc1 $f6, 8($10)\r\n" - "mov.s $f4, %3\r\n" - "mov.s $f6, %5\r\n" - "subu $9, $9, 4\r\n" - "daddu $10, $10, 16\r\n" - - "1: \r\n" - "li $8, 2\r\n" /* st8 */ - "bltu $9, $8, 2f\r\n" - "nop \r\n" - "gssdlc1 $f4, 7($10)\r\n" - "gssdrc1 $f4, 0($10)\r\n" - "mov.s $f4, $f6\r\n" - "subu $9, $9, 2\r\n" - "daddu $10, $10, 8\r\n" - - "2: \r\n" - "li $8, 1\r\n" /* st4 */ - "bltu $9, $8, 3f\r\n" - "nop \r\n" - "gsswlc1 $f4, 3($10)\r\n" - "gsswrc1 $f4, 0($10)\r\n" - - "3: \r\n" - "li %1, 0\r\n" /* end */ - : "=m" (*outptr), "=r" (col) - : "f" (mmA), "f" (mmC), "f" (mmD), "f" (mmH), "r" (col), - "r" (outptr) - : "$f4", "$f6", "$8", "$9", "$10", "memory" - ); - } - -#endif - - } - } -} - -#undef mmA -#undef mmB -#undef mmC -#undef mmD -#undef mmE -#undef mmF -#undef mmG -#undef mmH diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jdsample-mmi.c b/third-party/mozjpeg/mozjpeg/simd/loongson/jdsample-mmi.c deleted file mode 100644 index 00a6265176e..00000000000 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jdsample-mmi.c +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Loongson MMI optimizations for libjpeg-turbo - * - * Copyright (C) 2015, 2018, D. R. Commander. All Rights Reserved. - * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. - * All Rights Reserved. - * Authors: ZhuChen - * CaiWanwei - * SunZhangzhi - * - * Based on the x86 SIMD extension for IJG JPEG library - * Copyright (C) 1999-2006, MIYASAKA Masaru. - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ - -/* CHROMA UPSAMPLING */ - -#include "jsimd_mmi.h" - - -enum const_index { - index_PW_THREE, - index_PW_SEVEN, - index_PW_EIGHT, -}; - -static uint64_t const_value[] = { - _uint64_set_pi16(3, 3, 3, 3), - _uint64_set_pi16(7, 7, 7, 7), - _uint64_set_pi16(8, 8, 8, 8), -}; - -#define PW_THREE get_const_value(index_PW_THREE) -#define PW_SEVEN get_const_value(index_PW_SEVEN) -#define PW_EIGHT get_const_value(index_PW_EIGHT) - - -#define PROCESS_ROW(r) { \ - mm7 = _mm_load_si64((__m64 *)outptr##r); /* mm7=IntrL=( 0 1 2 3) */ \ - mm3 = _mm_load_si64((__m64 *)outptr##r + 1); /* mm3=IntrH=( 4 5 6 7) */ \ - \ - mm0 = mm7; \ - mm4 = mm3; \ - mm0 = _mm_srli_si64(mm0, 2 * BYTE_BIT); /* mm0=( 1 2 3 -) */ \ - mm4 = _mm_slli_si64(mm4, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* mm4=( - - - 4) */ \ - mm5 = mm7; \ - mm6 = mm3; \ - mm5 = _mm_srli_si64(mm5, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* mm5=( 3 - - -) */ \ - mm6 = _mm_slli_si64(mm6, 2 * BYTE_BIT); /* mm6=( - 4 5 6) */ \ - \ - mm0 = _mm_or_si64(mm0, mm4); /* mm0=( 1 2 3 4) */ \ - mm5 = _mm_or_si64(mm5, mm6); /* mm5=( 3 4 5 6) */ \ - \ - mm1 = mm7; \ - mm2 = mm3; \ - mm1 = _mm_slli_si64(mm1, 2 * BYTE_BIT); /* mm1=( - 0 1 2) */ \ - mm2 = _mm_srli_si64(mm2, 2 * BYTE_BIT); /* mm2=( 5 6 7 -) */ \ - mm4 = mm3; \ - mm4 = _mm_srli_si64(mm4, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* mm4=( 7 - - -) */ \ - \ - mm1 = _mm_or_si64(mm1, wk[r]); /* mm1=(-1 0 1 2) */ \ - mm2 = _mm_or_si64(mm2, wk[r + 2]); /* mm2=( 5 6 6 8) */ \ - \ - wk[r] = mm4; \ - \ - mm7 = _mm_mullo_pi16(mm7, PW_THREE); \ - mm3 = _mm_mullo_pi16(mm3, PW_THREE); \ - mm1 = _mm_add_pi16(mm1, PW_EIGHT); \ - mm5 = _mm_add_pi16(mm5, PW_EIGHT); \ - mm0 = _mm_add_pi16(mm0, PW_SEVEN); \ - mm2 = _mm_add_pi16(mm2, PW_SEVEN); \ - \ - mm1 = _mm_add_pi16(mm1, mm7); \ - mm5 = _mm_add_pi16(mm5, mm3); \ - mm1 = _mm_srli_pi16(mm1, 4); /* mm1=OutrLE=( 0 2 4 6) */ \ - mm5 = _mm_srli_pi16(mm5, 4); /* mm5=OutrHE=( 8 10 12 14) */ \ - mm0 = _mm_add_pi16(mm0, mm7); \ - mm2 = _mm_add_pi16(mm2, mm3); \ - mm0 = _mm_srli_pi16(mm0, 4); /* mm0=OutrLO=( 1 3 5 7) */ \ - mm2 = _mm_srli_pi16(mm2, 4); /* mm2=OutrHO=( 9 11 13 15) */ \ - \ - mm0 = _mm_slli_pi16(mm0, BYTE_BIT); \ - mm2 = _mm_slli_pi16(mm2, BYTE_BIT); \ - mm1 = _mm_or_si64(mm1, mm0); /* mm1=OutrL=( 0 1 2 3 4 5 6 7) */ \ - mm5 = _mm_or_si64(mm5, mm2); /* mm5=OutrH=( 8 9 10 11 12 13 14 15) */ \ - \ - _mm_store_si64((__m64 *)outptr##r, mm1); \ - _mm_store_si64((__m64 *)outptr##r + 1, mm5); \ -} - -void jsimd_h2v2_fancy_upsample_mmi(int max_v_samp_factor, - JDIMENSION downsampled_width, - JSAMPARRAY input_data, - JSAMPARRAY *output_data_ptr) -{ - JSAMPARRAY output_data = *output_data_ptr; - JSAMPROW inptr_1, inptr0, inptr1, outptr0, outptr1; - int inrow, outrow, incol, tmp, tmp1; - __m64 mm0, mm1, mm2, mm3 = 0.0, mm4, mm5, mm6, mm7 = 0.0; - __m64 wk[4], mm_tmp; - - for (inrow = 0, outrow = 0; outrow < max_v_samp_factor; inrow++) { - - inptr_1 = input_data[inrow - 1]; - inptr0 = input_data[inrow]; - inptr1 = input_data[inrow + 1]; - outptr0 = output_data[outrow++]; - outptr1 = output_data[outrow++]; - - if (downsampled_width & 7) { - tmp = (downsampled_width - 1) * sizeof(JSAMPLE); - tmp1 = downsampled_width * sizeof(JSAMPLE); - asm("daddu $8, %3, %6\r\n" - "lb $9, ($8)\r\n" - "daddu $8, %3, %7\r\n" - "sb $9, ($8)\r\n" - "daddu $8, %4, %6\r\n" - "lb $9, ($8)\r\n" - "daddu $8, %4, %7\r\n" - "sb $9, ($8)\r\n" - "daddu $8, %5, %6\r\n" - "lb $9, ($8)\r\n" - "daddu $8, %5, %7\r\n" - "sb $9, ($8)\r\n" - : "=m" (*inptr_1), "=m" (*inptr0), "=m" (*inptr1) - : "r" (inptr_1), "r" (inptr0), "r" (inptr1), "r" (tmp), "r" (tmp1) - : "$8", "$9" - ); - } - - /* process the first column block */ - mm0 = _mm_load_si64((__m64 *)inptr0); /* mm0 = row[ 0][0] */ - mm1 = _mm_load_si64((__m64 *)inptr_1); /* mm1 = row[-1][0] */ - mm2 = _mm_load_si64((__m64 *)inptr1); /* mm2 = row[ 1][0] */ - - mm3 = _mm_xor_si64(mm3, mm3); /* mm3 = (all 0's) */ - mm4 = mm0; - mm0 = _mm_unpacklo_pi8(mm0, mm3); /* mm0 = row[ 0][0]( 0 1 2 3) */ - mm4 = _mm_unpackhi_pi8(mm4, mm3); /* mm4 = row[ 0][0]( 4 5 6 7) */ - mm5 = mm1; - mm1 = _mm_unpacklo_pi8(mm1, mm3); /* mm1 = row[-1][0]( 0 1 2 3) */ - mm5 = _mm_unpackhi_pi8(mm5, mm3); /* mm5 = row[-1][0]( 4 5 6 7) */ - mm6 = mm2; - mm2 = _mm_unpacklo_pi8(mm2, mm3); /* mm2 = row[+1][0]( 0 1 2 3) */ - mm6 = _mm_unpackhi_pi8(mm6, mm3); /* mm6 = row[+1][0]( 4 5 6 7) */ - - mm0 = _mm_mullo_pi16(mm0, PW_THREE); - mm4 = _mm_mullo_pi16(mm4, PW_THREE); - - mm7 = _mm_cmpeq_pi8(mm7, mm7); - mm7 = _mm_srli_si64(mm7, (SIZEOF_MMWORD - 2) * BYTE_BIT); - - mm1 = _mm_add_pi16(mm1, mm0); /* mm1=Int0L=( 0 1 2 3) */ - mm5 = _mm_add_pi16(mm5, mm4); /* mm5=Int0H=( 4 5 6 7) */ - mm2 = _mm_add_pi16(mm2, mm0); /* mm2=Int1L=( 0 1 2 3) */ - mm6 = _mm_add_pi16(mm6, mm4); /* mm6=Int1H=( 4 5 6 7) */ - - _mm_store_si64((__m64 *)outptr0, mm1); /* temporarily save */ - _mm_store_si64((__m64 *)outptr0 + 1, mm5); /* the intermediate data */ - _mm_store_si64((__m64 *)outptr1, mm2); - _mm_store_si64((__m64 *)outptr1 + 1, mm6); - - mm1 = _mm_and_si64(mm1, mm7); /* mm1=( 0 - - -) */ - mm2 = _mm_and_si64(mm2, mm7); /* mm2=( 0 - - -) */ - - wk[0] = mm1; - wk[1] = mm2; - - for (incol = downsampled_width; incol > 0; - incol -= 8, inptr_1 += 8, inptr0 += 8, inptr1 += 8, - outptr0 += 16, outptr1 += 16) { - - if (incol > 8) { - /* process the next column block */ - mm0 = _mm_load_si64((__m64 *)inptr0 + 1); /* mm0 = row[ 0][1] */ - mm1 = _mm_load_si64((__m64 *)inptr_1 + 1); /* mm1 = row[-1][1] */ - mm2 = _mm_load_si64((__m64 *)inptr1 + 1); /* mm2 = row[+1][1] */ - - mm3 = _mm_setzero_si64(); /* mm3 = (all 0's) */ - mm4 = mm0; - mm0 = _mm_unpacklo_pi8(mm0, mm3); /* mm0 = row[ 0][1]( 0 1 2 3) */ - mm4 = _mm_unpackhi_pi8(mm4, mm3); /* mm4 = row[ 0][1]( 4 5 6 7) */ - mm5 = mm1; - mm1 = _mm_unpacklo_pi8(mm1, mm3); /* mm1 = row[-1][1]( 0 1 2 3) */ - mm5 = _mm_unpackhi_pi8(mm5, mm3); /* mm5 = row[-1][1]( 4 5 6 7) */ - mm6 = mm2; - mm2 = _mm_unpacklo_pi8(mm2, mm3); /* mm2 = row[+1][1]( 0 1 2 3) */ - mm6 = _mm_unpackhi_pi8(mm6, mm3); /* mm6 = row[+1][1]( 4 5 6 7) */ - - mm0 = _mm_mullo_pi16(mm0, PW_THREE); - mm4 = _mm_mullo_pi16(mm4, PW_THREE); - - mm1 = _mm_add_pi16(mm1, mm0); /* mm1 = Int0L = ( 0 1 2 3) */ - mm5 = _mm_add_pi16(mm5, mm4); /* mm5 = Int0H = ( 4 5 6 7) */ - mm2 = _mm_add_pi16(mm2, mm0); /* mm2 = Int1L = ( 0 1 2 3) */ - mm6 = _mm_add_pi16(mm6, mm4); /* mm6 = Int1H = ( 4 5 6 7) */ - - _mm_store_si64((__m64 *)outptr0 + 2, mm1); /* temporarily save */ - _mm_store_si64((__m64 *)outptr0 + 3, mm5); /* the intermediate data */ - _mm_store_si64((__m64 *)outptr1 + 2, mm2); - _mm_store_si64((__m64 *)outptr1 + 3, mm6); - - mm1 = _mm_slli_si64(mm1, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* mm1=( - - - 0) */ - mm2 = _mm_slli_si64(mm2, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* mm2=( - - - 0) */ - - wk[2] = mm1; - wk[3] = mm2; - } else { - /* process the last column block */ - mm1 = _mm_cmpeq_pi8(mm1, mm1); - mm1 = _mm_slli_si64(mm1, (SIZEOF_MMWORD - 2) * BYTE_BIT); - mm2 = mm1; - - mm_tmp = _mm_load_si64((__m64 *)outptr0 + 1); - mm1 = _mm_and_si64(mm1, mm_tmp); /* mm1=( - - - 7) */ - mm_tmp = _mm_load_si64((__m64 *)outptr1 + 1); - mm2 = _mm_and_si64(mm2, mm_tmp); /* mm2=( - - - 7) */ - - wk[2] = mm1; - wk[3] = mm2; - } - - /* process the upper row */ - PROCESS_ROW(0) - - /* process the lower row */ - PROCESS_ROW(1) - } - } -} diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jquanti-mmi.c b/third-party/mozjpeg/mozjpeg/simd/loongson/jquanti-mmi.c deleted file mode 100644 index f9a3f819967..00000000000 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jquanti-mmi.c +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Loongson MMI optimizations for libjpeg-turbo - * - * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. - * All Rights Reserved. - * Authors: ZhuChen - * CaiWanwei - * SunZhangzhi - * Copyright (C) 2018, D. R. Commander. All Rights Reserved. - * - * Based on the x86 SIMD extension for IJG JPEG library - * Copyright (C) 1999-2006, MIYASAKA Masaru. - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ - -/* INTEGER QUANTIZATION AND SAMPLE CONVERSION */ - -#include "jsimd_mmi.h" - - -#define DO_QUANT() { \ - mm2 = _mm_load_si64((__m64 *)&workspace[0]); \ - mm3 = _mm_load_si64((__m64 *)&workspace[4]); \ - \ - mm0 = mm2; \ - mm1 = mm3; \ - \ - mm2 = _mm_srai_pi16(mm2, (WORD_BIT - 1)); /* -1 if value < 0, */ \ - /* 0 otherwise */ \ - mm3 = _mm_srai_pi16(mm3, (WORD_BIT - 1)); \ - \ - mm0 = _mm_xor_si64(mm0, mm2); /* val = -val */ \ - mm1 = _mm_xor_si64(mm1, mm3); \ - mm0 = _mm_sub_pi16(mm0, mm2); \ - mm1 = _mm_sub_pi16(mm1, mm3); \ - \ - corr0 = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 1]); /* correction */ \ - corr1 = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 1 + 4]); \ - \ - mm0 = _mm_add_pi16(mm0, corr0); /* correction + roundfactor */ \ - mm1 = _mm_add_pi16(mm1, corr1); \ - \ - mm4 = mm0; \ - mm5 = mm1; \ - \ - recip0 = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 0]); /* reciprocal */ \ - recip1 = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 0 + 4]); \ - \ - mm0 = _mm_mulhi_pi16(mm0, recip0); \ - mm1 = _mm_mulhi_pi16(mm1, recip1); \ - \ - mm0 = _mm_add_pi16(mm0, mm4); /* reciprocal is always negative */ \ - mm1 = _mm_add_pi16(mm1, mm5); /* (MSB=1), so we always need to add the */ \ - /* initial value (input value is never */ \ - /* negative as we inverted it at the */ \ - /* start of this routine) */ \ - \ - scale0 = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 2]); /* scale */ \ - scale1 = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 2 + 4]); \ - \ - mm6 = scale0; \ - mm7 = scale1; \ - mm4 = mm0; \ - mm5 = mm1; \ - \ - mm0 = _mm_mulhi_pi16(mm0, mm6); \ - mm1 = _mm_mulhi_pi16(mm1, mm7); \ - \ - mm6 = _mm_srai_pi16(mm6, (WORD_BIT - 1)); /* determine if scale... */ \ - /* is negative */ \ - mm7 = _mm_srai_pi16(mm7, (WORD_BIT - 1)); \ - \ - mm6 = _mm_and_si64(mm6, mm4); /* and add input if it is */ \ - mm7 = _mm_and_si64(mm7, mm5); \ - mm0 = _mm_add_pi16(mm0, mm6); \ - mm1 = _mm_add_pi16(mm1, mm7); \ - \ - mm4 = _mm_srai_pi16(mm4, (WORD_BIT - 1)); /* then check if... */ \ - mm5 = _mm_srai_pi16(mm5, (WORD_BIT - 1)); /* negative input */ \ - \ - mm4 = _mm_and_si64(mm4, scale0); /* and add scale if it is */ \ - mm5 = _mm_and_si64(mm5, scale1); \ - mm0 = _mm_add_pi16(mm0, mm4); \ - mm1 = _mm_add_pi16(mm1, mm5); \ - \ - mm0 = _mm_xor_si64(mm0, mm2); /* val = -val */ \ - mm1 = _mm_xor_si64(mm1, mm3); \ - mm0 = _mm_sub_pi16(mm0, mm2); \ - mm1 = _mm_sub_pi16(mm1, mm3); \ - \ - _mm_store_si64((__m64 *)&output_ptr[0], mm0); \ - _mm_store_si64((__m64 *)&output_ptr[4], mm1); \ - \ - workspace += DCTSIZE; \ - divisors += DCTSIZE; \ - output_ptr += DCTSIZE; \ -} - - -void jsimd_quantize_mmi(JCOEFPTR coef_block, DCTELEM *divisors, - DCTELEM *workspace) -{ - JCOEFPTR output_ptr = coef_block; - __m64 mm0, mm1, mm2, mm3, mm4, mm5, mm6, mm7; - __m64 corr0, corr1, recip0, recip1, scale0, scale1; - - DO_QUANT() - DO_QUANT() - DO_QUANT() - DO_QUANT() - DO_QUANT() - DO_QUANT() - DO_QUANT() - DO_QUANT() -} diff --git a/third-party/mozjpeg/mozjpeg/simd/mips/jsimd.c b/third-party/mozjpeg/mozjpeg/simd/mips/jsimd.c index 454cc99c6f2..c6e789aa2f2 100644 --- a/third-party/mozjpeg/mozjpeg/simd/mips/jsimd.c +++ b/third-party/mozjpeg/mozjpeg/simd/mips/jsimd.c @@ -2,9 +2,9 @@ * jsimd_mips.c * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009-2011, 2014, 2016, 2018, D. R. Commander. + * Copyright (C) 2009-2011, 2014, 2016, 2018, 2020, 2022, D. R. Commander. * Copyright (C) 2013-2014, MIPS Technologies, Inc., California. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -23,15 +23,13 @@ #include "../../jsimddct.h" #include "../jsimd.h" -#include -#include #include -static unsigned int simd_support = ~0; +static THREAD_LOCAL unsigned int simd_support = ~0; -#if defined(__linux__) +#if !(defined(__mips_dsp) && (__mips_dsp_rev >= 2)) && defined(__linux__) -LOCAL(int) +LOCAL(void) parse_proc_cpuinfo(const char *search_string) { const char *file_name = "/proc/cpuinfo"; @@ -45,21 +43,18 @@ parse_proc_cpuinfo(const char *search_string) if (strstr(cpuinfo_line, search_string) != NULL) { fclose(f); simd_support |= JSIMD_DSPR2; - return 1; + return; } } fclose(f); } /* Did not find string in the proc file, or not Linux ELF. */ - return 0; } #endif /* * Check what SIMD accelerations are supported. - * - * FIXME: This code is racy under a multi-threaded environment. */ LOCAL(void) init_simd(void) @@ -73,14 +68,13 @@ init_simd(void) simd_support = 0; -#if defined(__MIPSEL__) && defined(__mips_dsp) && (__mips_dsp_rev >= 2) +#if defined(__mips_dsp) && (__mips_dsp_rev >= 2) simd_support |= JSIMD_DSPR2; #elif defined(__linux__) /* We still have a chance to use MIPS DSPR2 regardless of globally used * -mdspr2 options passed to gcc by performing runtime detection via * /proc/cpuinfo parsing on linux */ - if (!parse_proc_cpuinfo("MIPS 74K")) - return; + parse_proc_cpuinfo("MIPS 74K"); #endif #ifndef NO_GETENV @@ -340,8 +334,13 @@ jsimd_can_h2v2_downsample(void) if (sizeof(JDIMENSION) != 4) return 0; + /* FIXME: jsimd_h2v2_downsample_dspr2() fails some of the TJBench tiling + * regression tests, probably because the DSPr2 SIMD implementation predates + * those tests. */ +#if 0 if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -376,8 +375,13 @@ jsimd_can_h2v1_downsample(void) if (sizeof(JDIMENSION) != 4) return 0; + /* FIXME: jsimd_h2v1_downsample_dspr2() fails some of the TJBench tiling + * regression tests, probably because the DSPr2 SIMD implementation predates + * those tests. */ +#if 0 if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -441,8 +445,10 @@ jsimd_can_h2v1_upsample(void) if (sizeof(JDIMENSION) != 4) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -503,8 +509,10 @@ jsimd_can_h2v2_fancy_upsample(void) if (sizeof(JDIMENSION) != 4) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -520,8 +528,10 @@ jsimd_can_h2v1_fancy_upsample(void) if (sizeof(JDIMENSION) != 4) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -669,8 +679,10 @@ jsimd_can_convsamp(void) if (sizeof(DCTELEM) != 2) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -727,8 +739,10 @@ jsimd_can_fdct_islow(void) if (sizeof(DCTELEM) != 2) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -744,8 +758,10 @@ jsimd_can_fdct_ifast(void) if (sizeof(DCTELEM) != 2) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -872,8 +888,10 @@ jsimd_can_idct_4x4(void) if (sizeof(ISLOW_MULT_TYPE) != 2) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -1017,8 +1035,10 @@ jsimd_can_idct_ifast(void) if (IFAST_SCALE_BITS != 2) return 0; +#if defined(__MIPSEL__) if (simd_support & JSIMD_DSPR2) return 1; +#endif return 0; } @@ -1104,7 +1124,7 @@ jsimd_can_encode_mcu_AC_first_prepare(void) GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { } @@ -1117,7 +1137,7 @@ jsimd_can_encode_mcu_AC_refine_prepare(void) GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { return 0; } diff --git a/third-party/mozjpeg/mozjpeg/simd/mips/jsimd_dspr2.S b/third-party/mozjpeg/mozjpeg/simd/mips/jsimd_dspr2.S index a28c11618c9..c99288a8d1c 100644 --- a/third-party/mozjpeg/mozjpeg/simd/mips/jsimd_dspr2.S +++ b/third-party/mozjpeg/mozjpeg/simd/mips/jsimd_dspr2.S @@ -41,10 +41,10 @@ LEAF_DSPR2(jsimd_c_null_convert_dspr2) */ SAVE_REGS_ON_STACK 8, s0, s1 - lw t9, 24(sp) // t9 = num_rows - lw s0, 28(sp) // s0 = cinfo->num_components - andi t0, a0, 3 // t0 = cinfo->image_width & 3 - beqz t0, 4f // no residual + lw t9, 24(sp) /* t9 = num_rows */ + lw s0, 28(sp) /* s0 = cinfo->num_components */ + andi t0, a0, 3 /* t0 = cinfo->image_width & 3 */ + beqz t0, 4f /* no residual */ nop 0: addiu t9, t9, -1 @@ -52,10 +52,10 @@ LEAF_DSPR2(jsimd_c_null_convert_dspr2) li t1, 0 1: sll t3, t1, 2 - lwx t5, t3(a2) // t5 = outptr = output_buf[ci] - lw t2, 0(a1) // t2 = inptr = *input_buf + lwx t5, t3(a2) /* t5 = outptr = output_buf[ci] */ + lw t2, 0(a1) /* t2 = inptr = *input_buf */ sll t4, a3, 2 - lwx t5, t4(t5) // t5 = outptr = output_buf[ci][output_row] + lwx t5, t4(t5) /* t5 = outptr = output_buf[ci][output_row] */ addu t2, t2, t1 addu s1, t5, a0 addu t6, t5, t0 @@ -94,10 +94,10 @@ LEAF_DSPR2(jsimd_c_null_convert_dspr2) li t1, 0 5: sll t3, t1, 2 - lwx t5, t3(a2) // t5 = outptr = output_buf[ci] - lw t2, 0(a1) // t2 = inptr = *input_buf + lwx t5, t3(a2) /* t5 = outptr = output_buf[ci] */ + lw t2, 0(a1) /* t2 = inptr = *input_buf */ sll t4, a3, 2 - lwx t5, t4(t5) // t5 = outptr = output_buf[ci][output_row] + lwx t5, t4(t5) /* t5 = outptr = output_buf[ci][output_row] */ addu t2, t2, t1 addu s1, t5, a0 addu t6, t5, t0 @@ -163,29 +163,29 @@ LEAF_DSPR2(jsimd_\colorid\()_ycc_convert_dspr2) */ SAVE_REGS_ON_STACK 32, s0, s1, s2, s3, s4, s5, s6, s7 - lw t7, 48(sp) // t7 = num_rows - li s0, 0x4c8b // FIX(0.29900) - li s1, 0x9646 // FIX(0.58700) - li s2, 0x1d2f // FIX(0.11400) - li s3, 0xffffd4cd // -FIX(0.16874) - li s4, 0xffffab33 // -FIX(0.33126) - li s5, 0x8000 // FIX(0.50000) - li s6, 0xffff94d1 // -FIX(0.41869) - li s7, 0xffffeb2f // -FIX(0.08131) - li t8, 0x807fff // CBCR_OFFSET + ONE_HALF-1 + lw t7, 48(sp) /* t7 = num_rows */ + li s0, 0x4c8b /* FIX(0.29900) */ + li s1, 0x9646 /* FIX(0.58700) */ + li s2, 0x1d2f /* FIX(0.11400) */ + li s3, 0xffffd4cd /* -FIX(0.16874) */ + li s4, 0xffffab33 /* -FIX(0.33126) */ + li s5, 0x8000 /* FIX(0.50000) */ + li s6, 0xffff94d1 /* -FIX(0.41869) */ + li s7, 0xffffeb2f /* -FIX(0.08131) */ + li t8, 0x807fff /* CBCR_OFFSET + ONE_HALF-1 */ 0: - addiu t7, -1 // --num_rows - lw t6, 0(a1) // t6 = input_buf[0] + addiu t7, -1 /* --num_rows */ + lw t6, 0(a1) /* t6 = input_buf[0] */ lw t0, 0(a2) lw t1, 4(a2) lw t2, 8(a2) sll t3, a3, 2 - lwx t0, t3(t0) // t0 = output_buf[0][output_row] - lwx t1, t3(t1) // t1 = output_buf[1][output_row] - lwx t2, t3(t2) // t2 = output_buf[2][output_row] + lwx t0, t3(t0) /* t0 = output_buf[0][output_row] */ + lwx t1, t3(t1) /* t1 = output_buf[1][output_row] */ + lwx t2, t3(t2) /* t2 = output_buf[2][output_row] */ - addu t9, t2, a0 // t9 = end address + addu t9, t2, a0 /* t9 = end address */ addiu a3, 1 1: @@ -273,10 +273,10 @@ LEAF_DSPR2(jsimd_ycc_\colorid\()_convert_dspr2) lw s1, 48(sp) li t3, 0x8000 - li t4, 0x166e9 // FIX(1.40200) - li t5, 0x1c5a2 // FIX(1.77200) - li t6, 0xffff492e // -FIX(0.71414) - li t7, 0xffffa7e6 // -FIX(0.34414) + li t4, 0x166e9 /* FIX(1.40200) */ + li t5, 0x1c5a2 /* FIX(1.77200) */ + li t6, 0xffff492e /* -FIX(0.71414) */ + li t7, 0xffffa7e6 /* -FIX(0.34414) */ repl.ph t8, 128 0: @@ -293,25 +293,25 @@ LEAF_DSPR2(jsimd_ycc_\colorid\()_convert_dspr2) addiu a2, 1 1: - lbu s7, 0(s4) // cr - lbu s6, 0(s3) // cb - lbu s5, 0(s2) // y + lbu s7, 0(s4) /* cr */ + lbu s6, 0(s3) /* cb */ + lbu s5, 0(s2) /* y */ addiu s2, 1 addiu s4, 1 addiu s7, -128 addiu s6, -128 mul t2, t7, s6 - mul t0, t6, s7 // Crgtab[cr] + mul t0, t6, s7 /* Crgtab[cr] */ sll s7, 15 - mulq_rs.w t1, t4, s7 // Crrtab[cr] + mulq_rs.w t1, t4, s7 /* Crrtab[cr] */ sll s6, 15 - addu t2, t3 // Cbgtab[cb] + addu t2, t3 /* Cbgtab[cb] */ addu t2, t0 - mulq_rs.w t0, t5, s6 // Cbbtab[cb] + mulq_rs.w t0, t5, s6 /* Cbbtab[cb] */ sra t2, 16 addu t1, s5 - addu t2, s5 // add y + addu t2, s5 /* add y */ ins t2, t1, 16, 16 subu.ph t2, t2, t8 addu t0, s5 @@ -319,7 +319,7 @@ LEAF_DSPR2(jsimd_ycc_\colorid\()_convert_dspr2) subu t0, 128 shra.ph t2, t2, 8 shll_s.w t0, t0, 24 - addu.ph t2, t2, t8 // clip & store + addu.ph t2, t2, t8 /* clip & store */ sra t0, t0, 24 sra t1, t2, 16 addiu t0, 128 @@ -382,15 +382,15 @@ LEAF_DSPR2(jsimd_\colorid\()_gray_convert_dspr2) */ SAVE_REGS_ON_STACK 32, s0, s1, s2, s3, s4, s5, s6, s7 - li s0, 0x4c8b // s0 = FIX(0.29900) - li s1, 0x9646 // s1 = FIX(0.58700) - li s2, 0x1d2f // s2 = FIX(0.11400) - li s7, 0x8000 // s7 = FIX(0.50000) + li s0, 0x4c8b /* s0 = FIX(0.29900) */ + li s1, 0x9646 /* s1 = FIX(0.58700) */ + li s2, 0x1d2f /* s2 = FIX(0.11400) */ + li s7, 0x8000 /* s7 = FIX(0.50000) */ lw s6, 48(sp) andi t7, a0, 3 0: - addiu s6, -1 // s6 = num_rows + addiu s6, -1 /* s6 = num_rows */ lw t0, 0(a1) lw t1, 0(a2) sll t3, a3, 2 @@ -532,59 +532,59 @@ LEAF_DSPR2(jsimd_h2v2_\colorid\()_merged_upsample_dspr2) */ SAVE_REGS_ON_STACK 40, s0, s1, s2, s3, s4, s5, s6, s7, ra - lw t9, 56(sp) // cinfo->sample_range_limit + lw t9, 56(sp) /* cinfo->sample_range_limit */ lw v0, 0(a1) lw v1, 4(a1) lw t0, 8(a1) sll t1, a2, 3 addiu t2, t1, 4 sll t3, a2, 2 - lw t4, 0(a3) // t4 = output_buf[0] - lwx t1, t1(v0) // t1 = input_buf[0][in_row_group_ctr*2] - lwx t2, t2(v0) // t2 = input_buf[0][in_row_group_ctr*2 + 1] - lwx t5, t3(v1) // t5 = input_buf[1][in_row_group_ctr] - lwx t6, t3(t0) // t6 = input_buf[2][in_row_group_ctr] - lw t7, 4(a3) // t7 = output_buf[1] + lw t4, 0(a3) /* t4 = output_buf[0] */ + lwx t1, t1(v0) /* t1 = input_buf[0][in_row_group_ctr*2] */ + lwx t2, t2(v0) /* t2 = input_buf[0][in_row_group_ctr*2 + 1] */ + lwx t5, t3(v1) /* t5 = input_buf[1][in_row_group_ctr] */ + lwx t6, t3(t0) /* t6 = input_buf[2][in_row_group_ctr] */ + lw t7, 4(a3) /* t7 = output_buf[1] */ li s1, 0xe6ea - addiu t8, s1, 0x7fff // t8 = 0x166e9 [FIX(1.40200)] - addiu s0, t8, 0x5eb9 // s0 = 0x1c5a2 [FIX(1.77200)] - addiu s1, zero, 0xa7e6 // s4 = 0xffffa7e6 [-FIX(0.34414)] - xori s2, s1, 0xeec8 // s3 = 0xffff492e [-FIX(0.71414)] + addiu t8, s1, 0x7fff /* t8 = 0x166e9 [FIX(1.40200)] */ + addiu s0, t8, 0x5eb9 /* s0 = 0x1c5a2 [FIX(1.77200)] */ + addiu s1, zero, 0xa7e6 /* s4 = 0xffffa7e6 [-FIX(0.34414)] */ + xori s2, s1, 0xeec8 /* s3 = 0xffff492e [-FIX(0.71414)] */ srl t3, a0, 1 blez t3, 2f - addu t0, t5, t3 // t0 = end address + addu t0, t5, t3 /* t0 = end address */ 1: lbu t3, 0(t5) lbu s3, 0(t6) addiu t5, t5, 1 - addiu t3, t3, -128 // (cb - 128) - addiu s3, s3, -128 // (cr - 128) + addiu t3, t3, -128 /* (cb - 128) */ + addiu s3, s3, -128 /* (cr - 128) */ mult $ac1, s1, t3 madd $ac1, s2, s3 sll s3, s3, 15 sll t3, t3, 15 - mulq_rs.w s4, t8, s3 // s4 = (C1 * cr + ONE_HALF)>> SCALEBITS + mulq_rs.w s4, t8, s3 /* s4 = (C1 * cr + ONE_HALF)>> SCALEBITS */ extr_r.w s5, $ac1, 16 - mulq_rs.w s6, s0, t3 // s6 = (C2 * cb + ONE_HALF)>> SCALEBITS + mulq_rs.w s6, s0, t3 /* s6 = (C2 * cb + ONE_HALF)>> SCALEBITS */ lbu v0, 0(t1) addiu t6, t6, 1 addiu t1, t1, 2 - addu t3, v0, s4 // y+cred - addu s3, v0, s5 // y+cgreen - addu v1, v0, s6 // y+cblue - addu t3, t9, t3 // y+cred - addu s3, t9, s3 // y+cgreen - addu v1, t9, v1 // y+cblue + addu t3, v0, s4 /* y+cred */ + addu s3, v0, s5 /* y+cgreen */ + addu v1, v0, s6 /* y+cblue */ + addu t3, t9, t3 /* y+cred */ + addu s3, t9, s3 /* y+cgreen */ + addu v1, t9, v1 /* y+cblue */ lbu AT, 0(t3) lbu s7, 0(s3) lbu ra, 0(v1) lbu v0, -1(t1) - addu t3, v0, s4 // y+cred - addu s3, v0, s5 // y+cgreen - addu v1, v0, s6 // y+cblue - addu t3, t9, t3 // y+cred - addu s3, t9, s3 // y+cgreen - addu v1, t9, v1 // y+cblue + addu t3, v0, s4 /* y+cred */ + addu s3, v0, s5 /* y+cgreen */ + addu v1, v0, s6 /* y+cblue */ + addu t3, t9, t3 /* y+cred */ + addu s3, t9, s3 /* y+cgreen */ + addu v1, t9, v1 /* y+cblue */ lbu t3, 0(t3) lbu s3, 0(s3) lbu v1, 0(v1) @@ -592,23 +592,23 @@ LEAF_DSPR2(jsimd_h2v2_\colorid\()_merged_upsample_dspr2) STORE_H2V2_2_PIXELS AT, s7, ra, t3, s3, v1, t4 - addu t3, v0, s4 // y+cred - addu s3, v0, s5 // y+cgreen - addu v1, v0, s6 // y+cblue - addu t3, t9, t3 // y+cred - addu s3, t9, s3 // y+cgreen - addu v1, t9, v1 // y+cblue + addu t3, v0, s4 /* y+cred */ + addu s3, v0, s5 /* y+cgreen */ + addu v1, v0, s6 /* y+cblue */ + addu t3, t9, t3 /* y+cred */ + addu s3, t9, s3 /* y+cgreen */ + addu v1, t9, v1 /* y+cblue */ lbu AT, 0(t3) lbu s7, 0(s3) lbu ra, 0(v1) lbu v0, 1(t2) addiu t2, t2, 2 - addu t3, v0, s4 // y+cred - addu s3, v0, s5 // y+cgreen - addu v1, v0, s6 // y+cblue - addu t3, t9, t3 // y+cred - addu s3, t9, s3 // y+cgreen - addu v1, t9, v1 // y+cblue + addu t3, v0, s4 /* y+cred */ + addu s3, v0, s5 /* y+cgreen */ + addu v1, v0, s6 /* y+cblue */ + addu t3, t9, t3 /* y+cred */ + addu s3, t9, s3 /* y+cgreen */ + addu v1, t9, v1 /* y+cblue */ lbu t3, 0(t3) lbu s3, 0(s3) lbu v1, 0(v1) @@ -622,22 +622,22 @@ LEAF_DSPR2(jsimd_h2v2_\colorid\()_merged_upsample_dspr2) beqz t0, 4f lbu t3, 0(t5) lbu s3, 0(t6) - addiu t3, t3, -128 // (cb - 128) - addiu s3, s3, -128 // (cr - 128) + addiu t3, t3, -128 /* (cb - 128) */ + addiu s3, s3, -128 /* (cr - 128) */ mult $ac1, s1, t3 madd $ac1, s2, s3 sll s3, s3, 15 sll t3, t3, 15 lbu v0, 0(t1) extr_r.w s5, $ac1, 16 - mulq_rs.w s4, t8, s3 // s4 = (C1 * cr + ONE_HALF)>> SCALEBITS - mulq_rs.w s6, s0, t3 // s6 = (C2 * cb + ONE_HALF)>> SCALEBITS - addu t3, v0, s4 // y+cred - addu s3, v0, s5 // y+cgreen - addu v1, v0, s6 // y+cblue - addu t3, t9, t3 // y+cred - addu s3, t9, s3 // y+cgreen - addu v1, t9, v1 // y+cblue + mulq_rs.w s4, t8, s3 /* s4 = (C1 * cr + ONE_HALF)>> SCALEBITS */ + mulq_rs.w s6, s0, t3 /* s6 = (C2 * cb + ONE_HALF)>> SCALEBITS */ + addu t3, v0, s4 /* y+cred */ + addu s3, v0, s5 /* y+cgreen */ + addu v1, v0, s6 /* y+cblue */ + addu t3, t9, t3 /* y+cred */ + addu s3, t9, s3 /* y+cgreen */ + addu v1, t9, v1 /* y+cblue */ lbu t3, 0(t3) lbu s3, 0(s3) lbu v1, 0(v1) @@ -645,12 +645,12 @@ LEAF_DSPR2(jsimd_h2v2_\colorid\()_merged_upsample_dspr2) STORE_H2V2_1_PIXEL t3, s3, v1, t4 - addu t3, v0, s4 // y+cred - addu s3, v0, s5 // y+cgreen - addu v1, v0, s6 // y+cblue - addu t3, t9, t3 // y+cred - addu s3, t9, s3 // y+cgreen - addu v1, t9, v1 // y+cblue + addu t3, v0, s4 /* y+cred */ + addu s3, v0, s5 /* y+cgreen */ + addu v1, v0, s6 /* y+cblue */ + addu t3, t9, t3 /* y+cred */ + addu s3, t9, s3 /* y+cgreen */ + addu v1, t9, v1 /* y+cblue */ lbu t3, 0(t3) lbu s3, 0(s3) lbu v1, 0(v1) @@ -733,41 +733,41 @@ LEAF_DSPR2(jsimd_h2v1_\colorid\()_merged_upsample_dspr2) SAVE_REGS_ON_STACK 40, s0, s1, s2, s3, s4, s5, s6, s7, ra li t0, 0xe6ea - lw t1, 0(a1) // t1 = input_buf[0] - lw t2, 4(a1) // t2 = input_buf[1] - lw t3, 8(a1) // t3 = input_buf[2] - lw t8, 56(sp) // t8 = range_limit - addiu s1, t0, 0x7fff // s1 = 0x166e9 [FIX(1.40200)] - addiu s2, s1, 0x5eb9 // s2 = 0x1c5a2 [FIX(1.77200)] - addiu s0, t0, 0x9916 // s0 = 0x8000 - addiu s4, zero, 0xa7e6 // s4 = 0xffffa7e6 [-FIX(0.34414)] - xori s3, s4, 0xeec8 // s3 = 0xffff492e [-FIX(0.71414)] + lw t1, 0(a1) /* t1 = input_buf[0] */ + lw t2, 4(a1) /* t2 = input_buf[1] */ + lw t3, 8(a1) /* t3 = input_buf[2] */ + lw t8, 56(sp) /* t8 = range_limit */ + addiu s1, t0, 0x7fff /* s1 = 0x166e9 [FIX(1.40200)] */ + addiu s2, s1, 0x5eb9 /* s2 = 0x1c5a2 [FIX(1.77200)] */ + addiu s0, t0, 0x9916 /* s0 = 0x8000 */ + addiu s4, zero, 0xa7e6 /* s4 = 0xffffa7e6 [-FIX(0.34414)] */ + xori s3, s4, 0xeec8 /* s3 = 0xffff492e [-FIX(0.71414)] */ srl t0, a0, 1 sll t4, a2, 2 - lwx s5, t4(t1) // s5 = inptr0 - lwx s6, t4(t2) // s6 = inptr1 - lwx s7, t4(t3) // s7 = inptr2 - lw t7, 0(a3) // t7 = outptr + lwx s5, t4(t1) /* s5 = inptr0 */ + lwx s6, t4(t2) /* s6 = inptr1 */ + lwx s7, t4(t3) /* s7 = inptr2 */ + lw t7, 0(a3) /* t7 = outptr */ blez t0, 2f - addu t9, s6, t0 // t9 = end address + addu t9, s6, t0 /* t9 = end address */ 1: - lbu t2, 0(s6) // t2 = cb - lbu t0, 0(s7) // t0 = cr - lbu t1, 0(s5) // t1 = y - addiu t2, t2, -128 // t2 = cb - 128 - addiu t0, t0, -128 // t0 = cr - 128 + lbu t2, 0(s6) /* t2 = cb */ + lbu t0, 0(s7) /* t0 = cr */ + lbu t1, 0(s5) /* t1 = y */ + addiu t2, t2, -128 /* t2 = cb - 128 */ + addiu t0, t0, -128 /* t0 = cr - 128 */ mult $ac1, s4, t2 madd $ac1, s3, t0 sll t0, t0, 15 sll t2, t2, 15 - mulq_rs.w t0, s1, t0 // t0 = (C1*cr + ONE_HALF)>> SCALEBITS + mulq_rs.w t0, s1, t0 /* t0 = (C1*cr + ONE_HALF)>> SCALEBITS */ extr_r.w t5, $ac1, 16 - mulq_rs.w t6, s2, t2 // t6 = (C2*cb + ONE_HALF)>> SCALEBITS + mulq_rs.w t6, s2, t2 /* t6 = (C2*cb + ONE_HALF)>> SCALEBITS */ addiu s7, s7, 1 addiu s6, s6, 1 - addu t2, t1, t0 // t2 = y + cred - addu t3, t1, t5 // t3 = y + cgreen - addu t4, t1, t6 // t4 = y + cblue + addu t2, t1, t0 /* t2 = y + cred */ + addu t3, t1, t5 /* t3 = y + cgreen */ + addu t4, t1, t6 /* t4 = y + cblue */ addu t2, t8, t2 addu t3, t8, t3 addu t4, t8, t4 @@ -797,20 +797,20 @@ LEAF_DSPR2(jsimd_h2v1_\colorid\()_merged_upsample_dspr2) lbu t2, 0(s6) lbu t0, 0(s7) lbu t1, 0(s5) - addiu t2, t2, -128 // (cb - 128) - addiu t0, t0, -128 // (cr - 128) + addiu t2, t2, -128 /* (cb - 128) */ + addiu t0, t0, -128 /* (cr - 128) */ mul t3, s4, t2 mul t4, s3, t0 sll t0, t0, 15 sll t2, t2, 15 - mulq_rs.w t0, s1, t0 // (C1*cr + ONE_HALF)>> SCALEBITS - mulq_rs.w t6, s2, t2 // (C2*cb + ONE_HALF)>> SCALEBITS + mulq_rs.w t0, s1, t0 /* (C1*cr + ONE_HALF)>> SCALEBITS */ + mulq_rs.w t6, s2, t2 /* (C2*cb + ONE_HALF)>> SCALEBITS */ addu t3, t3, s0 addu t3, t4, t3 - sra t5, t3, 16 // (C4*cb + ONE_HALF + C3*cr)>> SCALEBITS - addu t2, t1, t0 // y + cred - addu t3, t1, t5 // y + cgreen - addu t4, t1, t6 // y + cblue + sra t5, t3, 16 /* (C4*cb + ONE_HALF + C3*cr)>> SCALEBITS */ + addu t2, t1, t0 /* y + cred */ + addu t3, t1, t5 /* y + cgreen */ + addu t4, t1, t6 /* y + cblue */ addu t2, t8, t2 addu t3, t8, t3 addu t4, t8, t4 @@ -856,15 +856,15 @@ LEAF_DSPR2(jsimd_h2v2_fancy_upsample_dspr2) SAVE_REGS_ON_STACK 24, s0, s1, s2, s3, s4, s5 li s4, 0 - lw s2, 0(a3) // s2 = *output_data_ptr + lw s2, 0(a3) /* s2 = *output_data_ptr */ 0: li t9, 2 - lw s1, -4(a2) // s1 = inptr1 + lw s1, -4(a2) /* s1 = inptr1 */ 1: - lw s0, 0(a2) // s0 = inptr0 + lw s0, 0(a2) /* s0 = inptr0 */ lwx s3, s4(s2) - addiu s5, a1, -2 // s5 = downsampled_width - 2 + addiu s5, a1, -2 /* s5 = downsampled_width - 2 */ srl t4, s5, 1 sll t4, t4, 1 lbu t0, 0(s0) @@ -873,50 +873,50 @@ LEAF_DSPR2(jsimd_h2v2_fancy_upsample_dspr2) lbu t3, 1(s1) addiu s0, 2 addiu s1, 2 - addu t8, s0, t4 // t8 = end address - andi s5, s5, 1 // s5 = residual + addu t8, s0, t4 /* t8 = end address */ + andi s5, s5, 1 /* s5 = residual */ sll t4, t0, 1 sll t6, t1, 1 - addu t0, t0, t4 // t0 = (*inptr0++) * 3 - addu t1, t1, t6 // t1 = (*inptr0++) * 3 - addu t7, t0, t2 // t7 = thiscolsum - addu t6, t1, t3 // t5 = nextcolsum - sll t0, t7, 2 // t0 = thiscolsum * 4 - subu t1, t0, t7 // t1 = thiscolsum * 3 + addu t0, t0, t4 /* t0 = (*inptr0++) * 3 */ + addu t1, t1, t6 /* t1 = (*inptr0++) * 3 */ + addu t7, t0, t2 /* t7 = thiscolsum */ + addu t6, t1, t3 /* t5 = nextcolsum */ + sll t0, t7, 2 /* t0 = thiscolsum * 4 */ + subu t1, t0, t7 /* t1 = thiscolsum * 3 */ shra_r.w t0, t0, 4 addiu t1, 7 addu t1, t1, t6 srl t1, t1, 4 sb t0, 0(s3) sb t1, 1(s3) - beq t8, s0, 22f // skip to final iteration if width == 3 + beq t8, s0, 22f /* skip to final iteration if width == 3 */ addiu s3, 2 2: - lh t0, 0(s0) // t0 = A3|A2 - lh t2, 0(s1) // t2 = B3|B2 + lh t0, 0(s0) /* t0 = A3|A2 */ + lh t2, 0(s1) /* t2 = B3|B2 */ addiu s0, 2 addiu s1, 2 - preceu.ph.qbr t0, t0 // t0 = 0|A3|0|A2 - preceu.ph.qbr t2, t2 // t2 = 0|B3|0|B2 + preceu.ph.qbr t0, t0 /* t0 = 0|A3|0|A2 */ + preceu.ph.qbr t2, t2 /* t2 = 0|B3|0|B2 */ shll.ph t1, t0, 1 sll t3, t6, 1 - addu.ph t0, t1, t0 // t0 = A3*3|A2*3 - addu t3, t3, t6 // t3 = this * 3 - addu.ph t0, t0, t2 // t0 = next2|next1 + addu.ph t0, t1, t0 /* t0 = A3*3|A2*3 */ + addu t3, t3, t6 /* t3 = this * 3 */ + addu.ph t0, t0, t2 /* t0 = next2|next1 */ addu t1, t3, t7 - andi t7, t0, 0xFFFF // t7 = next1 + andi t7, t0, 0xFFFF /* t7 = next1 */ sll t2, t7, 1 - addu t2, t7, t2 // t2 = next1*3 + addu t2, t7, t2 /* t2 = next1*3 */ addu t4, t2, t6 - srl t6, t0, 16 // t6 = next2 - shra_r.w t1, t1, 4 // t1 = (this*3 + last + 8) >> 4 + srl t6, t0, 16 /* t6 = next2 */ + shra_r.w t1, t1, 4 /* t1 = (this*3 + last + 8) >> 4 */ addu t0, t3, t7 addiu t0, 7 - srl t0, t0, 4 // t0 = (this*3 + next1 + 7) >> 4 - shra_r.w t4, t4, 4 // t3 = (next1*3 + this + 8) >> 4 + srl t0, t0, 4 /* t0 = (this*3 + next1 + 7) >> 4 */ + shra_r.w t4, t4, 4 /* t3 = (next1*3 + this + 8) >> 4 */ addu t2, t2, t6 addiu t2, 7 - srl t2, t2, 4 // t2 = (next1*3 + next2 + 7) >> 4 + srl t2, t2, 4 /* t2 = (next1*3 + next2 + 7) >> 4 */ sb t1, 0(s3) sb t0, 1(s3) sb t4, 2(s3) @@ -933,8 +933,8 @@ LEAF_DSPR2(jsimd_h2v2_fancy_upsample_dspr2) addiu s1, 1 sll t3, t6, 1 sll t1, t0, 1 - addu t1, t0, t1 // t1 = inptr0 * 3 - addu t3, t3, t6 // t3 = thiscolsum * 3 + addu t1, t0, t1 /* t1 = inptr0 * 3 */ + addu t3, t3, t6 /* t3 = thiscolsum * 3 */ addu t5, t1, t2 addu t1, t3, t7 shra_r.w t1, t1, 4 @@ -948,8 +948,8 @@ LEAF_DSPR2(jsimd_h2v2_fancy_upsample_dspr2) bne t8, s0, 3b move t6, t5 4: - sll t0, t6, 2 // t0 = thiscolsum * 4 - subu t1, t0, t6 // t1 = thiscolsum * 3 + sll t0, t6, 2 /* t0 = thiscolsum * 4 */ + subu t1, t0, t6 /* t1 = thiscolsum * 3 */ addu t1, t1, t7 addiu s4, 4 shra_r.w t1, t1, 4 @@ -996,9 +996,9 @@ LEAF_DSPR2(jsimd_h2v1_fancy_upsample_dspr2) lw t7, 0(a2) lw s2, 0(s1) lbu t0, 0(t7) - lbu t1, 1(t7) // t1 = inptr[1] + lbu t1, 1(t7) /* t1 = inptr[1] */ sll t2, t0, 1 - addu t2, t2, t0 // t2 = invalue*3 + addu t2, t2, t0 /* t2 = invalue*3 */ addu t2, t2, t1 shra_r.w t2, t2, 2 sb t0, 0(s2) @@ -1006,28 +1006,28 @@ LEAF_DSPR2(jsimd_h2v1_fancy_upsample_dspr2) beqz t9, 11f addiu s2, 2 1: - ulw t0, 0(t7) // t0 = |P3|P2|P1|P0| + ulw t0, 0(t7) /* t0 = |P3|P2|P1|P0| */ ulw t1, 1(t7) - ulh t2, 4(t7) // t2 = |0|0|P5|P4| - preceu.ph.qbl t3, t0 // t3 = |0|P3|0|P2| - preceu.ph.qbr t0, t0 // t0 = |0|P1|0|P0| - preceu.ph.qbr t2, t2 // t2 = |0|P5|0|P4| - preceu.ph.qbl t4, t1 // t4 = |0|P4|0|P3| - preceu.ph.qbr t1, t1 // t1 = |0|P2|0|P1| + ulh t2, 4(t7) /* t2 = |0|0|P5|P4| */ + preceu.ph.qbl t3, t0 /* t3 = |0|P3|0|P2| */ + preceu.ph.qbr t0, t0 /* t0 = |0|P1|0|P0| */ + preceu.ph.qbr t2, t2 /* t2 = |0|P5|0|P4| */ + preceu.ph.qbl t4, t1 /* t4 = |0|P4|0|P3| */ + preceu.ph.qbr t1, t1 /* t1 = |0|P2|0|P1| */ shll.ph t5, t4, 1 shll.ph t6, t1, 1 - addu.ph t5, t5, t4 // t5 = |P4*3|P3*3| - addu.ph t6, t6, t1 // t6 = |P2*3|P1*3| + addu.ph t5, t5, t4 /* t5 = |P4*3|P3*3| */ + addu.ph t6, t6, t1 /* t6 = |P2*3|P1*3| */ addu.ph t4, t3, s3 addu.ph t0, t0, s3 addu.ph t4, t4, t5 addu.ph t0, t0, t6 - shrl.ph t4, t4, 2 // t4 = |0|P3|0|P2| - shrl.ph t0, t0, 2 // t0 = |0|P1|0|P0| + shrl.ph t4, t4, 2 /* t4 = |0|P3|0|P2| */ + shrl.ph t0, t0, 2 /* t0 = |0|P1|0|P0| */ addu.ph t2, t2, t5 addu.ph t3, t3, t6 - shra_r.ph t2, t2, 2 // t2 = |0|P5|0|P4| - shra_r.ph t3, t3, 2 // t3 = |0|P3|0|P2| + shra_r.ph t2, t2, 2 /* t2 = |0|P5|0|P4| */ + shra_r.ph t3, t3, 2 /* t3 = |0|P3|0|P2| */ shll.ph t2, t2, 8 shll.ph t3, t3, 8 or t2, t4, t2 @@ -1047,7 +1047,7 @@ LEAF_DSPR2(jsimd_h2v1_fancy_upsample_dspr2) lbu t0, 0(t7) addiu t7, 1 sll t1, t0, 1 - addu t2, t0, t1 // t2 = invalue + addu t2, t0, t1 /* t2 = invalue */ lbu t3, -2(t7) lbu t4, 0(t7) addiu t3, 1 @@ -1066,7 +1066,7 @@ LEAF_DSPR2(jsimd_h2v1_fancy_upsample_dspr2) lbu t0, 0(t7) lbu t2, -1(t7) sll t1, t0, 1 - addu t1, t1, t0 // t1 = invalue * 3 + addu t1, t1, t0 /* t1 = invalue * 3 */ addu t1, t1, t2 addiu t1, 1 srl t1, t1, 2 @@ -1098,22 +1098,22 @@ LEAF_DSPR2(jsimd_h2v1_downsample_dspr2) SAVE_REGS_ON_STACK 24, s0, s1, s2, s3, s4 beqz a2, 7f - lw s1, 44(sp) // s1 = output_data - lw s0, 40(sp) // s0 = input_data + lw s1, 44(sp) /* s1 = output_data */ + lw s0, 40(sp) /* s0 = input_data */ srl s2, a0, 2 andi t9, a0, 2 srl t7, t9, 1 addu s2, t7, s2 - sll t0, a3, 3 // t0 = width_in_blocks*DCT + sll t0, a3, 3 /* t0 = width_in_blocks*DCT */ srl t7, t0, 1 subu s2, t7, s2 0: - andi t6, a0, 1 // t6 = temp_index + andi t6, a0, 1 /* t6 = temp_index */ addiu t6, -1 - lw t4, 0(s1) // t4 = outptr - lw t5, 0(s0) // t5 = inptr0 - li s3, 0 // s3 = bias - srl t7, a0, 1 // t7 = image_width1 + lw t4, 0(s1) /* t4 = outptr */ + lw t5, 0(s0) /* t5 = inptr0 */ + li s3, 0 /* s3 = bias */ + srl t7, a0, 1 /* t7 = image_width1 */ srl s4, t7, 2 andi t8, t7, 3 1: @@ -1151,12 +1151,12 @@ LEAF_DSPR2(jsimd_h2v1_downsample_dspr2) 3: lbux t1, t6(t5) sll t1, 1 - addqh.w t2, t1, s3 // t2 = pixval1 + addqh.w t2, t1, s3 /* t2 = pixval1 */ xori s3, s3, 1 - addqh.w t3, t1, s3 // t3 = pixval2 + addqh.w t3, t1, s3 /* t3 = pixval2 */ blez s2, 5f append t3, t2, 8 - addu t5, t4, s2 // t5 = loop_end2 + addu t5, t4, s2 /* t5 = loop_end2 */ 4: ush t3, 0(t4) addiu s2, -1 @@ -1194,33 +1194,33 @@ LEAF_DSPR2(jsimd_h2v2_downsample_dspr2) SAVE_REGS_ON_STACK 32, s0, s1, s2, s3, s4, s5, s6, s7 beqz a2, 8f - lw s1, 52(sp) // s1 = output_data - lw s0, 48(sp) // s0 = input_data + lw s1, 52(sp) /* s1 = output_data */ + lw s0, 48(sp) /* s0 = input_data */ - andi t6, a0, 1 // t6 = temp_index + andi t6, a0, 1 /* t6 = temp_index */ addiu t6, -1 - srl t7, a0, 1 // t7 = image_width1 + srl t7, a0, 1 /* t7 = image_width1 */ srl s4, t7, 2 andi t8, t7, 3 andi t9, a0, 2 srl s2, a0, 2 srl t7, t9, 1 addu s2, t7, s2 - sll t0, a3, 3 // s2 = width_in_blocks*DCT + sll t0, a3, 3 /* s2 = width_in_blocks*DCT */ srl t7, t0, 1 subu s2, t7, s2 0: - lw t4, 0(s1) // t4 = outptr - lw t5, 0(s0) // t5 = inptr0 - lw s7, 4(s0) // s7 = inptr1 - li s6, 1 // s6 = bias + lw t4, 0(s1) /* t4 = outptr */ + lw t5, 0(s0) /* t5 = inptr0 */ + lw s7, 4(s0) /* s7 = inptr1 */ + li s6, 1 /* s6 = bias */ 2: - ulw t0, 0(t5) // t0 = |P3|P2|P1|P0| - ulw t1, 0(s7) // t1 = |Q3|Q2|Q1|Q0| + ulw t0, 0(t5) /* t0 = |P3|P2|P1|P0| */ + ulw t1, 0(s7) /* t1 = |Q3|Q2|Q1|Q0| */ ulw t2, 4(t5) ulw t3, 4(s7) - precrq.ph.w t7, t0, t1 // t2 = |P3|P2|Q3|Q2| - ins t0, t1, 16, 16 // t0 = |Q1|Q0|P1|P0| + precrq.ph.w t7, t0, t1 /* t2 = |P3|P2|Q3|Q2| */ + ins t0, t1, 16, 16 /* t0 = |Q1|Q0|P1|P0| */ raddu.w.qb t1, t7 raddu.w.qb t0, t0 shra_r.w t1, t1, 2 @@ -1264,10 +1264,10 @@ LEAF_DSPR2(jsimd_h2v2_downsample_dspr2) sll t0, 1 addu t1, t1, t0 addu t3, t1, s6 - srl t0, t3, 2 // t2 = pixval1 + srl t0, t3, 2 /* t2 = pixval1 */ xori s6, s6, 3 addu t2, t1, s6 - srl t1, t2, 2 // t3 = pixval2 + srl t1, t2, 2 /* t3 = pixval2 */ blez s2, 6f append t1, t0, 8 5: @@ -1307,15 +1307,15 @@ LEAF_DSPR2(jsimd_h2v2_smooth_downsample_dspr2) SAVE_REGS_ON_STACK 32, s0, s1, s2, s3, s4, s5, s6, s7 - lw s7, 52(sp) // compptr->width_in_blocks - lw s0, 56(sp) // cinfo->image_width - lw s6, 48(sp) // cinfo->smoothing_factor - sll s7, 3 // output_cols = width_in_blocks * DCTSIZE + lw s7, 52(sp) /* compptr->width_in_blocks */ + lw s0, 56(sp) /* cinfo->image_width */ + lw s6, 48(sp) /* cinfo->smoothing_factor */ + sll s7, 3 /* output_cols = width_in_blocks * DCTSIZE */ sll v0, s7, 1 subu v0, v0, s0 blez v0, 2f move v1, zero - addiu t0, a3, 2 // t0 = cinfo->max_v_samp_factor + 2 + addiu t0, a3, 2 /* t0 = cinfo->max_v_samp_factor + 2 */ 0: addiu t1, a0, -4 sll t2, v1, 2 @@ -1337,20 +1337,20 @@ LEAF_DSPR2(jsimd_h2v2_smooth_downsample_dspr2) li v1, 16384 move t4, zero move t5, zero - subu t6, v1, v0 // t6 = 16384 - tmp_smoot_f * 80 - sll t7, s6, 4 // t7 = tmp_smoot_f * 16 + subu t6, v1, v0 /* t6 = 16384 - tmp_smoot_f * 80 */ + sll t7, s6, 4 /* t7 = tmp_smoot_f * 16 */ 3: /* Special case for first column: pretend column -1 is same as column 0 */ sll v0, t4, 2 - lwx t8, v0(a1) // outptr = output_data[outrow] + lwx t8, v0(a1) /* outptr = output_data[outrow] */ sll v1, t5, 2 addiu t9, v1, 4 addiu s0, v1, -4 addiu s1, v1, 8 - lwx s2, v1(a0) // inptr0 = input_data[inrow] - lwx t9, t9(a0) // inptr1 = input_data[inrow+1] - lwx s0, s0(a0) // above_ptr = input_data[inrow-1] - lwx s1, s1(a0) // below_ptr = input_data[inrow+2] + lwx s2, v1(a0) /* inptr0 = input_data[inrow] */ + lwx t9, t9(a0) /* inptr1 = input_data[inrow+1] */ + lwx s0, s0(a0) /* above_ptr = input_data[inrow-1] */ + lwx s1, s1(a0) /* below_ptr = input_data[inrow+2] */ lh v0, 0(s2) lh v1, 0(t9) lh t0, 0(s0) @@ -1387,7 +1387,7 @@ LEAF_DSPR2(jsimd_h2v2_smooth_downsample_dspr2) sb v0, -1(t8) addiu s4, s7, -2 and s4, s4, 3 - addu s5, s4, t8 // end address + addu s5, s4, t8 /* end address */ 4: lh v0, 0(s2) lh v1, 0(t9) @@ -1426,7 +1426,7 @@ LEAF_DSPR2(jsimd_h2v2_smooth_downsample_dspr2) addiu s1, s1, 2 addiu s5, s7, -2 subu s5, s5, s4 - addu s5, s5, t8 // end address + addu s5, s5, t8 /* end address */ 5: lh v0, 0(s2) lh v1, 0(t9) @@ -1607,24 +1607,24 @@ LEAF_DSPR2(jsimd_int_upsample_dspr2) SAVE_REGS_ON_STACK 16, s0, s1, s2, s3 - lw s0, 0(a3) // s0 = output_data - lw s1, 32(sp) // s1 = cinfo->output_width - lw s2, 36(sp) // s2 = cinfo->max_v_samp_factor - li t6, 0 // t6 = inrow + lw s0, 0(a3) /* s0 = output_data */ + lw s1, 32(sp) /* s1 = cinfo->output_width */ + lw s2, 36(sp) /* s2 = cinfo->max_v_samp_factor */ + li t6, 0 /* t6 = inrow */ beqz s2, 10f - li s3, 0 // s3 = outrow + li s3, 0 /* s3 = outrow */ 0: addu t0, a2, t6 addu t7, s0, s3 - lw t3, 0(t0) // t3 = inptr - lw t8, 0(t7) // t8 = outptr + lw t3, 0(t0) /* t3 = inptr */ + lw t8, 0(t7) /* t8 = outptr */ beqz s1, 4f - addu t5, t8, s1 // t5 = outend + addu t5, t8, s1 /* t5 = outend */ 1: - lb t2, 0(t3) // t2 = invalue = *inptr++ + lb t2, 0(t3) /* t2 = invalue = *inptr++ */ addiu t3, 1 beqz a0, 3f - move t0, a0 // t0 = h_expand + move t0, a0 /* t0 = h_expand */ 2: sb t2, 0(t8) addiu t0, -1 @@ -1634,7 +1634,7 @@ LEAF_DSPR2(jsimd_int_upsample_dspr2) bgt t5, t8, 1b nop 4: - addiu t9, a1, -1 // t9 = v_expand - 1 + addiu t9, a1, -1 /* t9 = v_expand - 1 */ blez t9, 9f nop 5: @@ -1642,8 +1642,8 @@ LEAF_DSPR2(jsimd_int_upsample_dspr2) lw t4, 4(s0) subu t0, s1, 0xF blez t0, 7f - addu t5, t3, s1 // t5 = end address - andi t7, s1, 0xF // t7 = residual + addu t5, t3, s1 /* t5 = end address */ + andi t7, s1, 0xF /* t7 = residual */ subu t8, t5, t7 6: ulw t0, 0(t3) @@ -1689,33 +1689,33 @@ LEAF_DSPR2(jsimd_h2v1_upsample_dspr2) * a2 = input_data * a3 = output_data_ptr */ - lw t7, 0(a3) // t7 = output_data - andi t8, a1, 0xf // t8 = residual + lw t7, 0(a3) /* t7 = output_data */ + andi t8, a1, 0xf /* t8 = residual */ sll t0, a0, 2 blez a0, 4f - addu t9, t7, t0 // t9 = output_data end address + addu t9, t7, t0 /* t9 = output_data end address */ 0: - lw t5, 0(t7) // t5 = outptr - lw t6, 0(a2) // t6 = inptr - addu t3, t5, a1 // t3 = outptr + output_width (end address) - subu t3, t8 // t3 = end address - residual + lw t5, 0(t7) /* t5 = outptr */ + lw t6, 0(a2) /* t6 = inptr */ + addu t3, t5, a1 /* t3 = outptr + output_width (end address) */ + subu t3, t8 /* t3 = end address - residual */ beq t5, t3, 2f move t4, t8 1: - ulw t0, 0(t6) // t0 = |P3|P2|P1|P0| - ulw t2, 4(t6) // t2 = |P7|P6|P5|P4| - srl t1, t0, 16 // t1 = |X|X|P3|P2| - ins t0, t0, 16, 16 // t0 = |P1|P0|P1|P0| - ins t1, t1, 16, 16 // t1 = |P3|P2|P3|P2| - ins t0, t0, 8, 16 // t0 = |P1|P1|P0|P0| - ins t1, t1, 8, 16 // t1 = |P3|P3|P2|P2| + ulw t0, 0(t6) /* t0 = |P3|P2|P1|P0| */ + ulw t2, 4(t6) /* t2 = |P7|P6|P5|P4| */ + srl t1, t0, 16 /* t1 = |X|X|P3|P2| */ + ins t0, t0, 16, 16 /* t0 = |P1|P0|P1|P0| */ + ins t1, t1, 16, 16 /* t1 = |P3|P2|P3|P2| */ + ins t0, t0, 8, 16 /* t0 = |P1|P1|P0|P0| */ + ins t1, t1, 8, 16 /* t1 = |P3|P3|P2|P2| */ usw t0, 0(t5) usw t1, 4(t5) - srl t0, t2, 16 // t0 = |X|X|P7|P6| - ins t2, t2, 16, 16 // t2 = |P5|P4|P5|P4| - ins t0, t0, 16, 16 // t0 = |P7|P6|P7|P6| - ins t2, t2, 8, 16 // t2 = |P5|P5|P4|P4| - ins t0, t0, 8, 16 // t0 = |P7|P7|P6|P6| + srl t0, t2, 16 /* t0 = |X|X|P7|P6| */ + ins t2, t2, 16, 16 /* t2 = |P5|P4|P5|P4| */ + ins t0, t0, 16, 16 /* t0 = |P7|P6|P7|P6| */ + ins t2, t2, 8, 16 /* t2 = |P5|P5|P4|P4| */ + ins t0, t0, 8, 16 /* t0 = |P7|P7|P6|P6| */ usw t2, 8(t5) usw t0, 12(t5) addiu t5, 16 @@ -1751,12 +1751,12 @@ LEAF_DSPR2(jsimd_h2v2_upsample_dspr2) */ lw t7, 0(a3) blez a0, 7f - andi t9, a1, 0xf // t9 = residual + andi t9, a1, 0xf /* t9 = residual */ 0: - lw t6, 0(a2) // t6 = inptr - lw t5, 0(t7) // t5 = outptr - addu t8, t5, a1 // t8 = outptr end address - subu t8, t9 // t8 = end address - residual + lw t6, 0(a2) /* t6 = inptr */ + lw t5, 0(t7) /* t5 = outptr */ + addu t8, t5, a1 /* t8 = outptr end address */ + subu t8, t9 /* t8 = end address - residual */ beq t5, t8, 2f move t4, t9 1: @@ -1790,9 +1790,9 @@ LEAF_DSPR2(jsimd_h2v2_upsample_dspr2) bgtz t4, 2b addiu t5, 2 3: - lw t6, 0(t7) // t6 = outptr[0] - lw t5, 4(t7) // t5 = outptr[1] - addu t4, t6, a1 // t4 = new end address + lw t6, 0(t7) /* t6 = outptr[0] */ + lw t5, 4(t7) /* t5 = outptr[1] */ + addu t4, t6, a1 /* t4 = new end address */ beq a1, t9, 5f subu t8, t4, t9 4: @@ -1838,15 +1838,15 @@ LEAF_DSPR2(jsimd_idct_islow_dspr2) addiu sp, sp, -256 move v0, sp - addiu v1, zero, 8 // v1 = DCTSIZE = 8 + addiu v1, zero, 8 /* v1 = DCTSIZE = 8 */ 1: - lh s4, 32(a0) // s4 = inptr[16] - lh s5, 64(a0) // s5 = inptr[32] - lh s6, 96(a0) // s6 = inptr[48] - lh t1, 112(a0) // t1 = inptr[56] - lh t7, 16(a0) // t7 = inptr[8] - lh t5, 80(a0) // t5 = inptr[40] - lh t3, 48(a0) // t3 = inptr[24] + lh s4, 32(a0) /* s4 = inptr[16] */ + lh s5, 64(a0) /* s5 = inptr[32] */ + lh s6, 96(a0) /* s6 = inptr[48] */ + lh t1, 112(a0) /* t1 = inptr[56] */ + lh t7, 16(a0) /* t7 = inptr[8] */ + lh t5, 80(a0) /* t5 = inptr[40] */ + lh t3, 48(a0) /* t3 = inptr[24] */ or s4, s4, t1 or s4, s4, t3 or s4, s4, t5 @@ -1855,9 +1855,9 @@ LEAF_DSPR2(jsimd_idct_islow_dspr2) or s4, s4, s6 bnez s4, 2f addiu v1, v1, -1 - lh s5, 0(a1) // quantptr[DCTSIZE*0] - lh s6, 0(a0) // inptr[DCTSIZE*0] - mul s5, s5, s6 // DEQUANTIZE(inptr[0], quantptr[0]) + lh s5, 0(a1) /* quantptr[DCTSIZE*0] */ + lh s6, 0(a0) /* inptr[DCTSIZE*0] */ + mul s5, s5, s6 /* DEQUANTIZE(inptr[0], quantptr[0]) */ sll s5, s5, 2 sw s5, 0(v0) sw s5, 32(v0) @@ -1873,68 +1873,77 @@ LEAF_DSPR2(jsimd_idct_islow_dspr2) lh t2, 48(a1) lh t4, 80(a1) lh t6, 16(a1) - mul t0, t0, t1 // DEQUANTIZE(inptr[DCTSIZE*7], quantptr[DCTSIZE*7]) - mul t1, t2, t3 // DEQUANTIZE(inptr[DCTSIZE*3], quantptr[DCTSIZE*3]) - mul t2, t4, t5 // DEQUANTIZE(inptr[DCTSIZE*5], quantptr[DCTSIZE*5]) - mul t3, t6, t7 // DEQUANTIZE(inptr[DCTSIZE*1], quantptr[DCTSIZE*1]) + mul t0, t0, t1 /* DEQUANTIZE(inptr[DCTSIZE*7], + quantptr[DCTSIZE*7]) */ + mul t1, t2, t3 /* DEQUANTIZE(inptr[DCTSIZE*3], + quantptr[DCTSIZE*3]) */ + mul t2, t4, t5 /* DEQUANTIZE(inptr[DCTSIZE*5], + quantptr[DCTSIZE*5]) */ + mul t3, t6, t7 /* DEQUANTIZE(inptr[DCTSIZE*1], + quantptr[DCTSIZE*1]) */ lh t4, 32(a1) lh t5, 32(a0) lh t6, 96(a1) lh t7, 96(a0) - addu s0, t0, t1 // z3 = tmp0 + tmp2 - addu s1, t1, t2 // z2 = tmp1 + tmp2 - addu s2, t2, t3 // z4 = tmp1 + tmp3 - addu s3, s0, s2 // z3 + z4 - addiu t9, zero, 9633 // FIX_1_175875602 - mul s3, s3, t9 // z5 = MULTIPLY(z3 + z4, FIX_1_175875602) - addu t8, t0, t3 // z1 = tmp0 + tmp3 - addiu t9, zero, 2446 // FIX_0_298631336 - mul t0, t0, t9 // tmp0 = MULTIPLY(tmp0, FIX_0_298631336) - addiu t9, zero, 16819 // FIX_2_053119869 - mul t2, t2, t9 // tmp1 = MULTIPLY(tmp1, FIX_2_053119869) - addiu t9, zero, 25172 // FIX_3_072711026 - mul t1, t1, t9 // tmp2 = MULTIPLY(tmp2, FIX_3_072711026) - addiu t9, zero, 12299 // FIX_1_501321110 - mul t3, t3, t9 // tmp3 = MULTIPLY(tmp3, FIX_1_501321110) - addiu t9, zero, 16069 // FIX_1_961570560 - mul s0, s0, t9 // -z3 = MULTIPLY(z3, FIX_1_961570560) - addiu t9, zero, 3196 // FIX_0_390180644 - mul s2, s2, t9 // -z4 = MULTIPLY(z4, FIX_0_390180644) - addiu t9, zero, 7373 // FIX_0_899976223 - mul t8, t8, t9 // -z1 = MULTIPLY(z1, FIX_0_899976223) - addiu t9, zero, 20995 // FIX_2_562915447 - mul s1, s1, t9 // -z2 = MULTIPLY(z2, FIX_2_562915447) - subu s0, s3, s0 // z3 += z5 - addu t0, t0, s0 // tmp0 += z3 - addu t1, t1, s0 // tmp2 += z3 - subu s2, s3, s2 // z4 += z5 - addu t2, t2, s2 // tmp1 += z4 - addu t3, t3, s2 // tmp3 += z4 - subu t0, t0, t8 // tmp0 += z1 - subu t1, t1, s1 // tmp2 += z2 - subu t2, t2, s1 // tmp1 += z2 - subu t3, t3, t8 // tmp3 += z1 - mul s0, t4, t5 // DEQUANTIZE(inptr[DCTSIZE*2], quantptr[DCTSIZE*2]) - addiu t9, zero, 6270 // FIX_0_765366865 - mul s1, t6, t7 // DEQUANTIZE(inptr[DCTSIZE*6], quantptr[DCTSIZE*6]) + addu s0, t0, t1 /* z3 = tmp0 + tmp2 */ + addu s1, t1, t2 /* z2 = tmp1 + tmp2 */ + addu s2, t2, t3 /* z4 = tmp1 + tmp3 */ + addu s3, s0, s2 /* z3 + z4 */ + addiu t9, zero, 9633 /* FIX_1_175875602 */ + mul s3, s3, t9 /* z5 = MULTIPLY(z3 + z4, FIX_1_175875602) */ + addu t8, t0, t3 /* z1 = tmp0 + tmp3 */ + addiu t9, zero, 2446 /* FIX_0_298631336 */ + mul t0, t0, t9 /* tmp0 = MULTIPLY(tmp0, FIX_0_298631336) */ + addiu t9, zero, 16819 /* FIX_2_053119869 */ + mul t2, t2, t9 /* tmp1 = MULTIPLY(tmp1, FIX_2_053119869) */ + addiu t9, zero, 25172 /* FIX_3_072711026 */ + mul t1, t1, t9 /* tmp2 = MULTIPLY(tmp2, FIX_3_072711026) */ + addiu t9, zero, 12299 /* FIX_1_501321110 */ + mul t3, t3, t9 /* tmp3 = MULTIPLY(tmp3, FIX_1_501321110) */ + addiu t9, zero, 16069 /* FIX_1_961570560 */ + mul s0, s0, t9 /* -z3 = MULTIPLY(z3, FIX_1_961570560) */ + addiu t9, zero, 3196 /* FIX_0_390180644 */ + mul s2, s2, t9 /* -z4 = MULTIPLY(z4, FIX_0_390180644) */ + addiu t9, zero, 7373 /* FIX_0_899976223 */ + mul t8, t8, t9 /* -z1 = MULTIPLY(z1, FIX_0_899976223) */ + addiu t9, zero, 20995 /* FIX_2_562915447 */ + mul s1, s1, t9 /* -z2 = MULTIPLY(z2, FIX_2_562915447) */ + subu s0, s3, s0 /* z3 += z5 */ + addu t0, t0, s0 /* tmp0 += z3 */ + addu t1, t1, s0 /* tmp2 += z3 */ + subu s2, s3, s2 /* z4 += z5 */ + addu t2, t2, s2 /* tmp1 += z4 */ + addu t3, t3, s2 /* tmp3 += z4 */ + subu t0, t0, t8 /* tmp0 += z1 */ + subu t1, t1, s1 /* tmp2 += z2 */ + subu t2, t2, s1 /* tmp1 += z2 */ + subu t3, t3, t8 /* tmp3 += z1 */ + mul s0, t4, t5 /* DEQUANTIZE(inptr[DCTSIZE*2], + quantptr[DCTSIZE*2]) */ + addiu t9, zero, 6270 /* FIX_0_765366865 */ + mul s1, t6, t7 /* DEQUANTIZE(inptr[DCTSIZE*6], + quantptr[DCTSIZE*6]) */ lh t4, 0(a1) lh t5, 0(a0) lh t6, 64(a1) lh t7, 64(a0) - mul s2, t9, s0 // MULTIPLY(z2, FIX_0_765366865) - mul t5, t4, t5 // DEQUANTIZE(inptr[DCTSIZE*0], quantptr[DCTSIZE*0]) - mul t6, t6, t7 // DEQUANTIZE(inptr[DCTSIZE*4], quantptr[DCTSIZE*4]) - addiu t9, zero, 4433 // FIX_0_541196100 - addu s3, s0, s1 // z2 + z3 - mul s3, s3, t9 // z1 = MULTIPLY(z2 + z3, FIX_0_541196100) - addiu t9, zero, 15137 // FIX_1_847759065 - mul t8, s1, t9 // MULTIPLY(z3, FIX_1_847759065) + mul s2, t9, s0 /* MULTIPLY(z2, FIX_0_765366865) */ + mul t5, t4, t5 /* DEQUANTIZE(inptr[DCTSIZE*0], + quantptr[DCTSIZE*0]) */ + mul t6, t6, t7 /* DEQUANTIZE(inptr[DCTSIZE*4], + quantptr[DCTSIZE*4]) */ + addiu t9, zero, 4433 /* FIX_0_541196100 */ + addu s3, s0, s1 /* z2 + z3 */ + mul s3, s3, t9 /* z1 = MULTIPLY(z2 + z3, FIX_0_541196100) */ + addiu t9, zero, 15137 /* FIX_1_847759065 */ + mul t8, s1, t9 /* MULTIPLY(z3, FIX_1_847759065) */ addu t4, t5, t6 subu t5, t5, t6 - sll t4, t4, 13 // tmp0 = (z2 + z3) << CONST_BITS - sll t5, t5, 13 // tmp1 = (z2 - z3) << CONST_BITS - addu t7, s3, s2 // tmp3 = z1 + MULTIPLY(z2, FIX_0_765366865) - subu t6, s3, t8 // tmp2 = z1 + MULTIPLY(z3, -FIX_1_847759065) + sll t4, t4, 13 /* tmp0 = (z2 + z3) << CONST_BITS */ + sll t5, t5, 13 /* tmp1 = (z2 - z3) << CONST_BITS */ + addu t7, s3, s2 /* tmp3 = z1 + MULTIPLY(z2, FIX_0_765366865) */ + subu t6, s3, t8 /* tmp2 = + z1 + MULTIPLY(z3, -FIX_1_847759065) */ addu s0, t4, t7 subu s1, t4, t7 addu s2, t5, t6 @@ -1971,14 +1980,14 @@ LEAF_DSPR2(jsimd_idct_islow_dspr2) move v0, sp addiu v1, zero, 8 4: - lw t0, 8(v0) // z2 = (JLONG)wsptr[2] - lw t1, 24(v0) // z3 = (JLONG)wsptr[6] - lw t2, 0(v0) // (JLONG)wsptr[0] - lw t3, 16(v0) // (JLONG)wsptr[4] - lw s4, 4(v0) // (JLONG)wsptr[1] - lw s5, 12(v0) // (JLONG)wsptr[3] - lw s6, 20(v0) // (JLONG)wsptr[5] - lw s7, 28(v0) // (JLONG)wsptr[7] + lw t0, 8(v0) /* z2 = (JLONG)wsptr[2] */ + lw t1, 24(v0) /* z3 = (JLONG)wsptr[6] */ + lw t2, 0(v0) /* (JLONG)wsptr[0] */ + lw t3, 16(v0) /* (JLONG)wsptr[4] */ + lw s4, 4(v0) /* (JLONG)wsptr[1] */ + lw s5, 12(v0) /* (JLONG)wsptr[3] */ + lw s6, 20(v0) /* (JLONG)wsptr[5] */ + lw s7, 28(v0) /* (JLONG)wsptr[7] */ or s4, s4, t0 or s4, s4, t1 or s4, s4, t3 @@ -1997,60 +2006,64 @@ LEAF_DSPR2(jsimd_idct_islow_dspr2) b 6f nop 5: - addu t4, t0, t1 // z2 + z3 - addiu t8, zero, 4433 // FIX_0_541196100 - mul t5, t4, t8 // z1 = MULTIPLY(z2 + z3, FIX_0_541196100) - addiu t8, zero, 15137 // FIX_1_847759065 - mul t1, t1, t8 // MULTIPLY(z3, FIX_1_847759065) - addiu t8, zero, 6270 // FIX_0_765366865 - mul t0, t0, t8 // MULTIPLY(z2, FIX_0_765366865) - addu t4, t2, t3 // (JLONG)wsptr[0] + (JLONG)wsptr[4] - subu t2, t2, t3 // (JLONG)wsptr[0] - (JLONG)wsptr[4] - sll t4, t4, 13 // tmp0 = (wsptr[0] + wsptr[4]) << CONST_BITS - sll t2, t2, 13 // tmp1 = (wsptr[0] - wsptr[4]) << CONST_BITS - subu t1, t5, t1 // tmp2 = z1 + MULTIPLY(z3, -FIX_1_847759065) - subu t3, t2, t1 // tmp12 = tmp1 - tmp2 - addu t2, t2, t1 // tmp11 = tmp1 + tmp2 - addu t5, t5, t0 // tmp3 = z1 + MULTIPLY(z2, FIX_0_765366865) - subu t1, t4, t5 // tmp13 = tmp0 - tmp3 - addu t0, t4, t5 // tmp10 = tmp0 + tmp3 - lw t4, 28(v0) // tmp0 = (JLONG)wsptr[7] - lw t6, 12(v0) // tmp2 = (JLONG)wsptr[3] - lw t5, 20(v0) // tmp1 = (JLONG)wsptr[5] - lw t7, 4(v0) // tmp3 = (JLONG)wsptr[1] - addu s0, t4, t6 // z3 = tmp0 + tmp2 - addiu t8, zero, 9633 // FIX_1_175875602 - addu s1, t5, t7 // z4 = tmp1 + tmp3 - addu s2, s0, s1 // z3 + z4 - mul s2, s2, t8 // z5 = MULTIPLY(z3 + z4, FIX_1_175875602) - addu s3, t4, t7 // z1 = tmp0 + tmp3 - addu t9, t5, t6 // z2 = tmp1 + tmp2 - addiu t8, zero, 16069 // FIX_1_961570560 - mul s0, s0, t8 // -z3 = MULTIPLY(z3, FIX_1_961570560) - addiu t8, zero, 3196 // FIX_0_390180644 - mul s1, s1, t8 // -z4 = MULTIPLY(z4, FIX_0_390180644) - addiu t8, zero, 2446 // FIX_0_298631336 - mul t4, t4, t8 // tmp0 = MULTIPLY(tmp0, FIX_0_298631336) - addiu t8, zero, 7373 // FIX_0_899976223 - mul s3, s3, t8 // -z1 = MULTIPLY(z1, FIX_0_899976223) - addiu t8, zero, 16819 // FIX_2_053119869 - mul t5, t5, t8 // tmp1 = MULTIPLY(tmp1, FIX_2_053119869) - addiu t8, zero, 20995 // FIX_2_562915447 - mul t9, t9, t8 // -z2 = MULTIPLY(z2, FIX_2_562915447) - addiu t8, zero, 25172 // FIX_3_072711026 - mul t6, t6, t8 // tmp2 = MULTIPLY(tmp2, FIX_3_072711026) - addiu t8, zero, 12299 // FIX_1_501321110 - mul t7, t7, t8 // tmp3 = MULTIPLY(tmp3, FIX_1_501321110) - subu s0, s2, s0 // z3 += z5 - subu s1, s2, s1 // z4 += z5 + addu t4, t0, t1 /* z2 + z3 */ + addiu t8, zero, 4433 /* FIX_0_541196100 */ + mul t5, t4, t8 /* z1 = MULTIPLY(z2 + z3, FIX_0_541196100) */ + addiu t8, zero, 15137 /* FIX_1_847759065 */ + mul t1, t1, t8 /* MULTIPLY(z3, FIX_1_847759065) */ + addiu t8, zero, 6270 /* FIX_0_765366865 */ + mul t0, t0, t8 /* MULTIPLY(z2, FIX_0_765366865) */ + addu t4, t2, t3 /* (JLONG)wsptr[0] + (JLONG)wsptr[4] */ + subu t2, t2, t3 /* (JLONG)wsptr[0] - (JLONG)wsptr[4] */ + sll t4, t4, 13 /* tmp0 = + (wsptr[0] + wsptr[4]) << CONST_BITS */ + sll t2, t2, 13 /* tmp1 = + (wsptr[0] - wsptr[4]) << CONST_BITS */ + subu t1, t5, t1 /* tmp2 = + z1 + MULTIPLY(z3, -FIX_1_847759065) */ + subu t3, t2, t1 /* tmp12 = tmp1 - tmp2 */ + addu t2, t2, t1 /* tmp11 = tmp1 + tmp2 */ + addu t5, t5, t0 /* tmp3 = + z1 + MULTIPLY(z2, FIX_0_765366865) */ + subu t1, t4, t5 /* tmp13 = tmp0 - tmp3 */ + addu t0, t4, t5 /* tmp10 = tmp0 + tmp3 */ + lw t4, 28(v0) /* tmp0 = (JLONG)wsptr[7] */ + lw t6, 12(v0) /* tmp2 = (JLONG)wsptr[3] */ + lw t5, 20(v0) /* tmp1 = (JLONG)wsptr[5] */ + lw t7, 4(v0) /* tmp3 = (JLONG)wsptr[1] */ + addu s0, t4, t6 /* z3 = tmp0 + tmp2 */ + addiu t8, zero, 9633 /* FIX_1_175875602 */ + addu s1, t5, t7 /* z4 = tmp1 + tmp3 */ + addu s2, s0, s1 /* z3 + z4 */ + mul s2, s2, t8 /* z5 = MULTIPLY(z3 + z4, FIX_1_175875602) */ + addu s3, t4, t7 /* z1 = tmp0 + tmp3 */ + addu t9, t5, t6 /* z2 = tmp1 + tmp2 */ + addiu t8, zero, 16069 /* FIX_1_961570560 */ + mul s0, s0, t8 /* -z3 = MULTIPLY(z3, FIX_1_961570560) */ + addiu t8, zero, 3196 /* FIX_0_390180644 */ + mul s1, s1, t8 /* -z4 = MULTIPLY(z4, FIX_0_390180644) */ + addiu t8, zero, 2446 /* FIX_0_298631336 */ + mul t4, t4, t8 /* tmp0 = MULTIPLY(tmp0, FIX_0_298631336) */ + addiu t8, zero, 7373 /* FIX_0_899976223 */ + mul s3, s3, t8 /* -z1 = MULTIPLY(z1, FIX_0_899976223) */ + addiu t8, zero, 16819 /* FIX_2_053119869 */ + mul t5, t5, t8 /* tmp1 = MULTIPLY(tmp1, FIX_2_053119869) */ + addiu t8, zero, 20995 /* FIX_2_562915447 */ + mul t9, t9, t8 /* -z2 = MULTIPLY(z2, FIX_2_562915447) */ + addiu t8, zero, 25172 /* FIX_3_072711026 */ + mul t6, t6, t8 /* tmp2 = MULTIPLY(tmp2, FIX_3_072711026) */ + addiu t8, zero, 12299 /* FIX_1_501321110 */ + mul t7, t7, t8 /* tmp3 = MULTIPLY(tmp3, FIX_1_501321110) */ + subu s0, s2, s0 /* z3 += z5 */ + subu s1, s2, s1 /* z4 += z5 */ addu t4, t4, s0 - subu t4, t4, s3 // tmp0 + subu t4, t4, s3 /* tmp0 */ addu t5, t5, s1 - subu t5, t5, t9 // tmp1 + subu t5, t5, t9 /* tmp1 */ addu t6, t6, s0 - subu t6, t6, t9 // tmp2 + subu t6, t6, t9 /* tmp2 */ addu t7, t7, s1 - subu t7, t7, s3 // tmp3 + subu t7, t7, s3 /* tmp3 */ addu s0, t0, t7 subu t0, t0, t7 addu t7, t2, t6 @@ -2116,124 +2129,124 @@ LEAF_DSPR2(jsimd_idct_ifast_cols_dspr2) */ SAVE_REGS_ON_STACK 32, s0, s1, s2, s3, s4, s5, s6, s7 - addiu t9, a0, 16 // end address + addiu t9, a0, 16 /* end address */ or AT, a3, zero 0: - lw s0, 0(a1) // quantptr[DCTSIZE*0] - lw t0, 0(a0) // inptr[DCTSIZE*0] - lw t1, 16(a0) // inptr[DCTSIZE*1] - muleq_s.w.phl v0, t0, s0 // tmp0 ... - lw t2, 32(a0) // inptr[DCTSIZE*2] - lw t3, 48(a0) // inptr[DCTSIZE*3] - lw t4, 64(a0) // inptr[DCTSIZE*4] - lw t5, 80(a0) // inptr[DCTSIZE*5] - muleq_s.w.phr t0, t0, s0 // ... tmp0 ... - lw t6, 96(a0) // inptr[DCTSIZE*6] - lw t7, 112(a0) // inptr[DCTSIZE*7] + lw s0, 0(a1) /* quantptr[DCTSIZE*0] */ + lw t0, 0(a0) /* inptr[DCTSIZE*0] */ + lw t1, 16(a0) /* inptr[DCTSIZE*1] */ + muleq_s.w.phl v0, t0, s0 /* tmp0 ... */ + lw t2, 32(a0) /* inptr[DCTSIZE*2] */ + lw t3, 48(a0) /* inptr[DCTSIZE*3] */ + lw t4, 64(a0) /* inptr[DCTSIZE*4] */ + lw t5, 80(a0) /* inptr[DCTSIZE*5] */ + muleq_s.w.phr t0, t0, s0 /* ... tmp0 ... */ + lw t6, 96(a0) /* inptr[DCTSIZE*6] */ + lw t7, 112(a0) /* inptr[DCTSIZE*7] */ or s4, t1, t2 or s5, t3, t4 bnez s4, 1f - ins t0, v0, 16, 16 // ... tmp0 + ins t0, v0, 16, 16 /* ... tmp0 */ bnez s5, 1f or s6, t5, t6 or s6, s6, t7 bnez s6, 1f - sw t0, 0(a2) // wsptr[DCTSIZE*0] - sw t0, 16(a2) // wsptr[DCTSIZE*1] - sw t0, 32(a2) // wsptr[DCTSIZE*2] - sw t0, 48(a2) // wsptr[DCTSIZE*3] - sw t0, 64(a2) // wsptr[DCTSIZE*4] - sw t0, 80(a2) // wsptr[DCTSIZE*5] - sw t0, 96(a2) // wsptr[DCTSIZE*6] - sw t0, 112(a2) // wsptr[DCTSIZE*7] + sw t0, 0(a2) /* wsptr[DCTSIZE*0] */ + sw t0, 16(a2) /* wsptr[DCTSIZE*1] */ + sw t0, 32(a2) /* wsptr[DCTSIZE*2] */ + sw t0, 48(a2) /* wsptr[DCTSIZE*3] */ + sw t0, 64(a2) /* wsptr[DCTSIZE*4] */ + sw t0, 80(a2) /* wsptr[DCTSIZE*5] */ + sw t0, 96(a2) /* wsptr[DCTSIZE*6] */ + sw t0, 112(a2) /* wsptr[DCTSIZE*7] */ addiu a0, a0, 4 b 2f addiu a1, a1, 4 1: - lw s1, 32(a1) // quantptr[DCTSIZE*2] - lw s2, 64(a1) // quantptr[DCTSIZE*4] - muleq_s.w.phl v0, t2, s1 // tmp1 ... - muleq_s.w.phr t2, t2, s1 // ... tmp1 ... - lw s0, 16(a1) // quantptr[DCTSIZE*1] - lw s1, 48(a1) // quantptr[DCTSIZE*3] - lw s3, 96(a1) // quantptr[DCTSIZE*6] - muleq_s.w.phl v1, t4, s2 // tmp2 ... - muleq_s.w.phr t4, t4, s2 // ... tmp2 ... - lw s2, 80(a1) // quantptr[DCTSIZE*5] - lw t8, 4(AT) // FIX(1.414213562) - ins t2, v0, 16, 16 // ... tmp1 - muleq_s.w.phl v0, t6, s3 // tmp3 ... - muleq_s.w.phr t6, t6, s3 // ... tmp3 ... - ins t4, v1, 16, 16 // ... tmp2 - addq.ph s4, t0, t4 // tmp10 - subq.ph s5, t0, t4 // tmp11 - ins t6, v0, 16, 16 // ... tmp3 - subq.ph s6, t2, t6 // tmp12 ... - addq.ph s7, t2, t6 // tmp13 - mulq_s.ph s6, s6, t8 // ... tmp12 ... - addq.ph t0, s4, s7 // tmp0 - subq.ph t6, s4, s7 // tmp3 - muleq_s.w.phl v0, t1, s0 // tmp4 ... - muleq_s.w.phr t1, t1, s0 // ... tmp4 ... - shll_s.ph s6, s6, 1 // x2 - lw s3, 112(a1) // quantptr[DCTSIZE*7] - subq.ph s6, s6, s7 // ... tmp12 - muleq_s.w.phl v1, t7, s3 // tmp7 ... - muleq_s.w.phr t7, t7, s3 // ... tmp7 ... - ins t1, v0, 16, 16 // ... tmp4 - addq.ph t2, s5, s6 // tmp1 - subq.ph t4, s5, s6 // tmp2 - muleq_s.w.phl v0, t5, s2 // tmp6 ... - muleq_s.w.phr t5, t5, s2 // ... tmp6 ... - ins t7, v1, 16, 16 // ... tmp7 - addq.ph s5, t1, t7 // z11 - subq.ph s6, t1, t7 // z12 - muleq_s.w.phl v1, t3, s1 // tmp5 ... - muleq_s.w.phr t3, t3, s1 // ... tmp5 ... - ins t5, v0, 16, 16 // ... tmp6 - ins t3, v1, 16, 16 // ... tmp5 - addq.ph s7, t5, t3 // z13 - subq.ph v0, t5, t3 // z10 - addq.ph t7, s5, s7 // tmp7 - subq.ph s5, s5, s7 // tmp11 ... - addq.ph v1, v0, s6 // z5 ... - mulq_s.ph s5, s5, t8 // ... tmp11 - lw t8, 8(AT) // FIX(1.847759065) - lw s4, 0(AT) // FIX(1.082392200) + lw s1, 32(a1) /* quantptr[DCTSIZE*2] */ + lw s2, 64(a1) /* quantptr[DCTSIZE*4] */ + muleq_s.w.phl v0, t2, s1 /* tmp1 ... */ + muleq_s.w.phr t2, t2, s1 /* ... tmp1 ... */ + lw s0, 16(a1) /* quantptr[DCTSIZE*1] */ + lw s1, 48(a1) /* quantptr[DCTSIZE*3] */ + lw s3, 96(a1) /* quantptr[DCTSIZE*6] */ + muleq_s.w.phl v1, t4, s2 /* tmp2 ... */ + muleq_s.w.phr t4, t4, s2 /* ... tmp2 ... */ + lw s2, 80(a1) /* quantptr[DCTSIZE*5] */ + lw t8, 4(AT) /* FIX(1.414213562) */ + ins t2, v0, 16, 16 /* ... tmp1 */ + muleq_s.w.phl v0, t6, s3 /* tmp3 ... */ + muleq_s.w.phr t6, t6, s3 /* ... tmp3 ... */ + ins t4, v1, 16, 16 /* ... tmp2 */ + addq.ph s4, t0, t4 /* tmp10 */ + subq.ph s5, t0, t4 /* tmp11 */ + ins t6, v0, 16, 16 /* ... tmp3 */ + subq.ph s6, t2, t6 /* tmp12 ... */ + addq.ph s7, t2, t6 /* tmp13 */ + mulq_s.ph s6, s6, t8 /* ... tmp12 ... */ + addq.ph t0, s4, s7 /* tmp0 */ + subq.ph t6, s4, s7 /* tmp3 */ + muleq_s.w.phl v0, t1, s0 /* tmp4 ... */ + muleq_s.w.phr t1, t1, s0 /* ... tmp4 ... */ + shll_s.ph s6, s6, 1 /* x2 */ + lw s3, 112(a1) /* quantptr[DCTSIZE*7] */ + subq.ph s6, s6, s7 /* ... tmp12 */ + muleq_s.w.phl v1, t7, s3 /* tmp7 ... */ + muleq_s.w.phr t7, t7, s3 /* ... tmp7 ... */ + ins t1, v0, 16, 16 /* ... tmp4 */ + addq.ph t2, s5, s6 /* tmp1 */ + subq.ph t4, s5, s6 /* tmp2 */ + muleq_s.w.phl v0, t5, s2 /* tmp6 ... */ + muleq_s.w.phr t5, t5, s2 /* ... tmp6 ... */ + ins t7, v1, 16, 16 /* ... tmp7 */ + addq.ph s5, t1, t7 /* z11 */ + subq.ph s6, t1, t7 /* z12 */ + muleq_s.w.phl v1, t3, s1 /* tmp5 ... */ + muleq_s.w.phr t3, t3, s1 /* ... tmp5 ... */ + ins t5, v0, 16, 16 /* ... tmp6 */ + ins t3, v1, 16, 16 /* ... tmp5 */ + addq.ph s7, t5, t3 /* z13 */ + subq.ph v0, t5, t3 /* z10 */ + addq.ph t7, s5, s7 /* tmp7 */ + subq.ph s5, s5, s7 /* tmp11 ... */ + addq.ph v1, v0, s6 /* z5 ... */ + mulq_s.ph s5, s5, t8 /* ... tmp11 */ + lw t8, 8(AT) /* FIX(1.847759065) */ + lw s4, 0(AT) /* FIX(1.082392200) */ addq.ph s0, t0, t7 subq.ph s1, t0, t7 - mulq_s.ph v1, v1, t8 // ... z5 - shll_s.ph s5, s5, 1 // x2 - lw t8, 12(AT) // FIX(-2.613125930) - sw s0, 0(a2) // wsptr[DCTSIZE*0] - shll_s.ph v0, v0, 1 // x4 - mulq_s.ph v0, v0, t8 // tmp12 ... - mulq_s.ph s4, s6, s4 // tmp10 ... - shll_s.ph v1, v1, 1 // x2 + mulq_s.ph v1, v1, t8 /* ... z5 */ + shll_s.ph s5, s5, 1 /* x2 */ + lw t8, 12(AT) /* FIX(-2.613125930) */ + sw s0, 0(a2) /* wsptr[DCTSIZE*0] */ + shll_s.ph v0, v0, 1 /* x4 */ + mulq_s.ph v0, v0, t8 /* tmp12 ... */ + mulq_s.ph s4, s6, s4 /* tmp10 ... */ + shll_s.ph v1, v1, 1 /* x2 */ addiu a0, a0, 4 addiu a1, a1, 4 - sw s1, 112(a2) // wsptr[DCTSIZE*7] - shll_s.ph s6, v0, 1 // x4 - shll_s.ph s4, s4, 1 // x2 - addq.ph s6, s6, v1 // ... tmp12 - subq.ph t5, s6, t7 // tmp6 - subq.ph s4, s4, v1 // ... tmp10 - subq.ph t3, s5, t5 // tmp5 + sw s1, 112(a2) /* wsptr[DCTSIZE*7] */ + shll_s.ph s6, v0, 1 /* x4 */ + shll_s.ph s4, s4, 1 /* x2 */ + addq.ph s6, s6, v1 /* ... tmp12 */ + subq.ph t5, s6, t7 /* tmp6 */ + subq.ph s4, s4, v1 /* ... tmp10 */ + subq.ph t3, s5, t5 /* tmp5 */ addq.ph s2, t2, t5 - addq.ph t1, s4, t3 // tmp4 + addq.ph t1, s4, t3 /* tmp4 */ subq.ph s3, t2, t5 - sw s2, 16(a2) // wsptr[DCTSIZE*1] - sw s3, 96(a2) // wsptr[DCTSIZE*6] + sw s2, 16(a2) /* wsptr[DCTSIZE*1] */ + sw s3, 96(a2) /* wsptr[DCTSIZE*6] */ addq.ph v0, t4, t3 subq.ph v1, t4, t3 - sw v0, 32(a2) // wsptr[DCTSIZE*2] - sw v1, 80(a2) // wsptr[DCTSIZE*5] + sw v0, 32(a2) /* wsptr[DCTSIZE*2] */ + sw v1, 80(a2) /* wsptr[DCTSIZE*5] */ addq.ph v0, t6, t1 subq.ph v1, t6, t1 - sw v0, 64(a2) // wsptr[DCTSIZE*4] - sw v1, 48(a2) // wsptr[DCTSIZE*3] + sw v0, 64(a2) /* wsptr[DCTSIZE*4] */ + sw v1, 48(a2) /* wsptr[DCTSIZE*3] */ 2: bne a0, t9, 0b @@ -2257,22 +2270,22 @@ LEAF_DSPR2(jsimd_idct_ifast_rows_dspr2) */ SAVE_REGS_ON_STACK 40, s0, s1, s2, s3, s4, s5, s6, s7, s8, a3 - addiu t9, a0, 128 // end address + addiu t9, a0, 128 /* end address */ lui s8, 0x8080 ori s8, s8, 0x8080 0: - lw AT, 36(sp) // restore $a3 (mips_idct_ifast_coefs) - lw t0, 0(a0) // wsptr[DCTSIZE*0+0/1] b a - lw s0, 16(a0) // wsptr[DCTSIZE*1+0/1] B A - lw t2, 4(a0) // wsptr[DCTSIZE*0+2/3] d c - lw s2, 20(a0) // wsptr[DCTSIZE*1+2/3] D C - lw t4, 8(a0) // wsptr[DCTSIZE*0+4/5] f e - lw s4, 24(a0) // wsptr[DCTSIZE*1+4/5] F E - lw t6, 12(a0) // wsptr[DCTSIZE*0+6/7] h g - lw s6, 28(a0) // wsptr[DCTSIZE*1+6/7] H G - precrq.ph.w t1, s0, t0 // B b - ins t0, s0, 16, 16 // A a + lw AT, 36(sp) /* restore $a3 (mips_idct_ifast_coefs) */ + lw t0, 0(a0) /* wsptr[DCTSIZE*0+0/1] b a */ + lw s0, 16(a0) /* wsptr[DCTSIZE*1+0/1] B A */ + lw t2, 4(a0) /* wsptr[DCTSIZE*0+2/3] d c */ + lw s2, 20(a0) /* wsptr[DCTSIZE*1+2/3] D C */ + lw t4, 8(a0) /* wsptr[DCTSIZE*0+4/5] f e */ + lw s4, 24(a0) /* wsptr[DCTSIZE*1+4/5] F E */ + lw t6, 12(a0) /* wsptr[DCTSIZE*0+6/7] h g */ + lw s6, 28(a0) /* wsptr[DCTSIZE*1+6/7] H G */ + precrq.ph.w t1, s0, t0 /* B b */ + ins t0, s0, 16, 16 /* A a */ bnez t1, 1f or s0, t2, s2 bnez s0, 1f @@ -2280,15 +2293,15 @@ LEAF_DSPR2(jsimd_idct_ifast_rows_dspr2) bnez s0, 1f or s0, t6, s6 bnez s0, 1f - shll_s.ph s0, t0, 2 // A a + shll_s.ph s0, t0, 2 /* A a */ lw a3, 0(a1) lw AT, 4(a1) - precrq.ph.w t0, s0, s0 // A A - ins s0, s0, 16, 16 // a a + precrq.ph.w t0, s0, s0 /* A A */ + ins s0, s0, 16, 16 /* a a */ addu a3, a3, a2 addu AT, AT, a2 - precrq.qb.ph t0, t0, t0 // A A A A - precrq.qb.ph s0, s0, s0 // a a a a + precrq.qb.ph t0, t0, t0 /* A A A A */ + precrq.qb.ph s0, s0, s0 /* a a a a */ addu.qb s0, s0, s8 addu.qb t0, t0, s8 sw s0, 0(a3) @@ -2308,85 +2321,85 @@ LEAF_DSPR2(jsimd_idct_ifast_rows_dspr2) ins t4, s4, 16, 16 precrq.ph.w t7, s6, t6 ins t6, s6, 16, 16 - lw t8, 4(AT) // FIX(1.414213562) - addq.ph s4, t0, t4 // tmp10 - subq.ph s5, t0, t4 // tmp11 - subq.ph s6, t2, t6 // tmp12 ... - addq.ph s7, t2, t6 // tmp13 - mulq_s.ph s6, s6, t8 // ... tmp12 ... - addq.ph t0, s4, s7 // tmp0 - subq.ph t6, s4, s7 // tmp3 - shll_s.ph s6, s6, 1 // x2 - subq.ph s6, s6, s7 // ... tmp12 - addq.ph t2, s5, s6 // tmp1 - subq.ph t4, s5, s6 // tmp2 - addq.ph s5, t1, t7 // z11 - subq.ph s6, t1, t7 // z12 - addq.ph s7, t5, t3 // z13 - subq.ph v0, t5, t3 // z10 - addq.ph t7, s5, s7 // tmp7 - subq.ph s5, s5, s7 // tmp11 ... - addq.ph v1, v0, s6 // z5 ... - mulq_s.ph s5, s5, t8 // ... tmp11 - lw t8, 8(AT) // FIX(1.847759065) - lw s4, 0(AT) // FIX(1.082392200) - addq.ph s0, t0, t7 // tmp0 + tmp7 - subq.ph s7, t0, t7 // tmp0 - tmp7 - mulq_s.ph v1, v1, t8 // ... z5 + lw t8, 4(AT) /* FIX(1.414213562) */ + addq.ph s4, t0, t4 /* tmp10 */ + subq.ph s5, t0, t4 /* tmp11 */ + subq.ph s6, t2, t6 /* tmp12 ... */ + addq.ph s7, t2, t6 /* tmp13 */ + mulq_s.ph s6, s6, t8 /* ... tmp12 ... */ + addq.ph t0, s4, s7 /* tmp0 */ + subq.ph t6, s4, s7 /* tmp3 */ + shll_s.ph s6, s6, 1 /* x2 */ + subq.ph s6, s6, s7 /* ... tmp12 */ + addq.ph t2, s5, s6 /* tmp1 */ + subq.ph t4, s5, s6 /* tmp2 */ + addq.ph s5, t1, t7 /* z11 */ + subq.ph s6, t1, t7 /* z12 */ + addq.ph s7, t5, t3 /* z13 */ + subq.ph v0, t5, t3 /* z10 */ + addq.ph t7, s5, s7 /* tmp7 */ + subq.ph s5, s5, s7 /* tmp11 ... */ + addq.ph v1, v0, s6 /* z5 ... */ + mulq_s.ph s5, s5, t8 /* ... tmp11 */ + lw t8, 8(AT) /* FIX(1.847759065) */ + lw s4, 0(AT) /* FIX(1.082392200) */ + addq.ph s0, t0, t7 /* tmp0 + tmp7 */ + subq.ph s7, t0, t7 /* tmp0 - tmp7 */ + mulq_s.ph v1, v1, t8 /* ... z5 */ lw a3, 0(a1) - lw t8, 12(AT) // FIX(-2.613125930) - shll_s.ph s5, s5, 1 // x2 + lw t8, 12(AT) /* FIX(-2.613125930) */ + shll_s.ph s5, s5, 1 /* x2 */ addu a3, a3, a2 - shll_s.ph v0, v0, 1 // x4 - mulq_s.ph v0, v0, t8 // tmp12 ... - mulq_s.ph s4, s6, s4 // tmp10 ... - shll_s.ph v1, v1, 1 // x2 + shll_s.ph v0, v0, 1 /* x4 */ + mulq_s.ph v0, v0, t8 /* tmp12 ... */ + mulq_s.ph s4, s6, s4 /* tmp10 ... */ + shll_s.ph v1, v1, 1 /* x2 */ addiu a0, a0, 32 addiu a1, a1, 8 - shll_s.ph s6, v0, 1 // x4 - shll_s.ph s4, s4, 1 // x2 - addq.ph s6, s6, v1 // ... tmp12 + shll_s.ph s6, v0, 1 /* x4 */ + shll_s.ph s4, s4, 1 /* x2 */ + addq.ph s6, s6, v1 /* ... tmp12 */ shll_s.ph s0, s0, 2 - subq.ph t5, s6, t7 // tmp6 - subq.ph s4, s4, v1 // ... tmp10 - subq.ph t3, s5, t5 // tmp5 + subq.ph t5, s6, t7 /* tmp6 */ + subq.ph s4, s4, v1 /* ... tmp10 */ + subq.ph t3, s5, t5 /* tmp5 */ shll_s.ph s7, s7, 2 - addq.ph t1, s4, t3 // tmp4 - addq.ph s1, t2, t5 // tmp1 + tmp6 - subq.ph s6, t2, t5 // tmp1 - tmp6 - addq.ph s2, t4, t3 // tmp2 + tmp5 - subq.ph s5, t4, t3 // tmp2 - tmp5 - addq.ph s4, t6, t1 // tmp3 + tmp4 - subq.ph s3, t6, t1 // tmp3 - tmp4 + addq.ph t1, s4, t3 /* tmp4 */ + addq.ph s1, t2, t5 /* tmp1 + tmp6 */ + subq.ph s6, t2, t5 /* tmp1 - tmp6 */ + addq.ph s2, t4, t3 /* tmp2 + tmp5 */ + subq.ph s5, t4, t3 /* tmp2 - tmp5 */ + addq.ph s4, t6, t1 /* tmp3 + tmp4 */ + subq.ph s3, t6, t1 /* tmp3 - tmp4 */ shll_s.ph s1, s1, 2 shll_s.ph s2, s2, 2 shll_s.ph s3, s3, 2 shll_s.ph s4, s4, 2 shll_s.ph s5, s5, 2 shll_s.ph s6, s6, 2 - precrq.ph.w t0, s1, s0 // B A - ins s0, s1, 16, 16 // b a - precrq.ph.w t2, s3, s2 // D C - ins s2, s3, 16, 16 // d c - precrq.ph.w t4, s5, s4 // F E - ins s4, s5, 16, 16 // f e - precrq.ph.w t6, s7, s6 // H G - ins s6, s7, 16, 16 // h g - precrq.qb.ph t0, t2, t0 // D C B A - precrq.qb.ph s0, s2, s0 // d c b a - precrq.qb.ph t4, t6, t4 // H G F E - precrq.qb.ph s4, s6, s4 // h g f e + precrq.ph.w t0, s1, s0 /* B A */ + ins s0, s1, 16, 16 /* b a */ + precrq.ph.w t2, s3, s2 /* D C */ + ins s2, s3, 16, 16 /* d c */ + precrq.ph.w t4, s5, s4 /* F E */ + ins s4, s5, 16, 16 /* f e */ + precrq.ph.w t6, s7, s6 /* H G */ + ins s6, s7, 16, 16 /* h g */ + precrq.qb.ph t0, t2, t0 /* D C B A */ + precrq.qb.ph s0, s2, s0 /* d c b a */ + precrq.qb.ph t4, t6, t4 /* H G F E */ + precrq.qb.ph s4, s6, s4 /* h g f e */ addu.qb s0, s0, s8 addu.qb s4, s4, s8 - sw s0, 0(a3) // outptr[0/1/2/3] d c b a - sw s4, 4(a3) // outptr[4/5/6/7] h g f e + sw s0, 0(a3) /* outptr[0/1/2/3] d c b a */ + sw s4, 4(a3) /* outptr[4/5/6/7] h g f e */ lw a3, -4(a1) addu.qb t0, t0, s8 addu a3, a3, a2 addu.qb t4, t4, s8 - sw t0, 0(a3) // outptr[0/1/2/3] D C B A + sw t0, 0(a3) /* outptr[0/1/2/3] D C B A */ bne a0, t9, 0b - sw t4, 4(a3) // outptr[4/5/6/7] H G F E + sw t4, 4(a3) /* outptr[4/5/6/7] H G F E */ 2: @@ -2428,51 +2441,51 @@ LEAF_DSPR2(jsimd_fdct_islow_dspr2) li s8, 8 move a1, a0 1: - lw s0, 0(a1) // tmp0 = 1|0 - lw s1, 4(a1) // tmp1 = 3|2 - lw s2, 8(a1) // tmp2 = 5|4 - lw s3, 12(a1) // tmp3 = 7|6 - packrl.ph s1, s1, s1 // tmp1 = 2|3 - packrl.ph s3, s3, s3 // tmp3 = 6|7 - subq.ph s7, s1, s2 // tmp7 = 2-5|3-4 = t5|t4 - subq.ph s5, s0, s3 // tmp5 = 1-6|0-7 = t6|t7 - mult $0, $0 // ac0 = 0 - dpa.w.ph $ac0, s7, t0 // ac0 += t5* 6437 + t4* 2260 - dpa.w.ph $ac0, s5, t1 // ac0 += t6* 9633 + t7* 11363 - mult $ac1, $0, $0 // ac1 = 0 - dpa.w.ph $ac1, s7, t2 // ac1 += t5*-11362 + t4* -6436 - dpa.w.ph $ac1, s5, t3 // ac1 += t6* -2259 + t7* 9633 - mult $ac2, $0, $0 // ac2 = 0 - dpa.w.ph $ac2, s7, t4 // ac2 += t5* 2261 + t4* 9633 - dpa.w.ph $ac2, s5, t5 // ac2 += t6*-11362 + t7* 6437 - mult $ac3, $0, $0 // ac3 = 0 - dpa.w.ph $ac3, s7, t6 // ac3 += t5* 9633 + t4*-11363 - dpa.w.ph $ac3, s5, t7 // ac3 += t6* -6436 + t7* 2260 - addq.ph s6, s1, s2 // tmp6 = 2+5|3+4 = t2|t3 - addq.ph s4, s0, s3 // tmp4 = 1+6|0+7 = t1|t0 - extr_r.w s0, $ac0, 11 // tmp0 = (ac0 + 1024) >> 11 - extr_r.w s1, $ac1, 11 // tmp1 = (ac1 + 1024) >> 11 - extr_r.w s2, $ac2, 11 // tmp2 = (ac2 + 1024) >> 11 - extr_r.w s3, $ac3, 11 // tmp3 = (ac3 + 1024) >> 11 - addq.ph s5, s4, s6 // tmp5 = t1+t2|t0+t3 = t11|t10 - subq.ph s7, s4, s6 // tmp7 = t1-t2|t0-t3 = t12|t13 + lw s0, 0(a1) /* tmp0 = 1|0 */ + lw s1, 4(a1) /* tmp1 = 3|2 */ + lw s2, 8(a1) /* tmp2 = 5|4 */ + lw s3, 12(a1) /* tmp3 = 7|6 */ + packrl.ph s1, s1, s1 /* tmp1 = 2|3 */ + packrl.ph s3, s3, s3 /* tmp3 = 6|7 */ + subq.ph s7, s1, s2 /* tmp7 = 2-5|3-4 = t5|t4 */ + subq.ph s5, s0, s3 /* tmp5 = 1-6|0-7 = t6|t7 */ + mult $0, $0 /* ac0 = 0 */ + dpa.w.ph $ac0, s7, t0 /* ac0 += t5* 6437 + t4* 2260 */ + dpa.w.ph $ac0, s5, t1 /* ac0 += t6* 9633 + t7* 11363 */ + mult $ac1, $0, $0 /* ac1 = 0 */ + dpa.w.ph $ac1, s7, t2 /* ac1 += t5*-11362 + t4* -6436 */ + dpa.w.ph $ac1, s5, t3 /* ac1 += t6* -2259 + t7* 9633 */ + mult $ac2, $0, $0 /* ac2 = 0 */ + dpa.w.ph $ac2, s7, t4 /* ac2 += t5* 2261 + t4* 9633 */ + dpa.w.ph $ac2, s5, t5 /* ac2 += t6*-11362 + t7* 6437 */ + mult $ac3, $0, $0 /* ac3 = 0 */ + dpa.w.ph $ac3, s7, t6 /* ac3 += t5* 9633 + t4*-11363 */ + dpa.w.ph $ac3, s5, t7 /* ac3 += t6* -6436 + t7* 2260 */ + addq.ph s6, s1, s2 /* tmp6 = 2+5|3+4 = t2|t3 */ + addq.ph s4, s0, s3 /* tmp4 = 1+6|0+7 = t1|t0 */ + extr_r.w s0, $ac0, 11 /* tmp0 = (ac0 + 1024) >> 11 */ + extr_r.w s1, $ac1, 11 /* tmp1 = (ac1 + 1024) >> 11 */ + extr_r.w s2, $ac2, 11 /* tmp2 = (ac2 + 1024) >> 11 */ + extr_r.w s3, $ac3, 11 /* tmp3 = (ac3 + 1024) >> 11 */ + addq.ph s5, s4, s6 /* tmp5 = t1+t2|t0+t3 = t11|t10 */ + subq.ph s7, s4, s6 /* tmp7 = t1-t2|t0-t3 = t12|t13 */ sh s0, 2(a1) sh s1, 6(a1) sh s2, 10(a1) sh s3, 14(a1) - mult $0, $0 // ac0 = 0 - dpa.w.ph $ac0, s7, t8 // ac0 += t12* 4433 + t13* 10703 - mult $ac1, $0, $0 // ac1 = 0 - dpa.w.ph $ac1, s7, t9 // ac1 += t12*-10704 + t13* 4433 - sra s4, s5, 16 // tmp4 = t11 + mult $0, $0 /* ac0 = 0 */ + dpa.w.ph $ac0, s7, t8 /* ac0 += t12* 4433 + t13* 10703 */ + mult $ac1, $0, $0 /* ac1 = 0 */ + dpa.w.ph $ac1, s7, t9 /* ac1 += t12*-10704 + t13* 4433 */ + sra s4, s5, 16 /* tmp4 = t11 */ addiu a1, a1, 16 addiu s8, s8, -1 - extr_r.w s0, $ac0, 11 // tmp0 = (ac0 + 1024) >> 11 - extr_r.w s1, $ac1, 11 // tmp1 = (ac1 + 1024) >> 11 - addu s2, s5, s4 // tmp2 = t10 + t11 - subu s3, s5, s4 // tmp3 = t10 - t11 - sll s2, s2, 2 // tmp2 = (t10 + t11) << 2 - sll s3, s3, 2 // tmp3 = (t10 - t11) << 2 + extr_r.w s0, $ac0, 11 /* tmp0 = (ac0 + 1024) >> 11 */ + extr_r.w s1, $ac1, 11 /* tmp1 = (ac1 + 1024) >> 11 */ + addu s2, s5, s4 /* tmp2 = t10 + t11 */ + subu s3, s5, s4 /* tmp3 = t10 - t11 */ + sll s2, s2, 2 /* tmp2 = (t10 + t11) << 2 */ + sll s3, s3, 2 /* tmp3 = (t10 - t11) << 2 */ sh s2, -16(a1) sh s3, -8(a1) sh s0, -12(a1) @@ -2492,62 +2505,62 @@ LEAF_DSPR2(jsimd_fdct_islow_dspr2) li s8, 8 2: - lh a2, 0(a0) // 0 - lh a3, 16(a0) // 8 - lh v0, 32(a0) // 16 - lh v1, 48(a0) // 24 - lh s4, 64(a0) // 32 - lh s5, 80(a0) // 40 - lh s6, 96(a0) // 48 - lh s7, 112(a0) // 56 - addu s2, v0, s5 // tmp2 = 16 + 40 - subu s5, v0, s5 // tmp5 = 16 - 40 - addu s3, v1, s4 // tmp3 = 24 + 32 - subu s4, v1, s4 // tmp4 = 24 - 32 - addu s0, a2, s7 // tmp0 = 0 + 56 - subu s7, a2, s7 // tmp7 = 0 - 56 - addu s1, a3, s6 // tmp1 = 8 + 48 - subu s6, a3, s6 // tmp6 = 8 - 48 - addu a2, s0, s3 // tmp10 = tmp0 + tmp3 - subu v1, s0, s3 // tmp13 = tmp0 - tmp3 - addu a3, s1, s2 // tmp11 = tmp1 + tmp2 - subu v0, s1, s2 // tmp12 = tmp1 - tmp2 - mult s7, t1 // ac0 = tmp7 * c1 - madd s4, t0 // ac0 += tmp4 * c0 - madd s5, t4 // ac0 += tmp5 * c4 - madd s6, t2 // ac0 += tmp6 * c2 - mult $ac1, s7, t2 // ac1 = tmp7 * c2 - msub $ac1, s4, t3 // ac1 -= tmp4 * c3 - msub $ac1, s5, t6 // ac1 -= tmp5 * c6 - msub $ac1, s6, t7 // ac1 -= tmp6 * c7 - mult $ac2, s7, t4 // ac2 = tmp7 * c4 - madd $ac2, s4, t2 // ac2 += tmp4 * c2 - madd $ac2, s5, t5 // ac2 += tmp5 * c5 - msub $ac2, s6, t6 // ac2 -= tmp6 * c6 - mult $ac3, s7, t0 // ac3 = tmp7 * c0 - msub $ac3, s4, t1 // ac3 -= tmp4 * c1 - madd $ac3, s5, t2 // ac3 += tmp5 * c2 - msub $ac3, s6, t3 // ac3 -= tmp6 * c3 - extr_r.w s0, $ac0, 15 // tmp0 = (ac0 + 16384) >> 15 - extr_r.w s1, $ac1, 15 // tmp1 = (ac1 + 16384) >> 15 - extr_r.w s2, $ac2, 15 // tmp2 = (ac2 + 16384) >> 15 - extr_r.w s3, $ac3, 15 // tmp3 = (ac3 + 16384) >> 15 + lh a2, 0(a0) /* 0 */ + lh a3, 16(a0) /* 8 */ + lh v0, 32(a0) /* 16 */ + lh v1, 48(a0) /* 24 */ + lh s4, 64(a0) /* 32 */ + lh s5, 80(a0) /* 40 */ + lh s6, 96(a0) /* 48 */ + lh s7, 112(a0) /* 56 */ + addu s2, v0, s5 /* tmp2 = 16 + 40 */ + subu s5, v0, s5 /* tmp5 = 16 - 40 */ + addu s3, v1, s4 /* tmp3 = 24 + 32 */ + subu s4, v1, s4 /* tmp4 = 24 - 32 */ + addu s0, a2, s7 /* tmp0 = 0 + 56 */ + subu s7, a2, s7 /* tmp7 = 0 - 56 */ + addu s1, a3, s6 /* tmp1 = 8 + 48 */ + subu s6, a3, s6 /* tmp6 = 8 - 48 */ + addu a2, s0, s3 /* tmp10 = tmp0 + tmp3 */ + subu v1, s0, s3 /* tmp13 = tmp0 - tmp3 */ + addu a3, s1, s2 /* tmp11 = tmp1 + tmp2 */ + subu v0, s1, s2 /* tmp12 = tmp1 - tmp2 */ + mult s7, t1 /* ac0 = tmp7 * c1 */ + madd s4, t0 /* ac0 += tmp4 * c0 */ + madd s5, t4 /* ac0 += tmp5 * c4 */ + madd s6, t2 /* ac0 += tmp6 * c2 */ + mult $ac1, s7, t2 /* ac1 = tmp7 * c2 */ + msub $ac1, s4, t3 /* ac1 -= tmp4 * c3 */ + msub $ac1, s5, t6 /* ac1 -= tmp5 * c6 */ + msub $ac1, s6, t7 /* ac1 -= tmp6 * c7 */ + mult $ac2, s7, t4 /* ac2 = tmp7 * c4 */ + madd $ac2, s4, t2 /* ac2 += tmp4 * c2 */ + madd $ac2, s5, t5 /* ac2 += tmp5 * c5 */ + msub $ac2, s6, t6 /* ac2 -= tmp6 * c6 */ + mult $ac3, s7, t0 /* ac3 = tmp7 * c0 */ + msub $ac3, s4, t1 /* ac3 -= tmp4 * c1 */ + madd $ac3, s5, t2 /* ac3 += tmp5 * c2 */ + msub $ac3, s6, t3 /* ac3 -= tmp6 * c3 */ + extr_r.w s0, $ac0, 15 /* tmp0 = (ac0 + 16384) >> 15 */ + extr_r.w s1, $ac1, 15 /* tmp1 = (ac1 + 16384) >> 15 */ + extr_r.w s2, $ac2, 15 /* tmp2 = (ac2 + 16384) >> 15 */ + extr_r.w s3, $ac3, 15 /* tmp3 = (ac3 + 16384) >> 15 */ addiu s8, s8, -1 - addu s4, a2, a3 // tmp4 = tmp10 + tmp11 - subu s5, a2, a3 // tmp5 = tmp10 - tmp11 + addu s4, a2, a3 /* tmp4 = tmp10 + tmp11 */ + subu s5, a2, a3 /* tmp5 = tmp10 - tmp11 */ sh s0, 16(a0) sh s1, 48(a0) sh s2, 80(a0) sh s3, 112(a0) - mult v0, t8 // ac0 = tmp12 * c8 - madd v1, t9 // ac0 += tmp13 * c9 - mult $ac1, v1, t8 // ac1 = tmp13 * c8 - msub $ac1, v0, a1 // ac1 -= tmp12 * c10 + mult v0, t8 /* ac0 = tmp12 * c8 */ + madd v1, t9 /* ac0 += tmp13 * c9 */ + mult $ac1, v1, t8 /* ac1 = tmp13 * c8 */ + msub $ac1, v0, a1 /* ac1 -= tmp12 * c10 */ addiu a0, a0, 2 - extr_r.w s6, $ac0, 15 // tmp6 = (ac0 + 16384) >> 15 - extr_r.w s7, $ac1, 15 // tmp7 = (ac1 + 16384) >> 15 - shra_r.w s4, s4, 2 // tmp4 = (tmp4 + 2) >> 2 - shra_r.w s5, s5, 2 // tmp5 = (tmp5 + 2) >> 2 + extr_r.w s6, $ac0, 15 /* tmp6 = (ac0 + 16384) >> 15 */ + extr_r.w s7, $ac1, 15 /* tmp7 = (ac1 + 16384) >> 15 */ + shra_r.w s4, s4, 2 /* tmp4 = (tmp4 + 2) >> 2 */ + shra_r.w s5, s5, 2 /* tmp5 = (tmp5 + 2) >> 2 */ sh s4, -2(a0) sh s5, 62(a0) sh s6, 30(a0) @@ -2571,55 +2584,59 @@ LEAF_DSPR2(jsimd_fdct_ifast_dspr2) SAVE_REGS_ON_STACK 8, s0, s1 - li a1, 0x014e014e // FIX_1_306562965 (334 << 16)|(334 & 0xffff) - li a2, 0x008b008b // FIX_0_541196100 (139 << 16)|(139 & 0xffff) - li a3, 0x00620062 // FIX_0_382683433 (98 << 16) |(98 & 0xffff) - li s1, 0x00b500b5 // FIX_0_707106781 (181 << 16)|(181 & 0xffff) + li a1, 0x014e014e /* FIX_1_306562965 (334 << 16) | + (334 & 0xffff) */ + li a2, 0x008b008b /* FIX_0_541196100 (139 << 16) | + (139 & 0xffff) */ + li a3, 0x00620062 /* FIX_0_382683433 (98 << 16) | + (98 & 0xffff) */ + li s1, 0x00b500b5 /* FIX_0_707106781 (181 << 16) | + (181 & 0xffff) */ move v0, a0 - addiu v1, v0, 128 // end address + addiu v1, v0, 128 /* end address */ 0: - lw t0, 0(v0) // tmp0 = 1|0 - lw t1, 4(v0) // tmp1 = 3|2 - lw t2, 8(v0) // tmp2 = 5|4 - lw t3, 12(v0) // tmp3 = 7|6 - packrl.ph t1, t1, t1 // tmp1 = 2|3 - packrl.ph t3, t3, t3 // tmp3 = 6|7 - subq.ph t7, t1, t2 // tmp7 = 2-5|3-4 = t5|t4 - subq.ph t5, t0, t3 // tmp5 = 1-6|0-7 = t6|t7 - addq.ph t6, t1, t2 // tmp6 = 2+5|3+4 = t2|t3 - addq.ph t4, t0, t3 // tmp4 = 1+6|0+7 = t1|t0 - addq.ph t8, t4, t6 // tmp5 = t1+t2|t0+t3 = t11|t10 - subq.ph t9, t4, t6 // tmp7 = t1-t2|t0-t3 = t12|t13 - sra t4, t8, 16 // tmp4 = t11 - mult $0, $0 // ac0 = 0 + lw t0, 0(v0) /* tmp0 = 1|0 */ + lw t1, 4(v0) /* tmp1 = 3|2 */ + lw t2, 8(v0) /* tmp2 = 5|4 */ + lw t3, 12(v0) /* tmp3 = 7|6 */ + packrl.ph t1, t1, t1 /* tmp1 = 2|3 */ + packrl.ph t3, t3, t3 /* tmp3 = 6|7 */ + subq.ph t7, t1, t2 /* tmp7 = 2-5|3-4 = t5|t4 */ + subq.ph t5, t0, t3 /* tmp5 = 1-6|0-7 = t6|t7 */ + addq.ph t6, t1, t2 /* tmp6 = 2+5|3+4 = t2|t3 */ + addq.ph t4, t0, t3 /* tmp4 = 1+6|0+7 = t1|t0 */ + addq.ph t8, t4, t6 /* tmp5 = t1+t2|t0+t3 = t11|t10 */ + subq.ph t9, t4, t6 /* tmp7 = t1-t2|t0-t3 = t12|t13 */ + sra t4, t8, 16 /* tmp4 = t11 */ + mult $0, $0 /* ac0 = 0 */ dpa.w.ph $ac0, t9, s1 - mult $ac1, $0, $0 // ac1 = 0 - dpa.w.ph $ac1, t7, a3 // ac1 += t4*98 + t5*98 - dpsx.w.ph $ac1, t5, a3 // ac1 += t6*98 + t7*98 - mult $ac2, $0, $0 // ac2 = 0 - dpa.w.ph $ac2, t7, a2 // ac2 += t4*139 + t5*139 - mult $ac3, $0, $0 // ac3 = 0 - dpa.w.ph $ac3, t5, a1 // ac3 += t6*334 + t7*334 - precrq.ph.w t0, t5, t7 // t0 = t5|t6 - addq.ph t2, t8, t4 // tmp2 = t10 + t11 - subq.ph t3, t8, t4 // tmp3 = t10 - t11 + mult $ac1, $0, $0 /* ac1 = 0 */ + dpa.w.ph $ac1, t7, a3 /* ac1 += t4*98 + t5*98 */ + dpsx.w.ph $ac1, t5, a3 /* ac1 += t6*98 + t7*98 */ + mult $ac2, $0, $0 /* ac2 = 0 */ + dpa.w.ph $ac2, t7, a2 /* ac2 += t4*139 + t5*139 */ + mult $ac3, $0, $0 /* ac3 = 0 */ + dpa.w.ph $ac3, t5, a1 /* ac3 += t6*334 + t7*334 */ + precrq.ph.w t0, t5, t7 /* t0 = t5|t6 */ + addq.ph t2, t8, t4 /* tmp2 = t10 + t11 */ + subq.ph t3, t8, t4 /* tmp3 = t10 - t11 */ extr.w t4, $ac0, 8 - mult $0, $0 // ac0 = 0 - dpa.w.ph $ac0, t0, s1 // ac0 += t5*181 + t6*181 - extr.w t0, $ac1, 8 // t0 = z5 - extr.w t1, $ac2, 8 // t1 = MULTIPLY(tmp10, 139) - extr.w t7, $ac3, 8 // t2 = MULTIPLY(tmp12, 334) - extr.w t8, $ac0, 8 // t8 = z3 = MULTIPLY(tmp11, 181) - add t6, t1, t0 // t6 = z2 - add t7, t7, t0 // t7 = z4 - subq.ph t0, t5, t8 // t0 = z13 = tmp7 - z3 - addq.ph t8, t5, t8 // t9 = z11 = tmp7 + z3 - addq.ph t1, t0, t6 // t1 = z13 + z2 - subq.ph t6, t0, t6 // t6 = z13 - z2 - addq.ph t0, t8, t7 // t0 = z11 + z4 - subq.ph t7, t8, t7 // t7 = z11 - z4 + mult $0, $0 /* ac0 = 0 */ + dpa.w.ph $ac0, t0, s1 /* ac0 += t5*181 + t6*181 */ + extr.w t0, $ac1, 8 /* t0 = z5 */ + extr.w t1, $ac2, 8 /* t1 = MULTIPLY(tmp10, 139) */ + extr.w t7, $ac3, 8 /* t2 = MULTIPLY(tmp12, 334) */ + extr.w t8, $ac0, 8 /* t8 = z3 = MULTIPLY(tmp11, 181) */ + add t6, t1, t0 /* t6 = z2 */ + add t7, t7, t0 /* t7 = z4 */ + subq.ph t0, t5, t8 /* t0 = z13 = tmp7 - z3 */ + addq.ph t8, t5, t8 /* t9 = z11 = tmp7 + z3 */ + addq.ph t1, t0, t6 /* t1 = z13 + z2 */ + subq.ph t6, t0, t6 /* t6 = z13 - z2 */ + addq.ph t0, t8, t7 /* t0 = z11 + z4 */ + subq.ph t7, t8, t7 /* t7 = z11 - z4 */ addq.ph t5, t4, t9 subq.ph t4, t9, t4 sh t2, 0(v0) @@ -2637,64 +2654,69 @@ LEAF_DSPR2(jsimd_fdct_ifast_dspr2) addiu v1, v0, 16 1: - lh t0, 0(v0) // 0 - lh t1, 16(v0) // 8 - lh t2, 32(v0) // 16 - lh t3, 48(v0) // 24 - lh t4, 64(v0) // 32 - lh t5, 80(v0) // 40 - lh t6, 96(v0) // 48 - lh t7, 112(v0) // 56 - add t8, t0, t7 // t8 = tmp0 - sub t7, t0, t7 // t7 = tmp7 - add t0, t1, t6 // t0 = tmp1 - sub t1, t1, t6 // t1 = tmp6 - add t6, t2, t5 // t6 = tmp2 - sub t5, t2, t5 // t5 = tmp5 - add t2, t3, t4 // t2 = tmp3 - sub t3, t3, t4 // t3 = tmp4 - add t4, t8, t2 // t4 = tmp10 = tmp0 + tmp3 - sub t8, t8, t2 // t8 = tmp13 = tmp0 - tmp3 - sub s0, t0, t6 // s0 = tmp12 = tmp1 - tmp2 - ins t8, s0, 16, 16 // t8 = tmp12|tmp13 - add t2, t0, t6 // t2 = tmp11 = tmp1 + tmp2 - mult $0, $0 // ac0 = 0 - dpa.w.ph $ac0, t8, s1 // ac0 += t12*181 + t13*181 - add s0, t4, t2 // t8 = tmp10+tmp11 - sub t4, t4, t2 // t4 = tmp10-tmp11 + lh t0, 0(v0) /* 0 */ + lh t1, 16(v0) /* 8 */ + lh t2, 32(v0) /* 16 */ + lh t3, 48(v0) /* 24 */ + lh t4, 64(v0) /* 32 */ + lh t5, 80(v0) /* 40 */ + lh t6, 96(v0) /* 48 */ + lh t7, 112(v0) /* 56 */ + add t8, t0, t7 /* t8 = tmp0 */ + sub t7, t0, t7 /* t7 = tmp7 */ + add t0, t1, t6 /* t0 = tmp1 */ + sub t1, t1, t6 /* t1 = tmp6 */ + add t6, t2, t5 /* t6 = tmp2 */ + sub t5, t2, t5 /* t5 = tmp5 */ + add t2, t3, t4 /* t2 = tmp3 */ + sub t3, t3, t4 /* t3 = tmp4 */ + add t4, t8, t2 /* t4 = tmp10 = tmp0 + tmp3 */ + sub t8, t8, t2 /* t8 = tmp13 = tmp0 - tmp3 */ + sub s0, t0, t6 /* s0 = tmp12 = tmp1 - tmp2 */ + ins t8, s0, 16, 16 /* t8 = tmp12|tmp13 */ + add t2, t0, t6 /* t2 = tmp11 = tmp1 + tmp2 */ + mult $0, $0 /* ac0 = 0 */ + dpa.w.ph $ac0, t8, s1 /* ac0 += t12*181 + t13*181 */ + add s0, t4, t2 /* t8 = tmp10+tmp11 */ + sub t4, t4, t2 /* t4 = tmp10-tmp11 */ sh s0, 0(v0) sh t4, 64(v0) - extr.w t2, $ac0, 8 // z1 = MULTIPLY(tmp12+tmp13, FIX_0_707106781) - addq.ph t4, t8, t2 // t9 = tmp13 + z1 - subq.ph t8, t8, t2 // t2 = tmp13 - z1 + extr.w t2, $ac0, 8 /* z1 = MULTIPLY(tmp12+tmp13, + FIX_0_707106781) */ + addq.ph t4, t8, t2 /* t9 = tmp13 + z1 */ + subq.ph t8, t8, t2 /* t2 = tmp13 - z1 */ sh t4, 32(v0) sh t8, 96(v0) - add t3, t3, t5 // t3 = tmp10 = tmp4 + tmp5 - add t0, t5, t1 // t0 = tmp11 = tmp5 + tmp6 - add t1, t1, t7 // t1 = tmp12 = tmp6 + tmp7 + add t3, t3, t5 /* t3 = tmp10 = tmp4 + tmp5 */ + add t0, t5, t1 /* t0 = tmp11 = tmp5 + tmp6 */ + add t1, t1, t7 /* t1 = tmp12 = tmp6 + tmp7 */ andi t4, a1, 0xffff mul s0, t1, t4 - sra s0, s0, 8 // s0 = z4 = MULTIPLY(tmp12, FIX_1_306562965) - ins t1, t3, 16, 16 // t1 = tmp10|tmp12 - mult $0, $0 // ac0 = 0 - mulsa.w.ph $ac0, t1, a3 // ac0 += t10*98 - t12*98 - extr.w t8, $ac0, 8 // z5 = MULTIPLY(tmp10-tmp12, FIX_0_382683433) - add t2, t7, t8 // t2 = tmp7 + z5 - sub t7, t7, t8 // t7 = tmp7 - z5 + sra s0, s0, 8 /* s0 = z4 = + MULTIPLY(tmp12, FIX_1_306562965) */ + ins t1, t3, 16, 16 /* t1 = tmp10|tmp12 */ + mult $0, $0 /* ac0 = 0 */ + mulsa.w.ph $ac0, t1, a3 /* ac0 += t10*98 - t12*98 */ + extr.w t8, $ac0, 8 /* z5 = MULTIPLY(tmp10-tmp12, + FIX_0_382683433) */ + add t2, t7, t8 /* t2 = tmp7 + z5 */ + sub t7, t7, t8 /* t7 = tmp7 - z5 */ andi t4, a2, 0xffff mul t8, t3, t4 - sra t8, t8, 8 // t8 = z2 = MULTIPLY(tmp10, FIX_0_541196100) + sra t8, t8, 8 /* t8 = z2 = + MULTIPLY(tmp10, FIX_0_541196100) */ andi t4, s1, 0xffff mul t6, t0, t4 - sra t6, t6, 8 // t6 = z3 = MULTIPLY(tmp11, FIX_0_707106781) - add t0, t6, t8 // t0 = z3 + z2 - sub t1, t6, t8 // t1 = z3 - z2 - add t3, t6, s0 // t3 = z3 + z4 - sub t4, t6, s0 // t4 = z3 - z4 - sub t5, t2, t1 // t5 = dataptr[5] - sub t6, t7, t0 // t6 = dataptr[3] - add t3, t2, t3 // t3 = dataptr[1] - add t4, t7, t4 // t4 = dataptr[7] + sra t6, t6, 8 /* t6 = z3 = + MULTIPLY(tmp11, FIX_0_707106781) */ + add t0, t6, t8 /* t0 = z3 + z2 */ + sub t1, t6, t8 /* t1 = z3 - z2 */ + add t3, t6, s0 /* t3 = z3 + z4 */ + sub t4, t6, s0 /* t4 = z3 - z4 */ + sub t5, t2, t1 /* t5 = dataptr[5] */ + sub t6, t7, t0 /* t6 = dataptr[3] */ + add t3, t2, t3 /* t3 = dataptr[1] */ + add t4, t7, t4 /* t4 = dataptr[7] */ sh t5, 80(v0) sh t6, 48(v0) sh t3, 16(v0) @@ -2721,7 +2743,7 @@ LEAF_DSPR2(jsimd_quantize_dspr2) SAVE_REGS_ON_STACK 16, s0, s1, s2 - addiu v0, a2, 124 // v0 = workspace_end + addiu v0, a2, 124 /* v0 = workspace_end */ lh t0, 0(a2) lh t1, 0(a1) lh t2, 128(a1) @@ -2821,7 +2843,7 @@ LEAF_DSPR2(jsimd_quantize_float_dspr2) */ .set at - li t1, 0x46800100 // integer representation 16384.5 + li t1, 0x46800100 /* integer representation 16384.5 */ mtc1 t1, f0 li t0, 63 0: @@ -2913,30 +2935,30 @@ LEAF_DSPR2(jsimd_idct_2x2_dspr2) addiu s3, zero, -10426 addiu s4, zero, 6967 addiu s5, zero, -5906 - lh t0, 0(a1) // t0 = inptr[DCTSIZE*0] - lh t5, 0(a0) // t5 = quantptr[DCTSIZE*0] - lh t1, 48(a1) // t1 = inptr[DCTSIZE*3] - lh t6, 48(a0) // t6 = quantptr[DCTSIZE*3] + lh t0, 0(a1) /* t0 = inptr[DCTSIZE*0] */ + lh t5, 0(a0) /* t5 = quantptr[DCTSIZE*0] */ + lh t1, 48(a1) /* t1 = inptr[DCTSIZE*3] */ + lh t6, 48(a0) /* t6 = quantptr[DCTSIZE*3] */ mul t4, t5, t0 - lh t0, 16(a1) // t0 = inptr[DCTSIZE*1] - lh t5, 16(a0) // t5 = quantptr[DCTSIZE*1] + lh t0, 16(a1) /* t0 = inptr[DCTSIZE*1] */ + lh t5, 16(a0) /* t5 = quantptr[DCTSIZE*1] */ mul t6, t6, t1 mul t5, t5, t0 - lh t2, 80(a1) // t2 = inptr[DCTSIZE*5] - lh t7, 80(a0) // t7 = quantptr[DCTSIZE*5] - lh t3, 112(a1) // t3 = inptr[DCTSIZE*7] - lh t8, 112(a0) // t8 = quantptr[DCTSIZE*7] + lh t2, 80(a1) /* t2 = inptr[DCTSIZE*5] */ + lh t7, 80(a0) /* t7 = quantptr[DCTSIZE*5] */ + lh t3, 112(a1) /* t3 = inptr[DCTSIZE*7] */ + lh t8, 112(a0) /* t8 = quantptr[DCTSIZE*7] */ mul t7, t7, t2 mult zero, zero mul t8, t8, t3 - li s0, 0x73FCD746 // s0 = (29692 << 16) | (-10426 & 0xffff) - li s1, 0x1B37E8EE // s1 = (6967 << 16) | (-5906 & 0xffff) - ins t6, t5, 16, 16 // t6 = t5|t6 + li s0, 0x73FCD746 /* s0 = (29692 << 16) | (-10426 & 0xffff) */ + li s1, 0x1B37E8EE /* s1 = (6967 << 16) | (-5906 & 0xffff) */ + ins t6, t5, 16, 16 /* t6 = t5|t6 */ sll t4, t4, 15 dpa.w.ph $ac0, t6, s0 lh t1, 2(a1) lh t6, 2(a0) - ins t8, t7, 16, 16 // t8 = t7|t8 + ins t8, t7, 16, 16 /* t8 = t7|t8 */ dpa.w.ph $ac0, t8, s1 mflo t0, $ac0 mul t5, t6, t1 @@ -3122,7 +3144,7 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) * a1 = coef_block * a2 = output_buf * a3 = output_col - * 16(sp) = workspace[DCTSIZE*4]; // buffers data between passes + * 16(sp) = workspace[DCTSIZE*4] (buffers data between passes) */ .set at @@ -3138,35 +3160,44 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) li s3, 0x52031ccd 0: - lh s6, 32(t0) // inptr[DCTSIZE*2] - lh t6, 32(a0) // quantptr[DCTSIZE*2] - lh s7, 96(t0) // inptr[DCTSIZE*6] - lh t7, 96(a0) // quantptr[DCTSIZE*6] - mul t6, s6, t6 // z2 = (inptr[DCTSIZE*2] * quantptr[DCTSIZE*2]) - lh s4, 0(t0) // inptr[DCTSIZE*0] - mul t7, s7, t7 // z3 = (inptr[DCTSIZE*6] * quantptr[DCTSIZE*6]) - lh s5, 0(a0) // quantptr[0] + lh s6, 32(t0) /* inptr[DCTSIZE*2] */ + lh t6, 32(a0) /* quantptr[DCTSIZE*2] */ + lh s7, 96(t0) /* inptr[DCTSIZE*6] */ + lh t7, 96(a0) /* quantptr[DCTSIZE*6] */ + mul t6, s6, t6 /* z2 = (inptr[DCTSIZE*2] * + quantptr[DCTSIZE*2]) */ + lh s4, 0(t0) /* inptr[DCTSIZE*0] */ + mul t7, s7, t7 /* z3 = (inptr[DCTSIZE*6] * + quantptr[DCTSIZE*6]) */ + lh s5, 0(a0) /* quantptr[0] */ li s6, 15137 li s7, 6270 - mul t2, s4, s5 // tmp0 = (inptr[0] * quantptr[0]) - mul t6, s6, t6 // z2 = (inptr[DCTSIZE*2] * quantptr[DCTSIZE*2]) - lh t5, 112(t0) // inptr[DCTSIZE*7] - mul t7, s7, t7 // z3 = (inptr[DCTSIZE*6] * quantptr[DCTSIZE*6]) - lh s4, 112(a0) // quantptr[DCTSIZE*7] - lh v0, 80(t0) // inptr[DCTSIZE*5] - lh s5, 80(a0) // quantptr[DCTSIZE*5] - lh s6, 48(a0) // quantptr[DCTSIZE*3] - sll t2, t2, 14 // tmp0 <<= (CONST_BITS+1) - lh s7, 16(a0) // quantptr[DCTSIZE*1] - lh t8, 16(t0) // inptr[DCTSIZE*1] - subu t6, t6, t7 // tmp2 = MULTIPLY(z2, t5) - MULTIPLY(z3, t6) - lh t7, 48(t0) // inptr[DCTSIZE*3] - mul t5, s4, t5 // z1 = (inptr[DCTSIZE*7] * quantptr[DCTSIZE*7]) - mul v0, s5, v0 // z2 = (inptr[DCTSIZE*5] * quantptr[DCTSIZE*5]) - mul t7, s6, t7 // z3 = (inptr[DCTSIZE*3] * quantptr[DCTSIZE*3]) - mul t8, s7, t8 // z4 = (inptr[DCTSIZE*1] * quantptr[DCTSIZE*1]) - addu t3, t2, t6 // tmp10 = tmp0 + z2 - subu t4, t2, t6 // tmp10 = tmp0 - z2 + mul t2, s4, s5 /* tmp0 = (inptr[0] * quantptr[0]) */ + mul t6, s6, t6 /* z2 = (inptr[DCTSIZE*2] * + quantptr[DCTSIZE*2]) */ + lh t5, 112(t0) /* inptr[DCTSIZE*7] */ + mul t7, s7, t7 /* z3 = (inptr[DCTSIZE*6] * + quantptr[DCTSIZE*6]) */ + lh s4, 112(a0) /* quantptr[DCTSIZE*7] */ + lh v0, 80(t0) /* inptr[DCTSIZE*5] */ + lh s5, 80(a0) /* quantptr[DCTSIZE*5] */ + lh s6, 48(a0) /* quantptr[DCTSIZE*3] */ + sll t2, t2, 14 /* tmp0 <<= (CONST_BITS+1) */ + lh s7, 16(a0) /* quantptr[DCTSIZE*1] */ + lh t8, 16(t0) /* inptr[DCTSIZE*1] */ + subu t6, t6, t7 /* tmp2 = + MULTIPLY(z2, t5) - MULTIPLY(z3, t6) */ + lh t7, 48(t0) /* inptr[DCTSIZE*3] */ + mul t5, s4, t5 /* z1 = (inptr[DCTSIZE*7] * + quantptr[DCTSIZE*7]) */ + mul v0, s5, v0 /* z2 = (inptr[DCTSIZE*5] * + quantptr[DCTSIZE*5]) */ + mul t7, s6, t7 /* z3 = (inptr[DCTSIZE*3] * + quantptr[DCTSIZE*3]) */ + mul t8, s7, t8 /* z4 = (inptr[DCTSIZE*1] * + quantptr[DCTSIZE*1]) */ + addu t3, t2, t6 /* tmp10 = tmp0 + z2 */ + subu t4, t2, t6 /* tmp10 = tmp0 - z2 */ mult $ac0, zero, zero mult $ac1, zero, zero ins t5, v0, 16, 16 @@ -3185,47 +3216,56 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) subu t5, t4, s4 addu s6, t3, s5 subu s7, t3, s5 - shra_r.w t6, t6, 12 // DESCALE(tmp12 + temp1, 12) - shra_r.w t5, t5, 12 // DESCALE(tmp12 - temp1, 12) - shra_r.w s6, s6, 12 // DESCALE(tmp10 + temp2, 12) - shra_r.w s7, s7, 12 // DESCALE(tmp10 - temp2, 12) + shra_r.w t6, t6, 12 /* DESCALE(tmp12 + temp1, 12) */ + shra_r.w t5, t5, 12 /* DESCALE(tmp12 - temp1, 12) */ + shra_r.w s6, s6, 12 /* DESCALE(tmp10 + temp2, 12) */ + shra_r.w s7, s7, 12 /* DESCALE(tmp10 - temp2, 12) */ sw t6, 28(t1) sw t5, 60(t1) sw s6, -4(t1) bgtz t9, 0b sw s7, 92(t1) - // second loop three pass + /* second loop three pass */ li t9, 3 1: - lh s6, 34(t0) // inptr[DCTSIZE*2] - lh t6, 34(a0) // quantptr[DCTSIZE*2] - lh s7, 98(t0) // inptr[DCTSIZE*6] - lh t7, 98(a0) // quantptr[DCTSIZE*6] - mul t6, s6, t6 // z2 = (inptr[DCTSIZE*2] * quantptr[DCTSIZE*2]) - lh s4, 2(t0) // inptr[DCTSIZE*0] - mul t7, s7, t7 // z3 = (inptr[DCTSIZE*6] * quantptr[DCTSIZE*6]) - lh s5, 2(a0) // quantptr[DCTSIZE*0] + lh s6, 34(t0) /* inptr[DCTSIZE*2] */ + lh t6, 34(a0) /* quantptr[DCTSIZE*2] */ + lh s7, 98(t0) /* inptr[DCTSIZE*6] */ + lh t7, 98(a0) /* quantptr[DCTSIZE*6] */ + mul t6, s6, t6 /* z2 = (inptr[DCTSIZE*2] * + quantptr[DCTSIZE*2]) */ + lh s4, 2(t0) /* inptr[DCTSIZE*0] */ + mul t7, s7, t7 /* z3 = (inptr[DCTSIZE*6] * + quantptr[DCTSIZE*6]) */ + lh s5, 2(a0) /* quantptr[DCTSIZE*0] */ li s6, 15137 li s7, 6270 - mul t2, s4, s5 // tmp0 = (inptr[0] * quantptr[0]) - mul v0, s6, t6 // z2 = (inptr[DCTSIZE*2] * quantptr[DCTSIZE*2]) - lh t5, 114(t0) // inptr[DCTSIZE*7] - mul t7, s7, t7 // z3 = (inptr[DCTSIZE*6] * quantptr[DCTSIZE*6]) - lh s4, 114(a0) // quantptr[DCTSIZE*7] - lh s5, 82(a0) // quantptr[DCTSIZE*5] - lh t6, 82(t0) // inptr[DCTSIZE*5] - sll t2, t2, 14 // tmp0 <<= (CONST_BITS+1) - lh s6, 50(a0) // quantptr[DCTSIZE*3] - lh t8, 18(t0) // inptr[DCTSIZE*1] - subu v0, v0, t7 // tmp2 = MULTIPLY(z2, t5) - MULTIPLY(z3, t6) - lh t7, 50(t0) // inptr[DCTSIZE*3] - lh s7, 18(a0) // quantptr[DCTSIZE*1] - mul t5, s4, t5 // z1 = (inptr[DCTSIZE*7] * quantptr[DCTSIZE*7]) - mul t6, s5, t6 // z2 = (inptr[DCTSIZE*5] * quantptr[DCTSIZE*5]) - mul t7, s6, t7 // z3 = (inptr[DCTSIZE*3] * quantptr[DCTSIZE*3]) - mul t8, s7, t8 // z4 = (inptr[DCTSIZE*1] * quantptr[DCTSIZE*1]) - addu t3, t2, v0 // tmp10 = tmp0 + z2 - subu t4, t2, v0 // tmp10 = tmp0 - z2 + mul t2, s4, s5 /* tmp0 = (inptr[0] * quantptr[0]) */ + mul v0, s6, t6 /* z2 = (inptr[DCTSIZE*2] * + quantptr[DCTSIZE*2]) */ + lh t5, 114(t0) /* inptr[DCTSIZE*7] */ + mul t7, s7, t7 /* z3 = (inptr[DCTSIZE*6] * + quantptr[DCTSIZE*6]) */ + lh s4, 114(a0) /* quantptr[DCTSIZE*7] */ + lh s5, 82(a0) /* quantptr[DCTSIZE*5] */ + lh t6, 82(t0) /* inptr[DCTSIZE*5] */ + sll t2, t2, 14 /* tmp0 <<= (CONST_BITS+1) */ + lh s6, 50(a0) /* quantptr[DCTSIZE*3] */ + lh t8, 18(t0) /* inptr[DCTSIZE*1] */ + subu v0, v0, t7 /* tmp2 = + MULTIPLY(z2, t5) - MULTIPLY(z3, t6) */ + lh t7, 50(t0) /* inptr[DCTSIZE*3] */ + lh s7, 18(a0) /* quantptr[DCTSIZE*1] */ + mul t5, s4, t5 /* z1 = (inptr[DCTSIZE*7] * + quantptr[DCTSIZE*7]) */ + mul t6, s5, t6 /* z2 = (inptr[DCTSIZE*5] * + quantptr[DCTSIZE*5]) */ + mul t7, s6, t7 /* z3 = (inptr[DCTSIZE*3] * + quantptr[DCTSIZE*3]) */ + mul t8, s7, t8 /* z4 = (inptr[DCTSIZE*1] * + quantptr[DCTSIZE*1]) */ + addu t3, t2, v0 /* tmp10 = tmp0 + z2 */ + subu t4, t2, v0 /* tmp10 = tmp0 - z2 */ mult $ac0, zero, zero mult $ac1, zero, zero ins t5, t6, 16, 16 @@ -3244,10 +3284,10 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) subu s4, t4, t5 addu s6, t3, t6 subu s7, t3, t6 - shra_r.w s5, s5, 12 // DESCALE(tmp12 + temp1, 12) - shra_r.w s4, s4, 12 // DESCALE(tmp12 - temp1, 12) - shra_r.w s6, s6, 12 // DESCALE(tmp10 + temp2, 12) - shra_r.w s7, s7, 12 // DESCALE(tmp10 - temp2, 12) + shra_r.w s5, s5, 12 /* DESCALE(tmp12 + temp1, 12) */ + shra_r.w s4, s4, 12 /* DESCALE(tmp12 - temp1, 12) */ + shra_r.w s6, s6, 12 /* DESCALE(tmp10 + temp2, 12) */ + shra_r.w s7, s7, 12 /* DESCALE(tmp10 - temp2, 12) */ sw s5, 32(t1) sw s4, 64(t1) sw s6, 0(t1) @@ -3255,16 +3295,18 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sw s7, 96(t1) move t1, v1 li s4, 15137 - lw s6, 8(t1) // wsptr[2] + lw s6, 8(t1) /* wsptr[2] */ li s5, 6270 - lw s7, 24(t1) // wsptr[6] - mul s4, s4, s6 // MULTIPLY((JLONG)wsptr[2], FIX_1_847759065) - lw t2, 0(t1) // wsptr[0] - mul s5, s5, s7 // MULTIPLY((JLONG)wsptr[6], -FIX_0_765366865) - lh t5, 28(t1) // wsptr[7] - lh t6, 20(t1) // wsptr[5] - lh t7, 12(t1) // wsptr[3] - lh t8, 4(t1) // wsptr[1] + lw s7, 24(t1) /* wsptr[6] */ + mul s4, s4, s6 /* MULTIPLY((JLONG)wsptr[2], + FIX_1_847759065) */ + lw t2, 0(t1) /* wsptr[0] */ + mul s5, s5, s7 /* MULTIPLY((JLONG)wsptr[6], + -FIX_0_765366865) */ + lh t5, 28(t1) /* wsptr[7] */ + lh t6, 20(t1) /* wsptr[5] */ + lh t7, 12(t1) /* wsptr[3] */ + lh t8, 4(t1) /* wsptr[1] */ ins t5, t6, 16, 16 ins t7, t8, 16, 16 mult $ac0, zero, zero @@ -3273,23 +3315,25 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) mult $ac1, zero, zero dpa.w.ph $ac1, t5, s2 dpa.w.ph $ac1, t7, s3 - sll t2, t2, 14 // tmp0 = ((JLONG)wsptr[0]) << (CONST_BITS+1) + sll t2, t2, 14 /* tmp0 = + ((JLONG)wsptr[0]) << (CONST_BITS+1) */ mflo s6, $ac0 - // MULTIPLY(wsptr[2], FIX_1_847759065 + MULTIPLY(wsptr[6], -FIX_0_765366865) + /* MULTIPLY(wsptr[2], FIX_1_847759065) + + MULTIPLY(wsptr[6], -FIX_0_765366865) */ subu s4, s4, s5 - addu t3, t2, s4 // tmp10 = tmp0 + z2 + addu t3, t2, s4 /* tmp10 = tmp0 + z2 */ mflo s7, $ac1 - subu t4, t2, s4 // tmp10 = tmp0 - z2 + subu t4, t2, s4 /* tmp10 = tmp0 - z2 */ addu t7, t4, s6 subu t8, t4, s6 addu t5, t3, s7 subu t6, t3, s7 - shra_r.w t5, t5, 19 // DESCALE(tmp10 + temp2, 19) - shra_r.w t6, t6, 19 // DESCALE(tmp10 - temp2, 19) - shra_r.w t7, t7, 19 // DESCALE(tmp12 + temp1, 19) - shra_r.w t8, t8, 19 // DESCALE(tmp12 - temp1, 19) + shra_r.w t5, t5, 19 /* DESCALE(tmp10 + temp2, 19) */ + shra_r.w t6, t6, 19 /* DESCALE(tmp10 - temp2, 19) */ + shra_r.w t7, t7, 19 /* DESCALE(tmp12 + temp1, 19) */ + shra_r.w t8, t8, 19 /* DESCALE(tmp12 - temp1, 19) */ sll s4, t9, 2 - lw v0, 0(a2) // output_buf[ctr] + lw v0, 0(a2) /* output_buf[ctr] */ shll_s.w t5, t5, 24 shll_s.w t6, t6, 24 shll_s.w t7, t7, 24 @@ -3298,7 +3342,7 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sra t6, t6, 24 sra t7, t7, 24 sra t8, t8, 24 - addu v0, v0, a3 // outptr = output_buf[ctr] + output_col + addu v0, v0, a3 /* outptr = output_buf[ctr] + output_col */ addiu t5, t5, 128 addiu t6, t6, 128 addiu t7, t7, 128 @@ -3307,18 +3351,20 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sb t7, 1(v0) sb t8, 2(v0) sb t6, 3(v0) - // 2 + /* 2 */ li s4, 15137 - lw s6, 40(t1) // wsptr[2] + lw s6, 40(t1) /* wsptr[2] */ li s5, 6270 - lw s7, 56(t1) // wsptr[6] - mul s4, s4, s6 // MULTIPLY((JLONG)wsptr[2], FIX_1_847759065) - lw t2, 32(t1) // wsptr[0] - mul s5, s5, s7 // MULTIPLY((JLONG)wsptr[6], -FIX_0_765366865) - lh t5, 60(t1) // wsptr[7] - lh t6, 52(t1) // wsptr[5] - lh t7, 44(t1) // wsptr[3] - lh t8, 36(t1) // wsptr[1] + lw s7, 56(t1) /* wsptr[6] */ + mul s4, s4, s6 /* MULTIPLY((JLONG)wsptr[2], + FIX_1_847759065) */ + lw t2, 32(t1) /* wsptr[0] */ + mul s5, s5, s7 /* MULTIPLY((JLONG)wsptr[6], + -FIX_0_765366865) */ + lh t5, 60(t1) /* wsptr[7] */ + lh t6, 52(t1) /* wsptr[5] */ + lh t7, 44(t1) /* wsptr[3] */ + lh t8, 36(t1) /* wsptr[1] */ ins t5, t6, 16, 16 ins t7, t8, 16, 16 mult $ac0, zero, zero @@ -3327,23 +3373,29 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) mult $ac1, zero, zero dpa.w.ph $ac1, t5, s2 dpa.w.ph $ac1, t7, s3 - sll t2, t2, 14 // tmp0 = ((JLONG)wsptr[0]) << (CONST_BITS+1) + sll t2, t2, 14 /* tmp0 = + ((JLONG)wsptr[0]) << (CONST_BITS+1) */ mflo s6, $ac0 - // MULTIPLY(wsptr[2], FIX_1_847759065 + MULTIPLY(wsptr[6], -FIX_0_765366865) + /* MULTIPLY(wsptr[2], FIX_1_847759065) + + MULTIPLY(wsptr[6], -FIX_0_765366865) */ subu s4, s4, s5 - addu t3, t2, s4 // tmp10 = tmp0 + z2 + addu t3, t2, s4 /* tmp10 = tmp0 + z2 */ mflo s7, $ac1 - subu t4, t2, s4 // tmp10 = tmp0 - z2 + subu t4, t2, s4 /* tmp10 = tmp0 - z2 */ addu t7, t4, s6 subu t8, t4, s6 addu t5, t3, s7 subu t6, t3, s7 - shra_r.w t5, t5, 19 // DESCALE(tmp10 + temp2, CONST_BITS-PASS1_BITS+1) - shra_r.w t6, t6, 19 // DESCALE(tmp10 - temp2, CONST_BITS-PASS1_BITS+1) - shra_r.w t7, t7, 19 // DESCALE(tmp12 + temp1, CONST_BITS-PASS1_BITS+1) - shra_r.w t8, t8, 19 // DESCALE(tmp12 - temp1, CONST_BITS-PASS1_BITS+1) + shra_r.w t5, t5, 19 /* DESCALE(tmp10 + temp2, + CONST_BITS-PASS1_BITS+1) */ + shra_r.w t6, t6, 19 /* DESCALE(tmp10 - temp2, + CONST_BITS-PASS1_BITS+1) */ + shra_r.w t7, t7, 19 /* DESCALE(tmp12 + temp1, + CONST_BITS-PASS1_BITS+1) */ + shra_r.w t8, t8, 19 /* DESCALE(tmp12 - temp1, + CONST_BITS-PASS1_BITS+1) */ sll s4, t9, 2 - lw v0, 4(a2) // output_buf[ctr] + lw v0, 4(a2) /* output_buf[ctr] */ shll_s.w t5, t5, 24 shll_s.w t6, t6, 24 shll_s.w t7, t7, 24 @@ -3352,7 +3404,7 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sra t6, t6, 24 sra t7, t7, 24 sra t8, t8, 24 - addu v0, v0, a3 // outptr = output_buf[ctr] + output_col + addu v0, v0, a3 /* outptr = output_buf[ctr] + output_col */ addiu t5, t5, 128 addiu t6, t6, 128 addiu t7, t7, 128 @@ -3361,18 +3413,20 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sb t7, 1(v0) sb t8, 2(v0) sb t6, 3(v0) - // 3 + /* 3 */ li s4, 15137 - lw s6, 72(t1) // wsptr[2] + lw s6, 72(t1) /* wsptr[2] */ li s5, 6270 - lw s7, 88(t1) // wsptr[6] - mul s4, s4, s6 // MULTIPLY((JLONG)wsptr[2], FIX_1_847759065) - lw t2, 64(t1) // wsptr[0] - mul s5, s5, s7 // MULTIPLY((JLONG)wsptr[6], -FIX_0_765366865) - lh t5, 92(t1) // wsptr[7] - lh t6, 84(t1) // wsptr[5] - lh t7, 76(t1) // wsptr[3] - lh t8, 68(t1) // wsptr[1] + lw s7, 88(t1) /* wsptr[6] */ + mul s4, s4, s6 /* MULTIPLY((JLONG)wsptr[2], + FIX_1_847759065) */ + lw t2, 64(t1) /* wsptr[0] */ + mul s5, s5, s7 /* MULTIPLY((JLONG)wsptr[6], + -FIX_0_765366865) */ + lh t5, 92(t1) /* wsptr[7] */ + lh t6, 84(t1) /* wsptr[5] */ + lh t7, 76(t1) /* wsptr[3] */ + lh t8, 68(t1) /* wsptr[1] */ ins t5, t6, 16, 16 ins t7, t8, 16, 16 mult $ac0, zero, zero @@ -3381,23 +3435,25 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) mult $ac1, zero, zero dpa.w.ph $ac1, t5, s2 dpa.w.ph $ac1, t7, s3 - sll t2, t2, 14 // tmp0 = ((JLONG)wsptr[0]) << (CONST_BITS+1) + sll t2, t2, 14 /* tmp0 = + ((JLONG)wsptr[0]) << (CONST_BITS+1) */ mflo s6, $ac0 - // MULTIPLY(wsptr[2], FIX_1_847759065 + MULTIPLY(wsptr[6], -FIX_0_765366865) + /* MULTIPLY(wsptr[2], FIX_1_847759065) + + MULTIPLY(wsptr[6], -FIX_0_765366865) */ subu s4, s4, s5 - addu t3, t2, s4 // tmp10 = tmp0 + z2 + addu t3, t2, s4 /* tmp10 = tmp0 + z2 */ mflo s7, $ac1 - subu t4, t2, s4 // tmp10 = tmp0 - z2 + subu t4, t2, s4 /* tmp10 = tmp0 - z2 */ addu t7, t4, s6 subu t8, t4, s6 addu t5, t3, s7 subu t6, t3, s7 - shra_r.w t5, t5, 19 // DESCALE(tmp10 + temp2, 19) - shra_r.w t6, t6, 19 // DESCALE(tmp10 - temp2, 19) - shra_r.w t7, t7, 19 // DESCALE(tmp12 + temp1, 19) - shra_r.w t8, t8, 19 // DESCALE(tmp12 - temp1, 19) + shra_r.w t5, t5, 19 /* DESCALE(tmp10 + temp2, 19) */ + shra_r.w t6, t6, 19 /* DESCALE(tmp10 - temp2, 19) */ + shra_r.w t7, t7, 19 /* DESCALE(tmp12 + temp1, 19) */ + shra_r.w t8, t8, 19 /* DESCALE(tmp12 - temp1, 19) */ sll s4, t9, 2 - lw v0, 8(a2) // output_buf[ctr] + lw v0, 8(a2) /* output_buf[ctr] */ shll_s.w t5, t5, 24 shll_s.w t6, t6, 24 shll_s.w t7, t7, 24 @@ -3406,7 +3462,7 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sra t6, t6, 24 sra t7, t7, 24 sra t8, t8, 24 - addu v0, v0, a3 // outptr = output_buf[ctr] + output_col + addu v0, v0, a3 /* outptr = output_buf[ctr] + output_col */ addiu t5, t5, 128 addiu t6, t6, 128 addiu t7, t7, 128 @@ -3416,16 +3472,18 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sb t8, 2(v0) sb t6, 3(v0) li s4, 15137 - lw s6, 104(t1) // wsptr[2] + lw s6, 104(t1) /* wsptr[2] */ li s5, 6270 - lw s7, 120(t1) // wsptr[6] - mul s4, s4, s6 // MULTIPLY((JLONG)wsptr[2], FIX_1_847759065) - lw t2, 96(t1) // wsptr[0] - mul s5, s5, s7 // MULTIPLY((JLONG)wsptr[6], -FIX_0_765366865) - lh t5, 124(t1) // wsptr[7] - lh t6, 116(t1) // wsptr[5] - lh t7, 108(t1) // wsptr[3] - lh t8, 100(t1) // wsptr[1] + lw s7, 120(t1) /* wsptr[6] */ + mul s4, s4, s6 /* MULTIPLY((JLONG)wsptr[2], + FIX_1_847759065) */ + lw t2, 96(t1) /* wsptr[0] */ + mul s5, s5, s7 /* MULTIPLY((JLONG)wsptr[6], + -FIX_0_765366865) */ + lh t5, 124(t1) /* wsptr[7] */ + lh t6, 116(t1) /* wsptr[5] */ + lh t7, 108(t1) /* wsptr[3] */ + lh t8, 100(t1) /* wsptr[1] */ ins t5, t6, 16, 16 ins t7, t8, 16, 16 mult $ac0, zero, zero @@ -3434,23 +3492,25 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) mult $ac1, zero, zero dpa.w.ph $ac1, t5, s2 dpa.w.ph $ac1, t7, s3 - sll t2, t2, 14 // tmp0 = ((JLONG)wsptr[0]) << (CONST_BITS+1) + sll t2, t2, 14 /* tmp0 = + ((JLONG)wsptr[0]) << (CONST_BITS+1) */ mflo s6, $ac0 - // MULTIPLY(wsptr[2], FIX_1_847759065 + MULTIPLY(wsptr[6], -FIX_0_765366865) + /* MULTIPLY(wsptr[2], FIX_1_847759065) + + MULTIPLY(wsptr[6], -FIX_0_765366865) */ subu s4, s4, s5 - addu t3, t2, s4 // tmp10 = tmp0 + z2; + addu t3, t2, s4 /* tmp10 = tmp0 + z2; */ mflo s7, $ac1 - subu t4, t2, s4 // tmp10 = tmp0 - z2; + subu t4, t2, s4 /* tmp10 = tmp0 - z2; */ addu t7, t4, s6 subu t8, t4, s6 addu t5, t3, s7 subu t6, t3, s7 - shra_r.w t5, t5, 19 // DESCALE(tmp10 + temp2, 19) - shra_r.w t6, t6, 19 // DESCALE(tmp10 - temp2, 19) - shra_r.w t7, t7, 19 // DESCALE(tmp12 + temp1, 19) - shra_r.w t8, t8, 19 // DESCALE(tmp12 - temp1, 19) + shra_r.w t5, t5, 19 /* DESCALE(tmp10 + temp2, 19) */ + shra_r.w t6, t6, 19 /* DESCALE(tmp10 - temp2, 19) */ + shra_r.w t7, t7, 19 /* DESCALE(tmp12 + temp1, 19) */ + shra_r.w t8, t8, 19 /* DESCALE(tmp12 - temp1, 19) */ sll s4, t9, 2 - lw v0, 12(a2) // output_buf[ctr] + lw v0, 12(a2) /* output_buf[ctr] */ shll_s.w t5, t5, 24 shll_s.w t6, t6, 24 shll_s.w t7, t7, 24 @@ -3459,7 +3519,7 @@ LEAF_DSPR2(jsimd_idct_4x4_dspr2) sra t6, t6, 24 sra t7, t7, 24 sra t8, t8, 24 - addu v0, v0, a3 // outptr = output_buf[ctr] + output_col + addu v0, v0, a3 /* outptr = output_buf[ctr] + output_col */ addiu t5, t5, 128 addiu t6, t6, 128 addiu t7, t7, 128 @@ -3496,54 +3556,54 @@ LEAF_DSPR2(jsimd_idct_6x6_dspr2) addiu s1, zero, 2998 1: - lh s2, 0(a0) // q0 = quantptr[ 0] - lh s3, 32(a0) // q1 = quantptr[16] - lh s4, 64(a0) // q2 = quantptr[32] - lh t2, 64(a1) // tmp2 = inptr[32] - lh t1, 32(a1) // tmp1 = inptr[16] - lh t0, 0(a1) // tmp0 = inptr[ 0] - mul t2, t2, s4 // tmp2 = tmp2 * q2 - mul t1, t1, s3 // tmp1 = tmp1 * q1 - mul t0, t0, s2 // tmp0 = tmp0 * q0 - lh t6, 16(a1) // z1 = inptr[ 8] - lh t8, 80(a1) // z3 = inptr[40] - lh t7, 48(a1) // z2 = inptr[24] - lh s2, 16(a0) // q0 = quantptr[ 8] - lh s4, 80(a0) // q2 = quantptr[40] - lh s3, 48(a0) // q1 = quantptr[24] - mul t2, t2, t9 // tmp2 = tmp2 * 5793 - mul t1, t1, s0 // tmp1 = tmp1 * 10033 - sll t0, t0, 13 // tmp0 = tmp0 << 13 - mul t6, t6, s2 // z1 = z1 * q0 - mul t8, t8, s4 // z3 = z3 * q2 - mul t7, t7, s3 // z2 = z2 * q1 - addu t3, t0, t2 // tmp10 = tmp0 + tmp2 - sll t2, t2, 1 // tmp2 = tmp2 << 2 - subu t4, t0, t2 // tmp11 = tmp0 - tmp2; - subu t5, t3, t1 // tmp12 = tmp10 - tmp1 - addu t3, t3, t1 // tmp10 = tmp10 + tmp1 - addu t1, t6, t8 // tmp1 = z1 + z3 - mul t1, t1, s1 // tmp1 = tmp1 * 2998 - shra_r.w t4, t4, 11 // tmp11 = (tmp11 + 1024) >> 11 - subu t2, t6, t8 // tmp2 = z1 - z3 - subu t2, t2, t7 // tmp2 = tmp2 - z2 - sll t2, t2, 2 // tmp2 = tmp2 << 2 - addu t0, t6, t7 // tmp0 = z1 + z2 - sll t0, t0, 13 // tmp0 = tmp0 << 13 - subu s2, t8, t7 // q0 = z3 - z2 - sll s2, s2, 13 // q0 = q0 << 13 - addu t0, t0, t1 // tmp0 = tmp0 + tmp1 - addu t1, s2, t1 // tmp1 = q0 + tmp1 - addu s2, t4, t2 // q0 = tmp11 + tmp2 - subu s3, t4, t2 // q1 = tmp11 - tmp2 - addu t6, t3, t0 // z1 = tmp10 + tmp0 - subu t7, t3, t0 // z2 = tmp10 - tmp0 - addu t4, t5, t1 // tmp11 = tmp12 + tmp1 - subu t5, t5, t1 // tmp12 = tmp12 - tmp1 - shra_r.w t6, t6, 11 // z1 = (z1 + 1024) >> 11 - shra_r.w t7, t7, 11 // z2 = (z2 + 1024) >> 11 - shra_r.w t4, t4, 11 // tmp11 = (tmp11 + 1024) >> 11 - shra_r.w t5, t5, 11 // tmp12 = (tmp12 + 1024) >> 11 + lh s2, 0(a0) /* q0 = quantptr[ 0] */ + lh s3, 32(a0) /* q1 = quantptr[16] */ + lh s4, 64(a0) /* q2 = quantptr[32] */ + lh t2, 64(a1) /* tmp2 = inptr[32] */ + lh t1, 32(a1) /* tmp1 = inptr[16] */ + lh t0, 0(a1) /* tmp0 = inptr[ 0] */ + mul t2, t2, s4 /* tmp2 = tmp2 * q2 */ + mul t1, t1, s3 /* tmp1 = tmp1 * q1 */ + mul t0, t0, s2 /* tmp0 = tmp0 * q0 */ + lh t6, 16(a1) /* z1 = inptr[ 8] */ + lh t8, 80(a1) /* z3 = inptr[40] */ + lh t7, 48(a1) /* z2 = inptr[24] */ + lh s2, 16(a0) /* q0 = quantptr[ 8] */ + lh s4, 80(a0) /* q2 = quantptr[40] */ + lh s3, 48(a0) /* q1 = quantptr[24] */ + mul t2, t2, t9 /* tmp2 = tmp2 * 5793 */ + mul t1, t1, s0 /* tmp1 = tmp1 * 10033 */ + sll t0, t0, 13 /* tmp0 = tmp0 << 13 */ + mul t6, t6, s2 /* z1 = z1 * q0 */ + mul t8, t8, s4 /* z3 = z3 * q2 */ + mul t7, t7, s3 /* z2 = z2 * q1 */ + addu t3, t0, t2 /* tmp10 = tmp0 + tmp2 */ + sll t2, t2, 1 /* tmp2 = tmp2 << 2 */ + subu t4, t0, t2 /* tmp11 = tmp0 - tmp2; */ + subu t5, t3, t1 /* tmp12 = tmp10 - tmp1 */ + addu t3, t3, t1 /* tmp10 = tmp10 + tmp1 */ + addu t1, t6, t8 /* tmp1 = z1 + z3 */ + mul t1, t1, s1 /* tmp1 = tmp1 * 2998 */ + shra_r.w t4, t4, 11 /* tmp11 = (tmp11 + 1024) >> 11 */ + subu t2, t6, t8 /* tmp2 = z1 - z3 */ + subu t2, t2, t7 /* tmp2 = tmp2 - z2 */ + sll t2, t2, 2 /* tmp2 = tmp2 << 2 */ + addu t0, t6, t7 /* tmp0 = z1 + z2 */ + sll t0, t0, 13 /* tmp0 = tmp0 << 13 */ + subu s2, t8, t7 /* q0 = z3 - z2 */ + sll s2, s2, 13 /* q0 = q0 << 13 */ + addu t0, t0, t1 /* tmp0 = tmp0 + tmp1 */ + addu t1, s2, t1 /* tmp1 = q0 + tmp1 */ + addu s2, t4, t2 /* q0 = tmp11 + tmp2 */ + subu s3, t4, t2 /* q1 = tmp11 - tmp2 */ + addu t6, t3, t0 /* z1 = tmp10 + tmp0 */ + subu t7, t3, t0 /* z2 = tmp10 - tmp0 */ + addu t4, t5, t1 /* tmp11 = tmp12 + tmp1 */ + subu t5, t5, t1 /* tmp12 = tmp12 - tmp1 */ + shra_r.w t6, t6, 11 /* z1 = (z1 + 1024) >> 11 */ + shra_r.w t7, t7, 11 /* z2 = (z2 + 1024) >> 11 */ + shra_r.w t4, t4, 11 /* tmp11 = (tmp11 + 1024) >> 11 */ + shra_r.w t5, t5, 11 /* tmp12 = (tmp12 + 1024) >> 11 */ sw s2, 24(v0) sw s3, 96(v0) sw t6, 0(v0) @@ -3644,7 +3704,7 @@ LEAF_DSPR2(jsimd_idct_12x12_pass1_dspr2) li a3, 8 1: - // odd part + /* odd part */ lh t0, 48(a1) lh t1, 48(a0) lh t2, 16(a1) @@ -3653,55 +3713,55 @@ LEAF_DSPR2(jsimd_idct_12x12_pass1_dspr2) lh t5, 80(a0) lh t6, 112(a1) lh t7, 112(a0) - mul t0, t0, t1 // z2 - mul t1, t2, t3 // z1 - mul t2, t4, t5 // z3 - mul t3, t6, t7 // z4 - li t4, 10703 // FIX(1.306562965) - li t5, 4433 // FIX_0_541196100 - li t6, 7053 // FIX(0.860918669) - mul t4, t0, t4 // tmp11 - mul t5, t0, t5 // -tmp14 - addu t7, t1, t2 // tmp10 - addu t8, t7, t3 // tmp10 + z4 - mul t6, t6, t8 // tmp15 - li t8, 2139 // FIX(0.261052384) - mul t8, t7, t8 // MULTIPLY(tmp10, FIX(0.261052384)) - li t7, 2295 // FIX(0.280143716) - mul t7, t1, t7 // MULTIPLY(z1, FIX(0.280143716)) - addu t9, t2, t3 // z3 + z4 - li s0, 8565 // FIX(1.045510580) - mul t9, t9, s0 // -tmp13 - li s0, 12112 // FIX(1.478575242) - mul s0, t2, s0 // MULTIPLY(z3, FIX(1.478575242) - li s1, 12998 // FIX(1.586706681) - mul s1, t3, s1 // MULTIPLY(z4, FIX(1.586706681)) - li s2, 5540 // FIX(0.676326758) - mul s2, t1, s2 // MULTIPLY(z1, FIX(0.676326758)) - li s3, 16244 // FIX(1.982889723) - mul s3, t3, s3 // MULTIPLY(z4, FIX(1.982889723)) - subu t1, t1, t3 // z1-=z4 - subu t0, t0, t2 // z2-=z3 - addu t2, t0, t1 // z1+z2 - li t3, 4433 // FIX_0_541196100 - mul t2, t2, t3 // z3 - li t3, 6270 // FIX_0_765366865 - mul t1, t1, t3 // MULTIPLY(z1, FIX_0_765366865) - li t3, 15137 // FIX_0_765366865 - mul t0, t0, t3 // MULTIPLY(z2, FIX_1_847759065) - addu t8, t6, t8 // tmp12 - addu t3, t8, t4 // tmp12 + tmp11 - addu t3, t3, t7 // tmp10 - subu t8, t8, t9 // tmp12 + tmp13 + mul t0, t0, t1 /* z2 */ + mul t1, t2, t3 /* z1 */ + mul t2, t4, t5 /* z3 */ + mul t3, t6, t7 /* z4 */ + li t4, 10703 /* FIX(1.306562965) */ + li t5, 4433 /* FIX_0_541196100 */ + li t6, 7053 /* FIX(0.860918669) */ + mul t4, t0, t4 /* tmp11 */ + mul t5, t0, t5 /* -tmp14 */ + addu t7, t1, t2 /* tmp10 */ + addu t8, t7, t3 /* tmp10 + z4 */ + mul t6, t6, t8 /* tmp15 */ + li t8, 2139 /* FIX(0.261052384) */ + mul t8, t7, t8 /* MULTIPLY(tmp10, FIX(0.261052384)) */ + li t7, 2295 /* FIX(0.280143716) */ + mul t7, t1, t7 /* MULTIPLY(z1, FIX(0.280143716)) */ + addu t9, t2, t3 /* z3 + z4 */ + li s0, 8565 /* FIX(1.045510580) */ + mul t9, t9, s0 /* -tmp13 */ + li s0, 12112 /* FIX(1.478575242) */ + mul s0, t2, s0 /* MULTIPLY(z3, FIX(1.478575242) */ + li s1, 12998 /* FIX(1.586706681) */ + mul s1, t3, s1 /* MULTIPLY(z4, FIX(1.586706681)) */ + li s2, 5540 /* FIX(0.676326758) */ + mul s2, t1, s2 /* MULTIPLY(z1, FIX(0.676326758)) */ + li s3, 16244 /* FIX(1.982889723) */ + mul s3, t3, s3 /* MULTIPLY(z4, FIX(1.982889723)) */ + subu t1, t1, t3 /* z1-=z4 */ + subu t0, t0, t2 /* z2-=z3 */ + addu t2, t0, t1 /* z1+z2 */ + li t3, 4433 /* FIX_0_541196100 */ + mul t2, t2, t3 /* z3 */ + li t3, 6270 /* FIX_0_765366865 */ + mul t1, t1, t3 /* MULTIPLY(z1, FIX_0_765366865) */ + li t3, 15137 /* FIX_0_765366865 */ + mul t0, t0, t3 /* MULTIPLY(z2, FIX_1_847759065) */ + addu t8, t6, t8 /* tmp12 */ + addu t3, t8, t4 /* tmp12 + tmp11 */ + addu t3, t3, t7 /* tmp10 */ + subu t8, t8, t9 /* tmp12 + tmp13 */ addu s0, t5, s0 - subu t8, t8, s0 // tmp12 + subu t8, t8, s0 /* tmp12 */ subu t9, t6, t9 subu s1, s1, t4 - addu t9, t9, s1 // tmp13 + addu t9, t9, s1 /* tmp13 */ subu t6, t6, t5 subu t6, t6, s2 - subu t6, t6, s3 // tmp15 - // even part start + subu t6, t6, s3 /* tmp15 */ + /* even part start */ lh t4, 64(a1) lh t5, 64(a0) lh t7, 32(a1) @@ -3710,39 +3770,43 @@ LEAF_DSPR2(jsimd_idct_12x12_pass1_dspr2) lh s2, 0(a0) lh s3, 96(a1) lh v0, 96(a0) - mul t4, t4, t5 // DEQUANTIZE(inptr[DCTSIZE*4], quantptr[DCTSIZE*4]) - mul t5, t7, s0 // DEQUANTIZE(inptr[DCTSIZE*2], quantptr[DCTSIZE*2]) - mul t7, s1, s2 // DEQUANTIZE(inptr[DCTSIZE*0], quantptr[DCTSIZE*0]) - mul s0, s3, v0 // DEQUANTIZE(inptr[DCTSIZE*6], quantptr[DCTSIZE*6]) - // odd part end - addu t1, t2, t1 // tmp11 - subu t0, t2, t0 // tmp14 - // update counter and pointers + mul t4, t4, t5 /* DEQUANTIZE(inptr[DCTSIZE*4], + quantptr[DCTSIZE*4]) */ + mul t5, t7, s0 /* DEQUANTIZE(inptr[DCTSIZE*2], + quantptr[DCTSIZE*2]) */ + mul t7, s1, s2 /* DEQUANTIZE(inptr[DCTSIZE*0], + quantptr[DCTSIZE*0]) */ + mul s0, s3, v0 /* DEQUANTIZE(inptr[DCTSIZE*6], + quantptr[DCTSIZE*6]) */ + /* odd part end */ + addu t1, t2, t1 /* tmp11 */ + subu t0, t2, t0 /* tmp14 */ + /* update counter and pointers */ addiu a3, a3, -1 addiu a0, a0, 2 addiu a1, a1, 2 - // even part rest + /* even part rest */ li s1, 10033 li s2, 11190 - mul t4, t4, s1 // z4 - mul s1, t5, s2 // z4 - sll t5, t5, 13 // z1 + mul t4, t4, s1 /* z4 */ + mul s1, t5, s2 /* z4 */ + sll t5, t5, 13 /* z1 */ sll t7, t7, 13 - addiu t7, t7, 1024 // z3 - sll s0, s0, 13 // z2 - addu s2, t7, t4 // tmp10 - subu t4, t7, t4 // tmp11 - subu s3, t5, s0 // tmp12 - addu t2, t7, s3 // tmp21 - subu s3, t7, s3 // tmp24 - addu t7, s1, s0 // tmp12 - addu v0, s2, t7 // tmp20 - subu s2, s2, t7 // tmp25 - subu s1, s1, t5 // z4 - z1 - subu s1, s1, s0 // tmp12 - addu s0, t4, s1 // tmp22 - subu t4, t4, s1 // tmp23 - // final output stage + addiu t7, t7, 1024 /* z3 */ + sll s0, s0, 13 /* z2 */ + addu s2, t7, t4 /* tmp10 */ + subu t4, t7, t4 /* tmp11 */ + subu s3, t5, s0 /* tmp12 */ + addu t2, t7, s3 /* tmp21 */ + subu s3, t7, s3 /* tmp24 */ + addu t7, s1, s0 /* tmp12 */ + addu v0, s2, t7 /* tmp20 */ + subu s2, s2, t7 /* tmp25 */ + subu s1, s1, t5 /* z4 - z1 */ + subu s1, s1, s0 /* tmp12 */ + addu s0, t4, s1 /* tmp22 */ + subu t4, t4, s1 /* tmp23 */ + /* final output stage */ addu t5, v0, t3 subu v0, v0, t3 addu t3, t2, t1 @@ -3801,86 +3865,86 @@ LEAF_DSPR2(jsimd_idct_12x12_pass2_dspr2) li a3, 12 1: - // Odd part + /* Odd part */ lw t0, 12(a0) lw t1, 4(a0) lw t2, 20(a0) lw t3, 28(a0) - li t4, 10703 // FIX(1.306562965) - li t5, 4433 // FIX_0_541196100 - mul t4, t0, t4 // tmp11 - mul t5, t0, t5 // -tmp14 - addu t6, t1, t2 // tmp10 - li t7, 2139 // FIX(0.261052384) - mul t7, t6, t7 // MULTIPLY(tmp10, FIX(0.261052384)) - addu t6, t6, t3 // tmp10 + z4 - li t8, 7053 // FIX(0.860918669) - mul t6, t6, t8 // tmp15 - li t8, 2295 // FIX(0.280143716) - mul t8, t1, t8 // MULTIPLY(z1, FIX(0.280143716)) - addu t9, t2, t3 // z3 + z4 - li s0, 8565 // FIX(1.045510580) - mul t9, t9, s0 // -tmp13 - li s0, 12112 // FIX(1.478575242) - mul s0, t2, s0 // MULTIPLY(z3, FIX(1.478575242)) - li s1, 12998 // FIX(1.586706681) - mul s1, t3, s1 // MULTIPLY(z4, FIX(1.586706681)) - li s2, 5540 // FIX(0.676326758) - mul s2, t1, s2 // MULTIPLY(z1, FIX(0.676326758)) - li s3, 16244 // FIX(1.982889723) - mul s3, t3, s3 // MULTIPLY(z4, FIX(1.982889723)) - subu t1, t1, t3 // z1 -= z4 - subu t0, t0, t2 // z2 -= z3 - addu t2, t1, t0 // z1 + z2 - li t3, 4433 // FIX_0_541196100 - mul t2, t2, t3 // z3 - li t3, 6270 // FIX_0_765366865 - mul t1, t1, t3 // MULTIPLY(z1, FIX_0_765366865) - li t3, 15137 // FIX_1_847759065 - mul t0, t0, t3 // MULTIPLY(z2, FIX_1_847759065) - addu t3, t6, t7 // tmp12 + li t4, 10703 /* FIX(1.306562965) */ + li t5, 4433 /* FIX_0_541196100 */ + mul t4, t0, t4 /* tmp11 */ + mul t5, t0, t5 /* -tmp14 */ + addu t6, t1, t2 /* tmp10 */ + li t7, 2139 /* FIX(0.261052384) */ + mul t7, t6, t7 /* MULTIPLY(tmp10, FIX(0.261052384)) */ + addu t6, t6, t3 /* tmp10 + z4 */ + li t8, 7053 /* FIX(0.860918669) */ + mul t6, t6, t8 /* tmp15 */ + li t8, 2295 /* FIX(0.280143716) */ + mul t8, t1, t8 /* MULTIPLY(z1, FIX(0.280143716)) */ + addu t9, t2, t3 /* z3 + z4 */ + li s0, 8565 /* FIX(1.045510580) */ + mul t9, t9, s0 /* -tmp13 */ + li s0, 12112 /* FIX(1.478575242) */ + mul s0, t2, s0 /* MULTIPLY(z3, FIX(1.478575242)) */ + li s1, 12998 /* FIX(1.586706681) */ + mul s1, t3, s1 /* MULTIPLY(z4, FIX(1.586706681)) */ + li s2, 5540 /* FIX(0.676326758) */ + mul s2, t1, s2 /* MULTIPLY(z1, FIX(0.676326758)) */ + li s3, 16244 /* FIX(1.982889723) */ + mul s3, t3, s3 /* MULTIPLY(z4, FIX(1.982889723)) */ + subu t1, t1, t3 /* z1 -= z4 */ + subu t0, t0, t2 /* z2 -= z3 */ + addu t2, t1, t0 /* z1 + z2 */ + li t3, 4433 /* FIX_0_541196100 */ + mul t2, t2, t3 /* z3 */ + li t3, 6270 /* FIX_0_765366865 */ + mul t1, t1, t3 /* MULTIPLY(z1, FIX_0_765366865) */ + li t3, 15137 /* FIX_1_847759065 */ + mul t0, t0, t3 /* MULTIPLY(z2, FIX_1_847759065) */ + addu t3, t6, t7 /* tmp12 */ addu t7, t3, t4 - addu t7, t7, t8 // tmp10 + addu t7, t7, t8 /* tmp10 */ subu t3, t3, t9 subu t3, t3, t5 - subu t3, t3, s0 // tmp12 + subu t3, t3, s0 /* tmp12 */ subu t9, t6, t9 subu t9, t9, t4 - addu t9, t9, s1 // tmp13 + addu t9, t9, s1 /* tmp13 */ subu t6, t6, t5 subu t6, t6, s2 - subu t6, t6, s3 // tmp15 - addu t1, t2, t1 // tmp11 - subu t0, t2, t0 // tmp14 - // even part - lw t2, 16(a0) // z4 - lw t4, 8(a0) // z1 - lw t5, 0(a0) // z3 - lw t8, 24(a0) // z2 - li s0, 10033 // FIX(1.224744871) - li s1, 11190 // FIX(1.366025404) - mul t2, t2, s0 // z4 - mul s0, t4, s1 // z4 + subu t6, t6, s3 /* tmp15 */ + addu t1, t2, t1 /* tmp11 */ + subu t0, t2, t0 /* tmp14 */ + /* even part */ + lw t2, 16(a0) /* z4 */ + lw t4, 8(a0) /* z1 */ + lw t5, 0(a0) /* z3 */ + lw t8, 24(a0) /* z2 */ + li s0, 10033 /* FIX(1.224744871) */ + li s1, 11190 /* FIX(1.366025404) */ + mul t2, t2, s0 /* z4 */ + mul s0, t4, s1 /* z4 */ addiu t5, t5, 0x10 - sll t5, t5, 13 // z3 - sll t4, t4, 13 // z1 - sll t8, t8, 13 // z2 - subu s1, t4, t8 // tmp12 - addu s2, t5, t2 // tmp10 - subu t2, t5, t2 // tmp11 - addu s3, t5, s1 // tmp21 - subu s1, t5, s1 // tmp24 - addu t5, s0, t8 // tmp12 - addu v0, s2, t5 // tmp20 - subu t5, s2, t5 // tmp25 + sll t5, t5, 13 /* z3 */ + sll t4, t4, 13 /* z1 */ + sll t8, t8, 13 /* z2 */ + subu s1, t4, t8 /* tmp12 */ + addu s2, t5, t2 /* tmp10 */ + subu t2, t5, t2 /* tmp11 */ + addu s3, t5, s1 /* tmp21 */ + subu s1, t5, s1 /* tmp24 */ + addu t5, s0, t8 /* tmp12 */ + addu v0, s2, t5 /* tmp20 */ + subu t5, s2, t5 /* tmp25 */ subu t4, s0, t4 - subu t4, t4, t8 // tmp12 - addu t8, t2, t4 // tmp22 - subu t2, t2, t4 // tmp23 - // increment counter and pointers + subu t4, t4, t8 /* tmp12 */ + addu t8, t2, t4 /* tmp22 */ + subu t2, t2, t4 /* tmp23 */ + /* increment counter and pointers */ addiu a3, a3, -1 addiu a0, a0, 32 - // Final stage + /* Final stage */ addu t4, v0, t7 subu v0, v0, t7 addu t7, s3, t1 @@ -4169,7 +4233,7 @@ LEAF_DSPR2(jsimd_convsamp_float_dspr2) swc1 f12, 20(a2) swc1 f14, 24(a2) swc1 f16, 28(a2) - // elemr 1 + /* elemr 1 */ lbu t1, 0(t0) lbu t2, 1(t0) lbu t3, 2(t0) @@ -4212,7 +4276,7 @@ LEAF_DSPR2(jsimd_convsamp_float_dspr2) swc1 f12, 52(a2) swc1 f14, 56(a2) swc1 f16, 60(a2) - // elemr 2 + /* elemr 2 */ lbu t1, 0(t0) lbu t2, 1(t0) lbu t3, 2(t0) @@ -4255,7 +4319,7 @@ LEAF_DSPR2(jsimd_convsamp_float_dspr2) swc1 f12, 84(a2) swc1 f14, 88(a2) swc1 f16, 92(a2) - // elemr 3 + /* elemr 3 */ lbu t1, 0(t0) lbu t2, 1(t0) lbu t3, 2(t0) @@ -4298,7 +4362,7 @@ LEAF_DSPR2(jsimd_convsamp_float_dspr2) swc1 f12, 116(a2) swc1 f14, 120(a2) swc1 f16, 124(a2) - // elemr 4 + /* elemr 4 */ lbu t1, 0(t0) lbu t2, 1(t0) lbu t3, 2(t0) @@ -4341,7 +4405,7 @@ LEAF_DSPR2(jsimd_convsamp_float_dspr2) swc1 f12, 148(a2) swc1 f14, 152(a2) swc1 f16, 156(a2) - // elemr 5 + /* elemr 5 */ lbu t1, 0(t0) lbu t2, 1(t0) lbu t3, 2(t0) @@ -4384,7 +4448,7 @@ LEAF_DSPR2(jsimd_convsamp_float_dspr2) swc1 f12, 180(a2) swc1 f14, 184(a2) swc1 f16, 188(a2) - // elemr 6 + /* elemr 6 */ lbu t1, 0(t0) lbu t2, 1(t0) lbu t3, 2(t0) @@ -4427,7 +4491,7 @@ LEAF_DSPR2(jsimd_convsamp_float_dspr2) swc1 f12, 212(a2) swc1 f14, 216(a2) swc1 f16, 220(a2) - // elemr 7 + /* elemr 7 */ lbu t1, 0(t0) lbu t2, 1(t0) lbu t3, 2(t0) diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jccolext-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jccolext-mmi.c new file mode 100644 index 00000000000..558eb2ab102 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jccolext-mmi.c @@ -0,0 +1,455 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright 2009 Pierre Ossman for Cendio AB + * Copyright (C) 2014-2015, 2019, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhuChen + * SunZhangzhi + * CaiWanwei + * ZhangLixia + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jccolor-mmi.c */ + + +#if RGB_RED == 0 +#define mmA re +#define mmB ro +#elif RGB_GREEN == 0 +#define mmA ge +#define mmB go +#elif RGB_BLUE == 0 +#define mmA be +#define mmB bo +#else +#define mmA xe +#define mmB xo +#endif + +#if RGB_RED == 1 +#define mmC re +#define mmD ro +#elif RGB_GREEN == 1 +#define mmC ge +#define mmD go +#elif RGB_BLUE == 1 +#define mmC be +#define mmD bo +#else +#define mmC xe +#define mmD xo +#endif + +#if RGB_RED == 2 +#define mmE re +#define mmF ro +#elif RGB_GREEN == 2 +#define mmE ge +#define mmF go +#elif RGB_BLUE == 2 +#define mmE be +#define mmF bo +#else +#define mmE xe +#define mmF xo +#endif + +#if RGB_RED == 3 +#define mmG re +#define mmH ro +#elif RGB_GREEN == 3 +#define mmG ge +#define mmH go +#elif RGB_BLUE == 3 +#define mmG be +#define mmH bo +#else +#define mmG xe +#define mmH xo +#endif + + +void jsimd_rgb_ycc_convert_mmi(JDIMENSION image_width, JSAMPARRAY input_buf, + JSAMPIMAGE output_buf, JDIMENSION output_row, + int num_rows) +{ + JSAMPROW inptr, outptr0, outptr1, outptr2; + int num_cols, col; + __m64 re, ro, ge, go, be, bo, xe; +#if RGB_PIXELSIZE == 4 + __m64 xo; +#endif + __m64 rgle, rghe, rglo, rgho, bgle, bghe, bglo, bgho; + __m64 ble, halfble, bhe, halfbhe, blo, halfblo, bho, halfbho; + __m64 rle, halfrle, rhe, halfrhe, rlo, halfrlo, rho, halfrho; + __m64 yle_rg, yhe_rg, yle_bg, yhe_bg, yle, yhe, ye; + __m64 ylo_rg, yho_rg, ylo_bg, yho_bg, ylo, yho, yo, y; + __m64 cble, cbhe, cbe, cblo, cbho, cbo, cb; + __m64 crle, crhe, cre, crlo, crho, cro, cr; + + while (--num_rows >= 0) { + inptr = *input_buf++; + outptr0 = output_buf[0][output_row]; + outptr1 = output_buf[1][output_row]; + outptr2 = output_buf[2][output_row]; + output_row++; + + for (num_cols = image_width; num_cols > 0; num_cols -= 8, + outptr0 += 8, outptr1 += 8, outptr2 += 8) { + +#if RGB_PIXELSIZE == 3 + + if (num_cols < 8) { + col = num_cols * 3; + asm(".set noreorder\r\n" + + "li $8, 1\r\n" + "move $9, %3\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 1f\r\n" + "nop \r\n" + "subu $9, $9, 1\r\n" + "xor $12, $12, $12\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $9\r\n" + "lbu $12, 0($13)\r\n" + + "1: \r\n" + "li $8, 2\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 2f\r\n" + "nop \r\n" + "subu $9, $9, 2\r\n" + "xor $11, $11, $11\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $9\r\n" + "lhu $11, 0($13)\r\n" + "sll $12, $12, 16\r\n" + "or $12, $12, $11\r\n" + + "2: \r\n" + "dmtc1 $12, %0\r\n" + "li $8, 4\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 3f\r\n" + "nop \r\n" + "subu $9, $9, 4\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $9\r\n" + "lwu $14, 0($13)\r\n" + "dmtc1 $14, %1\r\n" + "dsll32 $12, $12, 0\r\n" + "or $12, $12, $14\r\n" + "dmtc1 $12, %0\r\n" + + "3: \r\n" + "li $8, 8\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 4f\r\n" + "nop \r\n" + "mov.s %1, %0\r\n" + "ldc1 %0, 0(%5)\r\n" + "li $9, 8\r\n" + "j 5f\r\n" + "nop \r\n" + + "4: \r\n" + "li $8, 16\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 5f\r\n" + "nop \r\n" + "mov.s %2, %0\r\n" + "ldc1 %0, 0(%5)\r\n" + "ldc1 %1, 8(%5)\r\n" + + "5: \r\n" + "nop \r\n" + ".set reorder\r\n" + + : "=f" (mmA), "=f" (mmG), "=f" (mmF) + : "r" (col), "r" (num_rows), "r" (inptr) + : "$f0", "$f2", "$f4", "$8", "$9", "$10", "$11", "$12", "$13", + "$14", "memory" + ); + } else { + if (!(((long)inptr) & 7)) { + mmA = _mm_load_si64((__m64 *)&inptr[0]); + mmG = _mm_load_si64((__m64 *)&inptr[8]); + mmF = _mm_load_si64((__m64 *)&inptr[16]); + } else { + mmA = _mm_loadu_si64((__m64 *)&inptr[0]); + mmG = _mm_loadu_si64((__m64 *)&inptr[8]); + mmF = _mm_loadu_si64((__m64 *)&inptr[16]); + } + inptr += RGB_PIXELSIZE * 8; + } + mmD = _mm_srli_si64(mmA, 4 * BYTE_BIT); + mmA = _mm_slli_si64(mmA, 4 * BYTE_BIT); + + mmA = _mm_unpackhi_pi8(mmA, mmG); + mmG = _mm_slli_si64(mmG, 4 * BYTE_BIT); + + mmD = _mm_unpacklo_pi8(mmD, mmF); + mmG = _mm_unpackhi_pi8(mmG, mmF); + + mmE = _mm_srli_si64(mmA, 4 * BYTE_BIT); + mmA = _mm_slli_si64(mmA, 4 * BYTE_BIT); + + mmA = _mm_unpackhi_pi8(mmA, mmD); + mmD = _mm_slli_si64(mmD, 4 * BYTE_BIT); + + mmE = _mm_unpacklo_pi8(mmE, mmG); + mmD = _mm_unpackhi_pi8(mmD, mmG); + mmC = _mm_loadhi_pi8_f(mmA); + mmA = _mm_loadlo_pi8_f(mmA); + + mmB = _mm_loadhi_pi8_f(mmE); + mmE = _mm_loadlo_pi8_f(mmE); + + mmF = _mm_loadhi_pi8_f(mmD); + mmD = _mm_loadlo_pi8_f(mmD); + +#else /* RGB_PIXELSIZE == 4 */ + + if (num_cols < 8) { + col = num_cols; + asm(".set noreorder\r\n" + + "li $8, 1\r\n" + "move $9, %4\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 1f\r\n" + "nop \r\n" + "subu $9, $9, 1\r\n" + PTR_SLL "$11, $9, 2\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $11\r\n" + "lwc1 %0, 0($13)\r\n" + + "1: \r\n" + "li $8, 2\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 2f\r\n" + "nop \r\n" + "subu $9, $9, 2\r\n" + PTR_SLL "$11, $9, 2\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $11\r\n" + "mov.s %1, %0\r\n" + "ldc1 %0, 0($13)\r\n" + + "2: \r\n" + "li $8, 4\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 3f\r\n" + "nop \r\n" + "mov.s %2, %0\r\n" + "mov.s %3, %1\r\n" + "ldc1 %0, 0(%5)\r\n" + "ldc1 %1, 8(%5)\r\n" + + "3: \r\n" + "nop \r\n" + ".set reorder\r\n" + + : "=f" (mmA), "=f" (mmF), "=f" (mmD), "=f" (mmC) + : "r" (col), "r" (inptr) + : "$f0", "$f2", "$8", "$9", "$10", "$11", "$13", "memory" + ); + } else { + if (!(((long)inptr) & 7)) { + mmA = _mm_load_si64((__m64 *)&inptr[0]); + mmF = _mm_load_si64((__m64 *)&inptr[8]); + mmD = _mm_load_si64((__m64 *)&inptr[16]); + mmC = _mm_load_si64((__m64 *)&inptr[24]); + } else { + mmA = _mm_loadu_si64((__m64 *)&inptr[0]); + mmF = _mm_loadu_si64((__m64 *)&inptr[8]); + mmD = _mm_loadu_si64((__m64 *)&inptr[16]); + mmC = _mm_loadu_si64((__m64 *)&inptr[24]); + } + inptr += RGB_PIXELSIZE * 8; + } + mmB = _mm_unpackhi_pi8(mmA, mmF); + mmA = _mm_unpacklo_pi8(mmA, mmF); + + mmG = _mm_unpackhi_pi8(mmD, mmC); + mmD = _mm_unpacklo_pi8(mmD, mmC); + + mmE = _mm_unpackhi_pi16(mmA, mmD); + mmA = _mm_unpacklo_pi16(mmA, mmD); + + mmH = _mm_unpackhi_pi16(mmB, mmG); + mmB = _mm_unpacklo_pi16(mmB, mmG); + + mmC = _mm_loadhi_pi8_f(mmA); + mmA = _mm_loadlo_pi8_f(mmA); + + mmD = _mm_loadhi_pi8_f(mmB); + mmB = _mm_loadlo_pi8_f(mmB); + + mmG = _mm_loadhi_pi8_f(mmE); + mmE = _mm_loadlo_pi8_f(mmE); + + mmF = _mm_unpacklo_pi8(mmH, mmH); + mmH = _mm_unpackhi_pi8(mmH, mmH); + mmF = _mm_srli_pi16(mmF, BYTE_BIT); + mmH = _mm_srli_pi16(mmH, BYTE_BIT); + +#endif + + /* re=(R0 R2 R4 R6), ge=(G0 G2 G4 G6), be=(B0 B2 B4 B6) + * ro=(R1 R3 R5 R7), go=(G1 G3 G5 G7), bo=(B1 B3 B5 B7) + * + * (Original) + * Y = 0.29900 * R + 0.58700 * G + 0.11400 * B + * Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + CENTERJSAMPLE + * Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + CENTERJSAMPLE + * + * (This implementation) + * Y = 0.29900 * R + 0.33700 * G + 0.11400 * B + 0.25000 * G + * Cb = -0.16874 * R - 0.33126 * G + 0.50000 * B + CENTERJSAMPLE + * Cr = 0.50000 * R - 0.41869 * G - 0.08131 * B + CENTERJSAMPLE + */ + + rglo = _mm_unpacklo_pi16(ro, go); + rgho = _mm_unpackhi_pi16(ro, go); + ylo_rg = _mm_madd_pi16(rglo, PW_F0299_F0337); + yho_rg = _mm_madd_pi16(rgho, PW_F0299_F0337); + cblo = _mm_madd_pi16(rglo, PW_MF016_MF033); + cbho = _mm_madd_pi16(rgho, PW_MF016_MF033); + + blo = _mm_loadlo_pi16_f(bo); + bho = _mm_loadhi_pi16_f(bo); + halfblo = _mm_srli_pi32(blo, 1); + halfbho = _mm_srli_pi32(bho, 1); + + cblo = _mm_add_pi32(cblo, halfblo); + cbho = _mm_add_pi32(cbho, halfbho); + cblo = _mm_add_pi32(cblo, PD_ONEHALFM1_CJ); + cbho = _mm_add_pi32(cbho, PD_ONEHALFM1_CJ); + cblo = _mm_srli_pi32(cblo, SCALEBITS); + cbho = _mm_srli_pi32(cbho, SCALEBITS); + cbo = _mm_packs_pi32(cblo, cbho); + + rgle = _mm_unpacklo_pi16(re, ge); + rghe = _mm_unpackhi_pi16(re, ge); + yle_rg = _mm_madd_pi16(rgle, PW_F0299_F0337); + yhe_rg = _mm_madd_pi16(rghe, PW_F0299_F0337); + cble = _mm_madd_pi16(rgle, PW_MF016_MF033); + cbhe = _mm_madd_pi16(rghe, PW_MF016_MF033); + + ble = _mm_loadlo_pi16_f(be); + bhe = _mm_loadhi_pi16_f(be); + halfble = _mm_srli_pi32(ble, 1); + halfbhe = _mm_srli_pi32(bhe, 1); + + cble = _mm_add_pi32(cble, halfble); + cbhe = _mm_add_pi32(cbhe, halfbhe); + cble = _mm_add_pi32(cble, PD_ONEHALFM1_CJ); + cbhe = _mm_add_pi32(cbhe, PD_ONEHALFM1_CJ); + cble = _mm_srli_pi32(cble, SCALEBITS); + cbhe = _mm_srli_pi32(cbhe, SCALEBITS); + cbe = _mm_packs_pi32(cble, cbhe); + + cbo = _mm_slli_pi16(cbo, BYTE_BIT); + cb = _mm_or_si64(cbe, cbo); + + bglo = _mm_unpacklo_pi16(bo, go); + bgho = _mm_unpackhi_pi16(bo, go); + ylo_bg = _mm_madd_pi16(bglo, PW_F0114_F0250); + yho_bg = _mm_madd_pi16(bgho, PW_F0114_F0250); + crlo = _mm_madd_pi16(bglo, PW_MF008_MF041); + crho = _mm_madd_pi16(bgho, PW_MF008_MF041); + + ylo = _mm_add_pi32(ylo_bg, ylo_rg); + yho = _mm_add_pi32(yho_bg, yho_rg); + ylo = _mm_add_pi32(ylo, PD_ONEHALF); + yho = _mm_add_pi32(yho, PD_ONEHALF); + ylo = _mm_srli_pi32(ylo, SCALEBITS); + yho = _mm_srli_pi32(yho, SCALEBITS); + yo = _mm_packs_pi32(ylo, yho); + + rlo = _mm_loadlo_pi16_f(ro); + rho = _mm_loadhi_pi16_f(ro); + halfrlo = _mm_srli_pi32(rlo, 1); + halfrho = _mm_srli_pi32(rho, 1); + + crlo = _mm_add_pi32(crlo, halfrlo); + crho = _mm_add_pi32(crho, halfrho); + crlo = _mm_add_pi32(crlo, PD_ONEHALFM1_CJ); + crho = _mm_add_pi32(crho, PD_ONEHALFM1_CJ); + crlo = _mm_srli_pi32(crlo, SCALEBITS); + crho = _mm_srli_pi32(crho, SCALEBITS); + cro = _mm_packs_pi32(crlo, crho); + + bgle = _mm_unpacklo_pi16(be, ge); + bghe = _mm_unpackhi_pi16(be, ge); + yle_bg = _mm_madd_pi16(bgle, PW_F0114_F0250); + yhe_bg = _mm_madd_pi16(bghe, PW_F0114_F0250); + crle = _mm_madd_pi16(bgle, PW_MF008_MF041); + crhe = _mm_madd_pi16(bghe, PW_MF008_MF041); + + yle = _mm_add_pi32(yle_bg, yle_rg); + yhe = _mm_add_pi32(yhe_bg, yhe_rg); + yle = _mm_add_pi32(yle, PD_ONEHALF); + yhe = _mm_add_pi32(yhe, PD_ONEHALF); + yle = _mm_srli_pi32(yle, SCALEBITS); + yhe = _mm_srli_pi32(yhe, SCALEBITS); + ye = _mm_packs_pi32(yle, yhe); + + yo = _mm_slli_pi16(yo, BYTE_BIT); + y = _mm_or_si64(ye, yo); + + rle = _mm_loadlo_pi16_f(re); + rhe = _mm_loadhi_pi16_f(re); + halfrle = _mm_srli_pi32(rle, 1); + halfrhe = _mm_srli_pi32(rhe, 1); + + crle = _mm_add_pi32(crle, halfrle); + crhe = _mm_add_pi32(crhe, halfrhe); + crle = _mm_add_pi32(crle, PD_ONEHALFM1_CJ); + crhe = _mm_add_pi32(crhe, PD_ONEHALFM1_CJ); + crle = _mm_srli_pi32(crle, SCALEBITS); + crhe = _mm_srli_pi32(crhe, SCALEBITS); + cre = _mm_packs_pi32(crle, crhe); + + cro = _mm_slli_pi16(cro, BYTE_BIT); + cr = _mm_or_si64(cre, cro); + + _mm_store_si64((__m64 *)&outptr0[0], y); + _mm_store_si64((__m64 *)&outptr1[0], cb); + _mm_store_si64((__m64 *)&outptr2[0], cr); + } + } +} + +#undef mmA +#undef mmB +#undef mmC +#undef mmD +#undef mmE +#undef mmF +#undef mmG +#undef mmH diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jccolor-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jccolor-mmi.c similarity index 100% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jccolor-mmi.c rename to third-party/mozjpeg/mozjpeg/simd/mips64/jccolor-mmi.c diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jcgray-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jcgray-mmi.c new file mode 100644 index 00000000000..9c7b833f2e7 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jcgray-mmi.c @@ -0,0 +1,132 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright (C) 2011, 2014, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhangLixia + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* RGB --> GRAYSCALE CONVERSION */ + +#include "jsimd_mmi.h" + + +#define F_0_114 ((short)7471) /* FIX(0.11400) */ +#define F_0_250 ((short)16384) /* FIX(0.25000) */ +#define F_0_299 ((short)19595) /* FIX(0.29900) */ +#define F_0_587 ((short)38470) /* FIX(0.58700) */ +#define F_0_337 ((short)(F_0_587 - F_0_250)) /* FIX(0.58700) - FIX(0.25000) */ + +enum const_index { + index_PD_ONEHALF, + index_PW_F0299_F0337, + index_PW_F0114_F0250 +}; + +static uint64_t const_value[] = { + _uint64_set_pi32((int)(1 << (SCALEBITS - 1)), (int)(1 << (SCALEBITS - 1))), + _uint64_set_pi16(F_0_337, F_0_299, F_0_337, F_0_299), + _uint64_set_pi16(F_0_250, F_0_114, F_0_250, F_0_114) +}; + +#define get_const_value(index) (*(__m64 *)&const_value[index]) + +#define PD_ONEHALF get_const_value(index_PD_ONEHALF) +#define PW_F0299_F0337 get_const_value(index_PW_F0299_F0337) +#define PW_F0114_F0250 get_const_value(index_PW_F0114_F0250) + + +#include "jcgryext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE + +#define RGB_RED EXT_RGB_RED +#define RGB_GREEN EXT_RGB_GREEN +#define RGB_BLUE EXT_RGB_BLUE +#define RGB_PIXELSIZE EXT_RGB_PIXELSIZE +#define jsimd_rgb_gray_convert_mmi jsimd_extrgb_gray_convert_mmi +#include "jcgryext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_mmi + +#define RGB_RED EXT_RGBX_RED +#define RGB_GREEN EXT_RGBX_GREEN +#define RGB_BLUE EXT_RGBX_BLUE +#define RGB_PIXELSIZE EXT_RGBX_PIXELSIZE +#define jsimd_rgb_gray_convert_mmi jsimd_extrgbx_gray_convert_mmi +#include "jcgryext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_mmi + +#define RGB_RED EXT_BGR_RED +#define RGB_GREEN EXT_BGR_GREEN +#define RGB_BLUE EXT_BGR_BLUE +#define RGB_PIXELSIZE EXT_BGR_PIXELSIZE +#define jsimd_rgb_gray_convert_mmi jsimd_extbgr_gray_convert_mmi +#include "jcgryext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_mmi + +#define RGB_RED EXT_BGRX_RED +#define RGB_GREEN EXT_BGRX_GREEN +#define RGB_BLUE EXT_BGRX_BLUE +#define RGB_PIXELSIZE EXT_BGRX_PIXELSIZE +#define jsimd_rgb_gray_convert_mmi jsimd_extbgrx_gray_convert_mmi +#include "jcgryext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_mmi + +#define RGB_RED EXT_XBGR_RED +#define RGB_GREEN EXT_XBGR_GREEN +#define RGB_BLUE EXT_XBGR_BLUE +#define RGB_PIXELSIZE EXT_XBGR_PIXELSIZE +#define jsimd_rgb_gray_convert_mmi jsimd_extxbgr_gray_convert_mmi +#include "jcgryext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_mmi + +#define RGB_RED EXT_XRGB_RED +#define RGB_GREEN EXT_XRGB_GREEN +#define RGB_BLUE EXT_XRGB_BLUE +#define RGB_PIXELSIZE EXT_XRGB_PIXELSIZE +#define jsimd_rgb_gray_convert_mmi jsimd_extxrgb_gray_convert_mmi +#include "jcgryext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_rgb_gray_convert_mmi diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jcgryext-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jcgryext-mmi.c new file mode 100644 index 00000000000..08a83d6699c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jcgryext-mmi.c @@ -0,0 +1,374 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright 2009 Pierre Ossman for Cendio AB + * Copyright (C) 2014-2015, 2019, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhangLixia + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jcgray-mmi.c */ + + +#if RGB_RED == 0 +#define mmA re +#define mmB ro +#elif RGB_GREEN == 0 +#define mmA ge +#define mmB go +#elif RGB_BLUE == 0 +#define mmA be +#define mmB bo +#else +#define mmA xe +#define mmB xo +#endif + +#if RGB_RED == 1 +#define mmC re +#define mmD ro +#elif RGB_GREEN == 1 +#define mmC ge +#define mmD go +#elif RGB_BLUE == 1 +#define mmC be +#define mmD bo +#else +#define mmC xe +#define mmD xo +#endif + +#if RGB_RED == 2 +#define mmE re +#define mmF ro +#elif RGB_GREEN == 2 +#define mmE ge +#define mmF go +#elif RGB_BLUE == 2 +#define mmE be +#define mmF bo +#else +#define mmE xe +#define mmF xo +#endif + +#if RGB_RED == 3 +#define mmG re +#define mmH ro +#elif RGB_GREEN == 3 +#define mmG ge +#define mmH go +#elif RGB_BLUE == 3 +#define mmG be +#define mmH bo +#else +#define mmG xe +#define mmH xo +#endif + + +void jsimd_rgb_gray_convert_mmi(JDIMENSION image_width, JSAMPARRAY input_buf, + JSAMPIMAGE output_buf, JDIMENSION output_row, + int num_rows) +{ + JSAMPROW inptr, outptr; + int num_cols, col; + __m64 re, ro, ge, go, be, bo, xe; +#if RGB_PIXELSIZE == 4 + __m64 xo; +#endif + __m64 rgle, rghe, rglo, rgho, bgle, bghe, bglo, bgho; + __m64 yle_rg, yhe_rg, yle_bg, yhe_bg, yle, yhe, ye; + __m64 ylo_rg, yho_rg, ylo_bg, yho_bg, ylo, yho, yo, y; + + while (--num_rows >= 0) { + inptr = *input_buf++; + outptr = output_buf[0][output_row]; + output_row++; + + for (num_cols = image_width; num_cols > 0; num_cols -= 8, + outptr += 8) { + +#if RGB_PIXELSIZE == 3 + + if (num_cols < 8) { + col = num_cols * 3; + asm(".set noreorder\r\n" + + "li $8, 1\r\n" + "move $9, %3\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 1f\r\n" + "nop \r\n" + "subu $9, $9, 1\r\n" + "xor $12, $12, $12\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $9\r\n" + "lbu $12, 0($13)\r\n" + + "1: \r\n" + "li $8, 2\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 2f\r\n" + "nop \r\n" + "subu $9, $9, 2\r\n" + "xor $11, $11, $11\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $9\r\n" + "lhu $11, 0($13)\r\n" + "sll $12, $12, 16\r\n" + "or $12, $12, $11\r\n" + + "2: \r\n" + "dmtc1 $12, %0\r\n" + "li $8, 4\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 3f\r\n" + "nop \r\n" + "subu $9, $9, 4\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $9\r\n" + "lwu $14, 0($13)\r\n" + "dmtc1 $14, %1\r\n" + "dsll32 $12, $12, 0\r\n" + "or $12, $12, $14\r\n" + "dmtc1 $12, %0\r\n" + + "3: \r\n" + "li $8, 8\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 4f\r\n" + "nop \r\n" + "mov.s %1, %0\r\n" + "ldc1 %0, 0(%5)\r\n" + "li $9, 8\r\n" + "j 5f\r\n" + "nop \r\n" + + "4: \r\n" + "li $8, 16\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 5f\r\n" + "nop \r\n" + "mov.s %2, %0\r\n" + "ldc1 %0, 0(%5)\r\n" + "ldc1 %1, 8(%5)\r\n" + + "5: \r\n" + "nop \r\n" + ".set reorder\r\n" + + : "=f" (mmA), "=f" (mmG), "=f" (mmF) + : "r" (col), "r" (num_rows), "r" (inptr) + : "$f0", "$f2", "$f4", "$8", "$9", "$10", "$11", "$12", "$13", + "$14", "memory" + ); + } else { + if (!(((long)inptr) & 7)) { + mmA = _mm_load_si64((__m64 *)&inptr[0]); + mmG = _mm_load_si64((__m64 *)&inptr[8]); + mmF = _mm_load_si64((__m64 *)&inptr[16]); + } else { + mmA = _mm_loadu_si64((__m64 *)&inptr[0]); + mmG = _mm_loadu_si64((__m64 *)&inptr[8]); + mmF = _mm_loadu_si64((__m64 *)&inptr[16]); + } + inptr += RGB_PIXELSIZE * 8; + } + mmD = _mm_srli_si64(mmA, 4 * BYTE_BIT); + mmA = _mm_slli_si64(mmA, 4 * BYTE_BIT); + + mmA = _mm_unpackhi_pi8(mmA, mmG); + mmG = _mm_slli_si64(mmG, 4 * BYTE_BIT); + + mmD = _mm_unpacklo_pi8(mmD, mmF); + mmG = _mm_unpackhi_pi8(mmG, mmF); + + mmE = _mm_srli_si64(mmA, 4 * BYTE_BIT); + mmA = _mm_slli_si64(mmA, 4 * BYTE_BIT); + + mmA = _mm_unpackhi_pi8(mmA, mmD); + mmD = _mm_slli_si64(mmD, 4 * BYTE_BIT); + + mmE = _mm_unpacklo_pi8(mmE, mmG); + mmD = _mm_unpackhi_pi8(mmD, mmG); + mmC = _mm_loadhi_pi8_f(mmA); + mmA = _mm_loadlo_pi8_f(mmA); + + mmB = _mm_loadhi_pi8_f(mmE); + mmE = _mm_loadlo_pi8_f(mmE); + + mmF = _mm_loadhi_pi8_f(mmD); + mmD = _mm_loadlo_pi8_f(mmD); + +#else /* RGB_PIXELSIZE == 4 */ + + if (num_cols < 8) { + col = num_cols; + asm(".set noreorder\r\n" + + "li $8, 1\r\n" + "move $9, %4\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 1f\r\n" + "nop \r\n" + "subu $9, $9, 1\r\n" + PTR_SLL "$11, $9, 2\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $11\r\n" + "lwc1 %0, 0($13)\r\n" + + "1: \r\n" + "li $8, 2\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 2f\r\n" + "nop \r\n" + "subu $9, $9, 2\r\n" + PTR_SLL "$11, $9, 2\r\n" + "move $13, %5\r\n" + PTR_ADDU "$13, $13, $11\r\n" + "mov.s %1, %0\r\n" + "ldc1 %0, 0($13)\r\n" + + "2: \r\n" + "li $8, 4\r\n" + "and $10, $9, $8\r\n" + "beqz $10, 3f\r\n" + "nop \r\n" + "mov.s %2, %0\r\n" + "mov.s %3, %1\r\n" + "ldc1 %0, 0(%5)\r\n" + "ldc1 %1, 8(%5)\r\n" + + "3: \r\n" + "nop \r\n" + ".set reorder\r\n" + + : "=f" (mmA), "=f" (mmF), "=f" (mmD), "=f" (mmC) + : "r" (col), "r" (inptr) + : "$f0", "$f2", "$8", "$9", "$10", "$11", "$13", "memory" + ); + } else { + if (!(((long)inptr) & 7)) { + mmA = _mm_load_si64((__m64 *)&inptr[0]); + mmF = _mm_load_si64((__m64 *)&inptr[8]); + mmD = _mm_load_si64((__m64 *)&inptr[16]); + mmC = _mm_load_si64((__m64 *)&inptr[24]); + } else { + mmA = _mm_loadu_si64((__m64 *)&inptr[0]); + mmF = _mm_loadu_si64((__m64 *)&inptr[8]); + mmD = _mm_loadu_si64((__m64 *)&inptr[16]); + mmC = _mm_loadu_si64((__m64 *)&inptr[24]); + } + inptr += RGB_PIXELSIZE * 8; + } + mmB = _mm_unpackhi_pi8(mmA, mmF); + mmA = _mm_unpacklo_pi8(mmA, mmF); + + mmG = _mm_unpackhi_pi8(mmD, mmC); + mmD = _mm_unpacklo_pi8(mmD, mmC); + + mmE = _mm_unpackhi_pi16(mmA, mmD); + mmA = _mm_unpacklo_pi16(mmA, mmD); + + mmH = _mm_unpackhi_pi16(mmB, mmG); + mmB = _mm_unpacklo_pi16(mmB, mmG); + + mmC = _mm_loadhi_pi8_f(mmA); + mmA = _mm_loadlo_pi8_f(mmA); + + mmD = _mm_loadhi_pi8_f(mmB); + mmB = _mm_loadlo_pi8_f(mmB); + + mmG = _mm_loadhi_pi8_f(mmE); + mmE = _mm_loadlo_pi8_f(mmE); + + mmF = _mm_unpacklo_pi8(mmH, mmH); + mmH = _mm_unpackhi_pi8(mmH, mmH); + mmF = _mm_srli_pi16(mmF, BYTE_BIT); + mmH = _mm_srli_pi16(mmH, BYTE_BIT); + +#endif + + /* re=(R0 R2 R4 R6), ge=(G0 G2 G4 G6), be=(B0 B2 B4 B6) + * ro=(R1 R3 R5 R7), go=(G1 G3 G5 G7), bo=(B1 B3 B5 B7) + * + * (Original) + * Y = 0.29900 * R + 0.58700 * G + 0.11400 * B + * + * (This implementation) + * Y = 0.29900 * R + 0.33700 * G + 0.11400 * B + 0.25000 * G + */ + + rglo = _mm_unpacklo_pi16(ro, go); + rgho = _mm_unpackhi_pi16(ro, go); + ylo_rg = _mm_madd_pi16(rglo, PW_F0299_F0337); + yho_rg = _mm_madd_pi16(rgho, PW_F0299_F0337); + + rgle = _mm_unpacklo_pi16(re, ge); + rghe = _mm_unpackhi_pi16(re, ge); + yle_rg = _mm_madd_pi16(rgle, PW_F0299_F0337); + yhe_rg = _mm_madd_pi16(rghe, PW_F0299_F0337); + + bglo = _mm_unpacklo_pi16(bo, go); + bgho = _mm_unpackhi_pi16(bo, go); + ylo_bg = _mm_madd_pi16(bglo, PW_F0114_F0250); + yho_bg = _mm_madd_pi16(bgho, PW_F0114_F0250); + + ylo = _mm_add_pi32(ylo_bg, ylo_rg); + yho = _mm_add_pi32(yho_bg, yho_rg); + ylo = _mm_add_pi32(ylo, PD_ONEHALF); + yho = _mm_add_pi32(yho, PD_ONEHALF); + ylo = _mm_srli_pi32(ylo, SCALEBITS); + yho = _mm_srli_pi32(yho, SCALEBITS); + yo = _mm_packs_pi32(ylo, yho); + + bgle = _mm_unpacklo_pi16(be, ge); + bghe = _mm_unpackhi_pi16(be, ge); + yle_bg = _mm_madd_pi16(bgle, PW_F0114_F0250); + yhe_bg = _mm_madd_pi16(bghe, PW_F0114_F0250); + + yle = _mm_add_pi32(yle_bg, yle_rg); + yhe = _mm_add_pi32(yhe_bg, yhe_rg); + yle = _mm_add_pi32(yle, PD_ONEHALF); + yhe = _mm_add_pi32(yhe, PD_ONEHALF); + yle = _mm_srli_pi32(yle, SCALEBITS); + yhe = _mm_srli_pi32(yhe, SCALEBITS); + ye = _mm_packs_pi32(yle, yhe); + + yo = _mm_slli_pi16(yo, BYTE_BIT); + y = _mm_or_si64(ye, yo); + + _mm_store_si64((__m64 *)&outptr[0], y); + } + } +} + +#undef mmA +#undef mmB +#undef mmC +#undef mmD +#undef mmE +#undef mmF +#undef mmG +#undef mmH diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jcsample-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jcsample-mmi.c similarity index 56% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jcsample-mmi.c rename to third-party/mozjpeg/mozjpeg/simd/mips64/jcsample-mmi.c index 2f2d85196cc..0354dac0879 100644 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jcsample-mmi.c +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jcsample-mmi.c @@ -1,7 +1,7 @@ /* * Loongson MMI optimizations for libjpeg-turbo * - * Copyright (C) 2015, 2018, D. R. Commander. All Rights Reserved. + * Copyright (C) 2015, 2018-2019, D. R. Commander. All Rights Reserved. * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. * All Rights Reserved. * Authors: ZhuChen @@ -39,18 +39,20 @@ void jsimd_h2v2_downsample_mmi(JDIMENSION image_width, int max_v_samp_factor, JDIMENSION width_in_blocks, JSAMPARRAY input_data, JSAMPARRAY output_data) { - int inrow, outrow, outcol, bias; + int inrow, outrow, outcol; JDIMENSION output_cols = width_in_blocks * DCTSIZE; JSAMPROW inptr0, inptr1, outptr; - __m64 mm0, mm1, mm2, mm3, mm4, mm5, mm6 = 0.0, mm7; + __m64 bias, mask = 0.0, thisavg, nextavg, avg; + __m64 this0o, this0e, this0, this0sum, next0o, next0e, next0, next0sum; + __m64 this1o, this1e, this1, this1sum, next1o, next1e, next1, next1sum; expand_right_edge(input_data, max_v_samp_factor, image_width, output_cols * 2); - bias = (1 << 17) + 1; /* 0x00020001 (bias pattern) */ - mm7 = _mm_set1_pi32(bias); /* mm7={1, 2, 1, 2} */ - mm6 = _mm_cmpeq_pi16(mm6, mm6); - mm6 = _mm_srli_pi16(mm6, BYTE_BIT); /* mm6={0xFF 0x00 0xFF 0x00 ..} */ + bias = _mm_set1_pi32((1 << 17) + 1); /* 0x00020001 (32-bit bias pattern) */ + /* bias={1, 2, 1, 2} (16-bit) */ + mask = _mm_cmpeq_pi16(mask, mask); + mask = _mm_srli_pi16(mask, BYTE_BIT); /* {0xFF 0x00 0xFF 0x00 ..} */ for (inrow = 0, outrow = 0; outrow < v_samp_factor; inrow += 2, outrow++) { @@ -62,39 +64,35 @@ void jsimd_h2v2_downsample_mmi(JDIMENSION image_width, int max_v_samp_factor, for (outcol = output_cols; outcol > 0; outcol -= 8, inptr0 += 16, inptr1 += 16, outptr += 8) { - mm0 = _mm_load_si64((__m64 *)&inptr0[0]); - mm1 = _mm_load_si64((__m64 *)&inptr1[0]); - mm2 = _mm_load_si64((__m64 *)&inptr0[8]); - mm3 = _mm_load_si64((__m64 *)&inptr1[8]); + this0 = _mm_load_si64((__m64 *)&inptr0[0]); + this1 = _mm_load_si64((__m64 *)&inptr1[0]); + next0 = _mm_load_si64((__m64 *)&inptr0[8]); + next1 = _mm_load_si64((__m64 *)&inptr1[8]); - mm4 = mm0; - mm5 = mm1; - mm0 = _mm_and_si64(mm0, mm6); - mm4 = _mm_srli_pi16(mm4, BYTE_BIT); - mm1 = _mm_and_si64(mm1, mm6); - mm5 = _mm_srli_pi16(mm5, BYTE_BIT); - mm0 = _mm_add_pi16(mm0, mm4); - mm1 = _mm_add_pi16(mm1, mm5); + this0o = _mm_and_si64(this0, mask); + this0e = _mm_srli_pi16(this0, BYTE_BIT); + this1o = _mm_and_si64(this1, mask); + this1e = _mm_srli_pi16(this1, BYTE_BIT); + this0sum = _mm_add_pi16(this0o, this0e); + this1sum = _mm_add_pi16(this1o, this1e); - mm4 = mm2; - mm5 = mm3; - mm2 = _mm_and_si64(mm2, mm6); - mm4 = _mm_srli_pi16(mm4, BYTE_BIT); - mm3 = _mm_and_si64(mm3, mm6); - mm5 = _mm_srli_pi16(mm5, BYTE_BIT); - mm2 = _mm_add_pi16(mm2, mm4); - mm3 = _mm_add_pi16(mm3, mm5); + next0o = _mm_and_si64(next0, mask); + next0e = _mm_srli_pi16(next0, BYTE_BIT); + next1o = _mm_and_si64(next1, mask); + next1e = _mm_srli_pi16(next1, BYTE_BIT); + next0sum = _mm_add_pi16(next0o, next0e); + next1sum = _mm_add_pi16(next1o, next1e); - mm0 = _mm_add_pi16(mm0, mm1); - mm2 = _mm_add_pi16(mm2, mm3); - mm0 = _mm_add_pi16(mm0, mm7); - mm2 = _mm_add_pi16(mm2, mm7); - mm0 = _mm_srli_pi16(mm0, 2); - mm2 = _mm_srli_pi16(mm2, 2); + thisavg = _mm_add_pi16(this0sum, this1sum); + nextavg = _mm_add_pi16(next0sum, next1sum); + thisavg = _mm_add_pi16(thisavg, bias); + nextavg = _mm_add_pi16(nextavg, bias); + thisavg = _mm_srli_pi16(thisavg, 2); + nextavg = _mm_srli_pi16(nextavg, 2); - mm0 = _mm_packs_pu16(mm0, mm2); + avg = _mm_packs_pu16(thisavg, nextavg); - _mm_store_si64((__m64 *)&outptr[0], mm0); + _mm_store_si64((__m64 *)&outptr[0], avg); } } } diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jcsample.h b/third-party/mozjpeg/mozjpeg/simd/mips64/jcsample.h similarity index 90% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jcsample.h rename to third-party/mozjpeg/mozjpeg/simd/mips64/jcsample.h index 2ac48167fc2..bd07fcc4ed4 100644 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jcsample.h +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jcsample.h @@ -20,7 +20,7 @@ expand_right_edge(JSAMPARRAY image_data, int num_rows, JDIMENSION input_cols, if (numcols > 0) { for (row = 0; row < num_rows; row++) { ptr = image_data[row] + input_cols; - pixval = ptr[-1]; /* don't need GETJSAMPLE() here */ + pixval = ptr[-1]; for (count = numcols; count > 0; count--) *ptr++ = pixval; } diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jdcolext-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jdcolext-mmi.c new file mode 100644 index 00000000000..3b5b2f20307 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jdcolext-mmi.c @@ -0,0 +1,415 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright 2009 Pierre Ossman for Cendio AB + * Copyright (C) 2015, 2019, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhuChen + * SunZhangzhi + * CaiWanwei + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jdcolor-mmi.c */ + + +#if RGB_RED == 0 +#define mmA re +#define mmB ro +#elif RGB_GREEN == 0 +#define mmA ge +#define mmB go +#elif RGB_BLUE == 0 +#define mmA be +#define mmB bo +#else +#define mmA xe +#define mmB xo +#endif + +#if RGB_RED == 1 +#define mmC re +#define mmD ro +#elif RGB_GREEN == 1 +#define mmC ge +#define mmD go +#elif RGB_BLUE == 1 +#define mmC be +#define mmD bo +#else +#define mmC xe +#define mmD xo +#endif + +#if RGB_RED == 2 +#define mmE re +#define mmF ro +#elif RGB_GREEN == 2 +#define mmE ge +#define mmF go +#elif RGB_BLUE == 2 +#define mmE be +#define mmF bo +#else +#define mmE xe +#define mmF xo +#endif + +#if RGB_RED == 3 +#define mmG re +#define mmH ro +#elif RGB_GREEN == 3 +#define mmG ge +#define mmH go +#elif RGB_BLUE == 3 +#define mmG be +#define mmH bo +#else +#define mmG xe +#define mmH xo +#endif + + +void jsimd_ycc_rgb_convert_mmi(JDIMENSION out_width, JSAMPIMAGE input_buf, + JDIMENSION input_row, JSAMPARRAY output_buf, + int num_rows) +{ + JSAMPROW outptr, inptr0, inptr1, inptr2; + int num_cols, col; + __m64 ye, yo, y, cbe, cbe2, cbo, cbo2, cb, cre, cre2, cro, cro2, cr; + __m64 re, ro, gle, ghe, ge, glo, gho, go, be, bo, xe = 0.0, xo = 0.0; + __m64 decenter, mask; + + while (--num_rows >= 0) { + inptr0 = input_buf[0][input_row]; + inptr1 = input_buf[1][input_row]; + inptr2 = input_buf[2][input_row]; + input_row++; + outptr = *output_buf++; + + for (num_cols = out_width; num_cols > 0; num_cols -= 8, + inptr0 += 8, inptr1 += 8, inptr2 += 8) { + + cb = _mm_load_si64((__m64 *)inptr1); + cr = _mm_load_si64((__m64 *)inptr2); + y = _mm_load_si64((__m64 *)inptr0); + + mask = decenter = 0.0; + mask = _mm_cmpeq_pi16(mask, mask); + decenter = _mm_cmpeq_pi16(decenter, decenter); + mask = _mm_srli_pi16(mask, BYTE_BIT); /* {0xFF 0x00 0xFF 0x00 ..} */ + decenter = _mm_slli_pi16(decenter, 7); /* {0xFF80 0xFF80 0xFF80 0xFF80} */ + + cbe = _mm_and_si64(mask, cb); /* Cb(0246) */ + cbo = _mm_srli_pi16(cb, BYTE_BIT); /* Cb(1357) */ + cre = _mm_and_si64(mask, cr); /* Cr(0246) */ + cro = _mm_srli_pi16(cr, BYTE_BIT); /* Cr(1357) */ + cbe = _mm_add_pi16(cbe, decenter); + cbo = _mm_add_pi16(cbo, decenter); + cre = _mm_add_pi16(cre, decenter); + cro = _mm_add_pi16(cro, decenter); + + /* (Original) + * R = Y + 1.40200 * Cr + * G = Y - 0.34414 * Cb - 0.71414 * Cr + * B = Y + 1.77200 * Cb + * + * (This implementation) + * R = Y + 0.40200 * Cr + Cr + * G = Y - 0.34414 * Cb + 0.28586 * Cr - Cr + * B = Y - 0.22800 * Cb + Cb + Cb + */ + + cbe2 = _mm_add_pi16(cbe, cbe); /* 2*CbE */ + cbo2 = _mm_add_pi16(cbo, cbo); /* 2*CbO */ + cre2 = _mm_add_pi16(cre, cre); /* 2*CrE */ + cro2 = _mm_add_pi16(cro, cro); /* 2*CrO */ + + be = _mm_mulhi_pi16(cbe2, PW_MF0228); /* (2*CbE * -FIX(0.22800) */ + bo = _mm_mulhi_pi16(cbo2, PW_MF0228); /* (2*CbO * -FIX(0.22800) */ + re = _mm_mulhi_pi16(cre2, PW_F0402); /* (2*CrE * FIX(0.40200)) */ + ro = _mm_mulhi_pi16(cro2, PW_F0402); /* (2*CrO * FIX(0.40200)) */ + + be = _mm_add_pi16(be, PW_ONE); + bo = _mm_add_pi16(bo, PW_ONE); + be = _mm_srai_pi16(be, 1); /* (CbE * -FIX(0.22800)) */ + bo = _mm_srai_pi16(bo, 1); /* (CbO * -FIX(0.22800)) */ + re = _mm_add_pi16(re, PW_ONE); + ro = _mm_add_pi16(ro, PW_ONE); + re = _mm_srai_pi16(re, 1); /* (CrE * FIX(0.40200)) */ + ro = _mm_srai_pi16(ro, 1); /* (CrO * FIX(0.40200)) */ + + be = _mm_add_pi16(be, cbe); + bo = _mm_add_pi16(bo, cbo); + be = _mm_add_pi16(be, cbe); /* (CbE * FIX(1.77200))=(B-Y)E */ + bo = _mm_add_pi16(bo, cbo); /* (CbO * FIX(1.77200))=(B-Y)O */ + re = _mm_add_pi16(re, cre); /* (CrE * FIX(1.40200))=(R-Y)E */ + ro = _mm_add_pi16(ro, cro); /* (CrO * FIX(1.40200))=(R-Y)O */ + + gle = _mm_unpacklo_pi16(cbe, cre); + ghe = _mm_unpackhi_pi16(cbe, cre); + gle = _mm_madd_pi16(gle, PW_MF0344_F0285); + ghe = _mm_madd_pi16(ghe, PW_MF0344_F0285); + glo = _mm_unpacklo_pi16(cbo, cro); + gho = _mm_unpackhi_pi16(cbo, cro); + glo = _mm_madd_pi16(glo, PW_MF0344_F0285); + gho = _mm_madd_pi16(gho, PW_MF0344_F0285); + + gle = _mm_add_pi32(gle, PD_ONEHALF); + ghe = _mm_add_pi32(ghe, PD_ONEHALF); + gle = _mm_srai_pi32(gle, SCALEBITS); + ghe = _mm_srai_pi32(ghe, SCALEBITS); + glo = _mm_add_pi32(glo, PD_ONEHALF); + gho = _mm_add_pi32(gho, PD_ONEHALF); + glo = _mm_srai_pi32(glo, SCALEBITS); + gho = _mm_srai_pi32(gho, SCALEBITS); + + ge = _mm_packs_pi32(gle, ghe); /* CbE*-FIX(0.344)+CrE*FIX(0.285) */ + go = _mm_packs_pi32(glo, gho); /* CbO*-FIX(0.344)+CrO*FIX(0.285) */ + ge = _mm_sub_pi16(ge, cre); /* CbE*-FIX(0.344)+CrE*-FIX(0.714)=(G-Y)E */ + go = _mm_sub_pi16(go, cro); /* CbO*-FIX(0.344)+CrO*-FIX(0.714)=(G-Y)O */ + + ye = _mm_and_si64(mask, y); /* Y(0246) */ + yo = _mm_srli_pi16(y, BYTE_BIT); /* Y(1357) */ + + re = _mm_add_pi16(re, ye); /* ((R-Y)E+YE)=(R0 R2 R4 R6) */ + ro = _mm_add_pi16(ro, yo); /* ((R-Y)O+YO)=(R1 R3 R5 R7) */ + re = _mm_packs_pu16(re, re); /* (R0 R2 R4 R6 ** ** ** **) */ + ro = _mm_packs_pu16(ro, ro); /* (R1 R3 R5 R7 ** ** ** **) */ + + ge = _mm_add_pi16(ge, ye); /* ((G-Y)E+YE)=(G0 G2 G4 G6) */ + go = _mm_add_pi16(go, yo); /* ((G-Y)O+YO)=(G1 G3 G5 G7) */ + ge = _mm_packs_pu16(ge, ge); /* (G0 G2 G4 G6 ** ** ** **) */ + go = _mm_packs_pu16(go, go); /* (G1 G3 G5 G7 ** ** ** **) */ + + be = _mm_add_pi16(be, ye); /* (YE+(B-Y)E)=(B0 B2 B4 B6) */ + bo = _mm_add_pi16(bo, yo); /* (YO+(B-Y)O)=(B1 B3 B5 B7) */ + be = _mm_packs_pu16(be, be); /* (B0 B2 B4 B6 ** ** ** **) */ + bo = _mm_packs_pu16(bo, bo); /* (B1 B3 B5 B7 ** ** ** **) */ + +#if RGB_PIXELSIZE == 3 + + /* mmA=(00 02 04 06 ** ** ** **), mmB=(01 03 05 07 ** ** ** **) */ + /* mmC=(10 12 14 16 ** ** ** **), mmD=(11 13 15 17 ** ** ** **) */ + mmA = _mm_unpacklo_pi8(mmA, mmC); /* (00 10 02 12 04 14 06 16) */ + mmE = _mm_unpacklo_pi8(mmE, mmB); /* (20 01 22 03 24 05 26 07) */ + mmD = _mm_unpacklo_pi8(mmD, mmF); /* (11 21 13 23 15 25 17 27) */ + + mmH = _mm_srli_si64(mmA, 2 * BYTE_BIT); + + mmG = _mm_unpackhi_pi16(mmA, mmE); /* (04 14 24 05 06 16 26 07) */ + mmA = _mm_unpacklo_pi16(mmA, mmE); /* (00 10 20 01 02 12 22 03) */ + + mmE = _mm_srli_si64(mmE, 2 * BYTE_BIT); + mmB = _mm_srli_si64(mmD, 2 * BYTE_BIT); /* (13 23 15 25 17 27 -- --) */ + + mmC = _mm_unpackhi_pi16(mmD, mmH); /* (15 25 06 16 17 27 -- --) */ + mmD = _mm_unpacklo_pi16(mmD, mmH); /* (11 21 02 12 13 23 04 14) */ + + mmF = _mm_unpackhi_pi16(mmE, mmB); /* (26 07 17 27 -- -- -- --) */ + mmE = _mm_unpacklo_pi16(mmE, mmB); /* (22 03 13 23 24 05 15 25) */ + + mmA = _mm_unpacklo_pi32(mmA, mmD); /* (00 10 20 01 11 21 02 12) */ + mmE = _mm_unpacklo_pi32(mmE, mmG); /* (22 03 13 23 04 14 24 05) */ + mmC = _mm_unpacklo_pi32(mmC, mmF); /* (15 25 06 16 26 07 17 27) */ + + if (num_cols >= 8) { + if (!(((long)outptr) & 7)) { + _mm_store_si64((__m64 *)outptr, mmA); + _mm_store_si64((__m64 *)(outptr + 8), mmE); + _mm_store_si64((__m64 *)(outptr + 16), mmC); + } else { + _mm_storeu_si64((__m64 *)outptr, mmA); + _mm_storeu_si64((__m64 *)(outptr + 8), mmE); + _mm_storeu_si64((__m64 *)(outptr + 16), mmC); + } + outptr += RGB_PIXELSIZE * 8; + } else { + col = num_cols * 3; + asm(".set noreorder\r\n" + + "li $8, 16\r\n" + "move $9, %4\r\n" + "mov.s $f4, %1\r\n" + "mov.s $f6, %3\r\n" + "move $10, %5\r\n" + "bltu $9, $8, 1f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "gssdlc1 $f6, 7+8($10)\r\n" + "gssdrc1 $f6, 8($10)\r\n" + "mov.s $f4, %2\r\n" + "subu $9, $9, 16\r\n" + PTR_ADDU "$10, $10, 16\r\n" + "b 2f\r\n" + "nop \r\n" + + "1: \r\n" + "li $8, 8\r\n" /* st8 */ + "bltu $9, $8, 2f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "mov.s $f4, %3\r\n" + "subu $9, $9, 8\r\n" + PTR_ADDU "$10, $10, 8\r\n" + + "2: \r\n" + "li $8, 4\r\n" /* st4 */ + "mfc1 $11, $f4\r\n" + "bltu $9, $8, 3f\r\n" + "nop \r\n" + "swl $11, 3($10)\r\n" + "swr $11, 0($10)\r\n" + "li $8, 32\r\n" + "mtc1 $8, $f6\r\n" + "dsrl $f4, $f4, $f6\r\n" + "mfc1 $11, $f4\r\n" + "subu $9, $9, 4\r\n" + PTR_ADDU "$10, $10, 4\r\n" + + "3: \r\n" + "li $8, 2\r\n" /* st2 */ + "bltu $9, $8, 4f\r\n" + "nop \r\n" + "ush $11, 0($10)\r\n" + "srl $11, 16\r\n" + "subu $9, $9, 2\r\n" + PTR_ADDU "$10, $10, 2\r\n" + + "4: \r\n" + "li $8, 1\r\n" /* st1 */ + "bltu $9, $8, 5f\r\n" + "nop \r\n" + "sb $11, 0($10)\r\n" + + "5: \r\n" + "nop \r\n" /* end */ + : "=m" (*outptr) + : "f" (mmA), "f" (mmC), "f" (mmE), "r" (col), "r" (outptr) + : "$f4", "$f6", "$8", "$9", "$10", "$11", "memory" + ); + } + +#else /* RGB_PIXELSIZE == 4 */ + +#ifdef RGBX_FILLER_0XFF + xe = _mm_cmpeq_pi8(xe, xe); + xo = _mm_cmpeq_pi8(xo, xo); +#else + xe = _mm_xor_si64(xe, xe); + xo = _mm_xor_si64(xo, xo); +#endif + /* mmA=(00 02 04 06 ** ** ** **), mmB=(01 03 05 07 ** ** ** **) */ + /* mmC=(10 12 14 16 ** ** ** **), mmD=(11 13 15 17 ** ** ** **) */ + /* mmE=(20 22 24 26 ** ** ** **), mmF=(21 23 25 27 ** ** ** **) */ + /* mmG=(30 32 34 36 ** ** ** **), mmH=(31 33 35 37 ** ** ** **) */ + + mmA = _mm_unpacklo_pi8(mmA, mmC); /* (00 10 02 12 04 14 06 16) */ + mmE = _mm_unpacklo_pi8(mmE, mmG); /* (20 30 22 32 24 34 26 36) */ + mmB = _mm_unpacklo_pi8(mmB, mmD); /* (01 11 03 13 05 15 07 17) */ + mmF = _mm_unpacklo_pi8(mmF, mmH); /* (21 31 23 33 25 35 27 37) */ + + mmC = _mm_unpackhi_pi16(mmA, mmE); /* (04 14 24 34 06 16 26 36) */ + mmA = _mm_unpacklo_pi16(mmA, mmE); /* (00 10 20 30 02 12 22 32) */ + mmG = _mm_unpackhi_pi16(mmB, mmF); /* (05 15 25 35 07 17 27 37) */ + mmB = _mm_unpacklo_pi16(mmB, mmF); /* (01 11 21 31 03 13 23 33) */ + + mmD = _mm_unpackhi_pi32(mmA, mmB); /* (02 12 22 32 03 13 23 33) */ + mmA = _mm_unpacklo_pi32(mmA, mmB); /* (00 10 20 30 01 11 21 31) */ + mmH = _mm_unpackhi_pi32(mmC, mmG); /* (06 16 26 36 07 17 27 37) */ + mmC = _mm_unpacklo_pi32(mmC, mmG); /* (04 14 24 34 05 15 25 35) */ + + if (num_cols >= 8) { + if (!(((long)outptr) & 7)) { + _mm_store_si64((__m64 *)outptr, mmA); + _mm_store_si64((__m64 *)(outptr + 8), mmD); + _mm_store_si64((__m64 *)(outptr + 16), mmC); + _mm_store_si64((__m64 *)(outptr + 24), mmH); + } else { + _mm_storeu_si64((__m64 *)outptr, mmA); + _mm_storeu_si64((__m64 *)(outptr + 8), mmD); + _mm_storeu_si64((__m64 *)(outptr + 16), mmC); + _mm_storeu_si64((__m64 *)(outptr + 24), mmH); + } + outptr += RGB_PIXELSIZE * 8; + } else { + col = num_cols; + asm(".set noreorder\r\n" /* st16 */ + + "li $8, 4\r\n" + "move $9, %6\r\n" + "move $10, %7\r\n" + "mov.s $f4, %2\r\n" + "mov.s $f6, %4\r\n" + "bltu $9, $8, 1f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "gssdlc1 $f6, 7+8($10)\r\n" + "gssdrc1 $f6, 8($10)\r\n" + "mov.s $f4, %3\r\n" + "mov.s $f6, %5\r\n" + "subu $9, $9, 4\r\n" + PTR_ADDU "$10, $10, 16\r\n" + + "1: \r\n" + "li $8, 2\r\n" /* st8 */ + "bltu $9, $8, 2f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "mov.s $f4, $f6\r\n" + "subu $9, $9, 2\r\n" + PTR_ADDU "$10, $10, 8\r\n" + + "2: \r\n" + "li $8, 1\r\n" /* st4 */ + "bltu $9, $8, 3f\r\n" + "nop \r\n" + "gsswlc1 $f4, 3($10)\r\n" + "gsswrc1 $f4, 0($10)\r\n" + + "3: \r\n" + "li %1, 0\r\n" /* end */ + : "=m" (*outptr), "=r" (col) + : "f" (mmA), "f" (mmC), "f" (mmD), "f" (mmH), "r" (col), + "r" (outptr) + : "$f4", "$f6", "$8", "$9", "$10", "memory" + ); + } + +#endif + + } + } +} + +#undef mmA +#undef mmB +#undef mmC +#undef mmD +#undef mmE +#undef mmF +#undef mmG +#undef mmH diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jdcolor-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jdcolor-mmi.c similarity index 100% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jdcolor-mmi.c rename to third-party/mozjpeg/mozjpeg/simd/mips64/jdcolor-mmi.c diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jdmerge-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jdmerge-mmi.c new file mode 100644 index 00000000000..0a39bd56805 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jdmerge-mmi.c @@ -0,0 +1,149 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright (C) 2011, 2015, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhangLixia + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* YCC --> RGB CONVERSION */ + +#include "jsimd_mmi.h" + + +#define F_0_344 ((short)22554) /* FIX(0.34414) */ +#define F_0_402 ((short)26345) /* FIX(1.40200) - FIX(1) */ +#define F_0_285 ((short)18734) /* FIX(1) - FIX(0.71414) */ +#define F_0_228 ((short)14942) /* FIX(2) - FIX(1.77200) */ + +enum const_index { + index_PW_ONE, + index_PW_F0402, + index_PW_MF0228, + index_PW_MF0344_F0285, + index_PD_ONEHALF +}; + +static uint64_t const_value[] = { + _uint64_set_pi16(1, 1, 1, 1), + _uint64_set_pi16(F_0_402, F_0_402, F_0_402, F_0_402), + _uint64_set_pi16(-F_0_228, -F_0_228, -F_0_228, -F_0_228), + _uint64_set_pi16(F_0_285, -F_0_344, F_0_285, -F_0_344), + _uint64_set_pi32((int)(1 << (SCALEBITS - 1)), (int)(1 << (SCALEBITS - 1))) +}; + +#define PW_ONE get_const_value(index_PW_ONE) +#define PW_F0402 get_const_value(index_PW_F0402) +#define PW_MF0228 get_const_value(index_PW_MF0228) +#define PW_MF0344_F0285 get_const_value(index_PW_MF0344_F0285) +#define PD_ONEHALF get_const_value(index_PD_ONEHALF) + +#define RGBX_FILLER_0XFF 1 + + +#include "jdmrgext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE + +#define RGB_RED EXT_RGB_RED +#define RGB_GREEN EXT_RGB_GREEN +#define RGB_BLUE EXT_RGB_BLUE +#define RGB_PIXELSIZE EXT_RGB_PIXELSIZE +#define jsimd_h2v1_merged_upsample_mmi jsimd_h2v1_extrgb_merged_upsample_mmi +#define jsimd_h2v2_merged_upsample_mmi jsimd_h2v2_extrgb_merged_upsample_mmi +#include "jdmrgext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_mmi +#undef jsimd_h2v2_merged_upsample_mmi + +#define RGB_RED EXT_RGBX_RED +#define RGB_GREEN EXT_RGBX_GREEN +#define RGB_BLUE EXT_RGBX_BLUE +#define RGB_PIXELSIZE EXT_RGBX_PIXELSIZE +#define jsimd_h2v1_merged_upsample_mmi jsimd_h2v1_extrgbx_merged_upsample_mmi +#define jsimd_h2v2_merged_upsample_mmi jsimd_h2v2_extrgbx_merged_upsample_mmi +#include "jdmrgext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_mmi +#undef jsimd_h2v2_merged_upsample_mmi + +#define RGB_RED EXT_BGR_RED +#define RGB_GREEN EXT_BGR_GREEN +#define RGB_BLUE EXT_BGR_BLUE +#define RGB_PIXELSIZE EXT_BGR_PIXELSIZE +#define jsimd_h2v1_merged_upsample_mmi jsimd_h2v1_extbgr_merged_upsample_mmi +#define jsimd_h2v2_merged_upsample_mmi jsimd_h2v2_extbgr_merged_upsample_mmi +#include "jdmrgext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_mmi +#undef jsimd_h2v2_merged_upsample_mmi + +#define RGB_RED EXT_BGRX_RED +#define RGB_GREEN EXT_BGRX_GREEN +#define RGB_BLUE EXT_BGRX_BLUE +#define RGB_PIXELSIZE EXT_BGRX_PIXELSIZE +#define jsimd_h2v1_merged_upsample_mmi jsimd_h2v1_extbgrx_merged_upsample_mmi +#define jsimd_h2v2_merged_upsample_mmi jsimd_h2v2_extbgrx_merged_upsample_mmi +#include "jdmrgext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_mmi +#undef jsimd_h2v2_merged_upsample_mmi + +#define RGB_RED EXT_XBGR_RED +#define RGB_GREEN EXT_XBGR_GREEN +#define RGB_BLUE EXT_XBGR_BLUE +#define RGB_PIXELSIZE EXT_XBGR_PIXELSIZE +#define jsimd_h2v1_merged_upsample_mmi jsimd_h2v1_extxbgr_merged_upsample_mmi +#define jsimd_h2v2_merged_upsample_mmi jsimd_h2v2_extxbgr_merged_upsample_mmi +#include "jdmrgext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_mmi +#undef jsimd_h2v2_merged_upsample_mmi + +#define RGB_RED EXT_XRGB_RED +#define RGB_GREEN EXT_XRGB_GREEN +#define RGB_BLUE EXT_XRGB_BLUE +#define RGB_PIXELSIZE EXT_XRGB_PIXELSIZE +#define jsimd_h2v1_merged_upsample_mmi jsimd_h2v1_extxrgb_merged_upsample_mmi +#define jsimd_h2v2_merged_upsample_mmi jsimd_h2v2_extxrgb_merged_upsample_mmi +#include "jdmrgext-mmi.c" +#undef RGB_RED +#undef RGB_GREEN +#undef RGB_BLUE +#undef RGB_PIXELSIZE +#undef jsimd_h2v1_merged_upsample_mmi +#undef jsimd_h2v2_merged_upsample_mmi diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jdmrgext-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jdmrgext-mmi.c new file mode 100644 index 00000000000..be09ff2a659 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jdmrgext-mmi.c @@ -0,0 +1,615 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright 2009 Pierre Ossman for Cendio AB + * Copyright (C) 2015, 2019, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhangLixia + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* This file is included by jdmerge-mmi.c */ + + +#if RGB_RED == 0 +#define mmA re +#define mmB ro +#elif RGB_GREEN == 0 +#define mmA ge +#define mmB go +#elif RGB_BLUE == 0 +#define mmA be +#define mmB bo +#else +#define mmA xe +#define mmB xo +#endif + +#if RGB_RED == 1 +#define mmC re +#define mmD ro +#elif RGB_GREEN == 1 +#define mmC ge +#define mmD go +#elif RGB_BLUE == 1 +#define mmC be +#define mmD bo +#else +#define mmC xe +#define mmD xo +#endif + +#if RGB_RED == 2 +#define mmE re +#define mmF ro +#elif RGB_GREEN == 2 +#define mmE ge +#define mmF go +#elif RGB_BLUE == 2 +#define mmE be +#define mmF bo +#else +#define mmE xe +#define mmF xo +#endif + +#if RGB_RED == 3 +#define mmG re +#define mmH ro +#elif RGB_GREEN == 3 +#define mmG ge +#define mmH go +#elif RGB_BLUE == 3 +#define mmG be +#define mmH bo +#else +#define mmG xe +#define mmH xo +#endif + + +void jsimd_h2v1_merged_upsample_mmi(JDIMENSION output_width, + JSAMPIMAGE input_buf, + JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf) +{ + JSAMPROW outptr, inptr0, inptr1, inptr2; + int num_cols, col; + __m64 ythise, ythiso, ythis, ynexte, ynexto, ynext, yl, y; + __m64 cbl, cbl2, cbh, cbh2, cb, crl, crl2, crh, crh2, cr; + __m64 rle, rlo, rl, rhe, rho, rh, re, ro; + __m64 ga, gb, gle, glo, gl, gc, gd, ghe, gho, gh, ge, go; + __m64 ble, blo, bl, bhe, bho, bh, be, bo, xe = 0.0, xo = 0.0; + __m64 decenter, mask, zero = 0.0; +#if RGB_PIXELSIZE == 4 + __m64 mm8, mm9; +#endif + + inptr0 = input_buf[0][in_row_group_ctr]; + inptr1 = input_buf[1][in_row_group_ctr]; + inptr2 = input_buf[2][in_row_group_ctr]; + outptr = output_buf[0]; + + for (num_cols = output_width >> 1; num_cols > 0; num_cols -= 8, + inptr0 += 16, inptr1 += 8, inptr2 += 8) { + + cb = _mm_load_si64((__m64 *)inptr1); + cr = _mm_load_si64((__m64 *)inptr2); + ythis = _mm_load_si64((__m64 *)inptr0); + ynext = _mm_load_si64((__m64 *)inptr0 + 1); + + mask = decenter = 0.0; + mask = _mm_cmpeq_pi16(mask, mask); + decenter = _mm_cmpeq_pi16(decenter, decenter); + mask = _mm_srli_pi16(mask, BYTE_BIT); /* {0xFF 0x00 0xFF 0x00 ..} */ + decenter = _mm_slli_pi16(decenter, 7); /* {0xFF80 0xFF80 0xFF80 0xFF80} */ + + cbl = _mm_unpacklo_pi8(cb, zero); /* Cb(0123) */ + cbh = _mm_unpackhi_pi8(cb, zero); /* Cb(4567) */ + crl = _mm_unpacklo_pi8(cr, zero); /* Cr(0123) */ + crh = _mm_unpackhi_pi8(cr, zero); /* Cr(4567) */ + cbl = _mm_add_pi16(cbl, decenter); + cbh = _mm_add_pi16(cbh, decenter); + crl = _mm_add_pi16(crl, decenter); + crh = _mm_add_pi16(crh, decenter); + + /* (Original) + * R = Y + 1.40200 * Cr + * G = Y - 0.34414 * Cb - 0.71414 * Cr + * B = Y + 1.77200 * Cb + * + * (This implementation) + * R = Y + 0.40200 * Cr + Cr + * G = Y - 0.34414 * Cb + 0.28586 * Cr - Cr + * B = Y - 0.22800 * Cb + Cb + Cb + */ + + cbl2 = _mm_add_pi16(cbl, cbl); /* 2*CbL */ + cbh2 = _mm_add_pi16(cbh, cbh); /* 2*CbH */ + crl2 = _mm_add_pi16(crl, crl); /* 2*CrL */ + crh2 = _mm_add_pi16(crh, crh); /* 2*CrH */ + + bl = _mm_mulhi_pi16(cbl2, PW_MF0228); /* (2*CbL * -FIX(0.22800) */ + bh = _mm_mulhi_pi16(cbh2, PW_MF0228); /* (2*CbH * -FIX(0.22800) */ + rl = _mm_mulhi_pi16(crl2, PW_F0402); /* (2*CrL * FIX(0.40200)) */ + rh = _mm_mulhi_pi16(crh2, PW_F0402); /* (2*CrH * FIX(0.40200)) */ + + bl = _mm_add_pi16(bl, PW_ONE); + bh = _mm_add_pi16(bh, PW_ONE); + bl = _mm_srai_pi16(bl, 1); /* (CbL * -FIX(0.22800)) */ + bh = _mm_srai_pi16(bh, 1); /* (CbH * -FIX(0.22800)) */ + rl = _mm_add_pi16(rl, PW_ONE); + rh = _mm_add_pi16(rh, PW_ONE); + rl = _mm_srai_pi16(rl, 1); /* (CrL * FIX(0.40200)) */ + rh = _mm_srai_pi16(rh, 1); /* (CrH * FIX(0.40200)) */ + + bl = _mm_add_pi16(bl, cbl); + bh = _mm_add_pi16(bh, cbh); + bl = _mm_add_pi16(bl, cbl); /* (CbL * FIX(1.77200))=(B-Y)L */ + bh = _mm_add_pi16(bh, cbh); /* (CbH * FIX(1.77200))=(B-Y)H */ + rl = _mm_add_pi16(rl, crl); /* (CrL * FIX(1.40200))=(R-Y)L */ + rh = _mm_add_pi16(rh, crh); /* (CrH * FIX(1.40200))=(R-Y)H */ + + ga = _mm_unpacklo_pi16(cbl, crl); + gb = _mm_unpackhi_pi16(cbl, crl); + ga = _mm_madd_pi16(ga, PW_MF0344_F0285); + gb = _mm_madd_pi16(gb, PW_MF0344_F0285); + gc = _mm_unpacklo_pi16(cbh, crh); + gd = _mm_unpackhi_pi16(cbh, crh); + gc = _mm_madd_pi16(gc, PW_MF0344_F0285); + gd = _mm_madd_pi16(gd, PW_MF0344_F0285); + + ga = _mm_add_pi32(ga, PD_ONEHALF); + gb = _mm_add_pi32(gb, PD_ONEHALF); + ga = _mm_srai_pi32(ga, SCALEBITS); + gb = _mm_srai_pi32(gb, SCALEBITS); + gc = _mm_add_pi32(gc, PD_ONEHALF); + gd = _mm_add_pi32(gd, PD_ONEHALF); + gc = _mm_srai_pi32(gc, SCALEBITS); + gd = _mm_srai_pi32(gd, SCALEBITS); + + gl = _mm_packs_pi32(ga, gb); /* CbL*-FIX(0.344)+CrL*FIX(0.285) */ + gh = _mm_packs_pi32(gc, gd); /* CbH*-FIX(0.344)+CrH*FIX(0.285) */ + gl = _mm_sub_pi16(gl, crl); /* CbL*-FIX(0.344)+CrL*-FIX(0.714)=(G-Y)L */ + gh = _mm_sub_pi16(gh, crh); /* CbH*-FIX(0.344)+CrH*-FIX(0.714)=(G-Y)H */ + + ythise = _mm_and_si64(mask, ythis); /* Y(0246) */ + ythiso = _mm_srli_pi16(ythis, BYTE_BIT); /* Y(1357) */ + ynexte = _mm_and_si64(mask, ynext); /* Y(8ACE) */ + ynexto = _mm_srli_pi16(ynext, BYTE_BIT); /* Y(9BDF) */ + + rle = _mm_add_pi16(rl, ythise); /* (R0 R2 R4 R6) */ + rlo = _mm_add_pi16(rl, ythiso); /* (R1 R3 R5 R7) */ + rhe = _mm_add_pi16(rh, ynexte); /* (R8 RA RC RE) */ + rho = _mm_add_pi16(rh, ynexto); /* (R9 RB RD RF) */ + re = _mm_packs_pu16(rle, rhe); /* (R0 R2 R4 R6 R8 RA RC RE) */ + ro = _mm_packs_pu16(rlo, rho); /* (R1 R3 R5 R7 R9 RB RD RF) */ + + gle = _mm_add_pi16(gl, ythise); /* (G0 G2 G4 G6) */ + glo = _mm_add_pi16(gl, ythiso); /* (G1 G3 G5 G7) */ + ghe = _mm_add_pi16(gh, ynexte); /* (G8 GA GC GE) */ + gho = _mm_add_pi16(gh, ynexto); /* (G9 GB GD GF) */ + ge = _mm_packs_pu16(gle, ghe); /* (G0 G2 G4 G6 G8 GA GC GE) */ + go = _mm_packs_pu16(glo, gho); /* (G1 G3 G5 G7 G9 GB GD GF) */ + + ble = _mm_add_pi16(bl, ythise); /* (B0 B2 B4 B6) */ + blo = _mm_add_pi16(bl, ythiso); /* (B1 B3 B5 B7) */ + bhe = _mm_add_pi16(bh, ynexte); /* (B8 BA BC BE) */ + bho = _mm_add_pi16(bh, ynexto); /* (B9 BB BD BF) */ + be = _mm_packs_pu16(ble, bhe); /* (B0 B2 B4 B6 B8 BA BC BE) */ + bo = _mm_packs_pu16(blo, bho); /* (B1 B3 B5 B7 B9 BB BD BF) */ + +#if RGB_PIXELSIZE == 3 + + /* mmA=(00 02 04 06 08 0A 0C 0E), mmB=(01 03 05 07 09 0B 0D 0F) */ + /* mmC=(10 12 14 16 18 1A 1C 1E), mmD=(11 13 15 17 19 1B 1D 1F) */ + /* mmE=(20 22 24 26 28 2A 2C 2E), mmF=(21 23 25 27 29 2B 2D 2F) */ + mmG = _mm_unpacklo_pi8(mmA, mmC); /* (00 10 02 12 04 14 06 16) */ + mmA = _mm_unpackhi_pi8(mmA, mmC); /* (08 18 0A 1A 0C 1C 0E 1E) */ + mmH = _mm_unpacklo_pi8(mmE, mmB); /* (20 01 22 03 24 05 26 07) */ + mmE = _mm_unpackhi_pi8(mmE, mmB); /* (28 09 2A 0B 2C 0D 2E 0F) */ + mmC = _mm_unpacklo_pi8(mmD, mmF); /* (11 21 13 23 15 25 17 27) */ + mmD = _mm_unpackhi_pi8(mmD, mmF); /* (19 29 1B 2B 1D 2D 1F 2F) */ + + mmB = _mm_unpacklo_pi16(mmG, mmA); /* (00 10 08 18 02 12 0A 1A) */ + mmA = _mm_unpackhi_pi16(mmG, mmA); /* (04 14 0C 1C 06 16 0E 1E) */ + mmF = _mm_unpacklo_pi16(mmH, mmE); /* (20 01 28 09 22 03 2A 0B) */ + mmE = _mm_unpackhi_pi16(mmH, mmE); /* (24 05 2C 0D 26 07 2E 0F) */ + mmH = _mm_unpacklo_pi16(mmC, mmD); /* (11 21 19 29 13 23 1B 2B) */ + mmG = _mm_unpackhi_pi16(mmC, mmD); /* (15 25 1D 2D 17 27 1F 2F) */ + + mmC = _mm_unpacklo_pi16(mmB, mmF); /* (00 10 20 01 08 18 28 09) */ + mmB = _mm_srli_si64(mmB, 4 * BYTE_BIT); + mmB = _mm_unpacklo_pi16(mmH, mmB); /* (11 21 02 12 19 29 0A 1A) */ + mmD = _mm_unpackhi_pi16(mmF, mmH); /* (22 03 13 23 2A 0B 1B 2B) */ + mmF = _mm_unpacklo_pi16(mmA, mmE); /* (04 14 24 05 0C 1C 2C 0D) */ + mmA = _mm_srli_si64(mmA, 4 * BYTE_BIT); + mmH = _mm_unpacklo_pi16(mmG, mmA); /* (15 25 06 16 1D 2D 0E 1E) */ + mmG = _mm_unpackhi_pi16(mmE, mmG); /* (26 07 17 27 2E 0F 1F 2F) */ + + mmA = _mm_unpacklo_pi32(mmC, mmB); /* (00 10 20 01 11 21 02 12) */ + mmE = _mm_unpackhi_pi32(mmC, mmB); /* (08 18 28 09 19 29 0A 1A) */ + mmB = _mm_unpacklo_pi32(mmD, mmF); /* (22 03 13 23 04 14 24 05) */ + mmF = _mm_unpackhi_pi32(mmD, mmF); /* (2A 0B 1B 2B 0C 1C 2C 0D) */ + mmC = _mm_unpacklo_pi32(mmH, mmG); /* (15 25 06 16 26 07 17 27) */ + mmG = _mm_unpackhi_pi32(mmH, mmG); /* (1D 2D 0E 1E 2E 0F 1F 2F) */ + + if (num_cols >= 8) { + if (!(((long)outptr) & 7)) { + _mm_store_si64((__m64 *)outptr, mmA); + _mm_store_si64((__m64 *)(outptr + 8), mmB); + _mm_store_si64((__m64 *)(outptr + 16), mmC); + _mm_store_si64((__m64 *)(outptr + 24), mmE); + _mm_store_si64((__m64 *)(outptr + 32), mmF); + _mm_store_si64((__m64 *)(outptr + 40), mmG); + } else { + _mm_storeu_si64((__m64 *)outptr, mmA); + _mm_storeu_si64((__m64 *)(outptr + 8), mmB); + _mm_storeu_si64((__m64 *)(outptr + 16), mmC); + _mm_storeu_si64((__m64 *)(outptr + 24), mmE); + _mm_storeu_si64((__m64 *)(outptr + 32), mmF); + _mm_storeu_si64((__m64 *)(outptr + 40), mmG); + } + outptr += RGB_PIXELSIZE * 16; + } else { + if (output_width & 1) + col = num_cols * 6 + 3; + else + col = num_cols * 6; + + asm(".set noreorder\r\n" /* st24 */ + + "li $8, 24\r\n" + "move $9, %7\r\n" + "mov.s $f4, %1\r\n" + "mov.s $f6, %2\r\n" + "mov.s $f8, %3\r\n" + "move $10, %8\r\n" + "bltu $9, $8, 1f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "gssdlc1 $f6, 7+8($10)\r\n" + "gssdrc1 $f6, 8($10)\r\n" + "gssdlc1 $f8, 7+16($10)\r\n" + "gssdrc1 $f8, 16($10)\r\n" + "mov.s $f4, %4\r\n" + "mov.s $f6, %5\r\n" + "mov.s $f8, %6\r\n" + "subu $9, $9, 24\r\n" + PTR_ADDU "$10, $10, 24\r\n" + + "1: \r\n" + "li $8, 16\r\n" /* st16 */ + "bltu $9, $8, 2f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "gssdlc1 $f6, 7+8($10)\r\n" + "gssdrc1 $f6, 8($10)\r\n" + "mov.s $f4, $f8\r\n" + "subu $9, $9, 16\r\n" + PTR_ADDU "$10, $10, 16\r\n" + + "2: \r\n" + "li $8, 8\r\n" /* st8 */ + "bltu $9, $8, 3f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "mov.s $f4, $f6\r\n" + "subu $9, $9, 8\r\n" + PTR_ADDU "$10, $10, 8\r\n" + + "3: \r\n" + "li $8, 4\r\n" /* st4 */ + "mfc1 $11, $f4\r\n" + "bltu $9, $8, 4f\r\n" + "nop \r\n" + "swl $11, 3($10)\r\n" + "swr $11, 0($10)\r\n" + "li $8, 32\r\n" + "mtc1 $8, $f6\r\n" + "dsrl $f4, $f4, $f6\r\n" + "mfc1 $11, $f4\r\n" + "subu $9, $9, 4\r\n" + PTR_ADDU "$10, $10, 4\r\n" + + "4: \r\n" + "li $8, 2\r\n" /* st2 */ + "bltu $9, $8, 5f\r\n" + "nop \r\n" + "ush $11, 0($10)\r\n" + "srl $11, 16\r\n" + "subu $9, $9, 2\r\n" + PTR_ADDU "$10, $10, 2\r\n" + + "5: \r\n" + "li $8, 1\r\n" /* st1 */ + "bltu $9, $8, 6f\r\n" + "nop \r\n" + "sb $11, 0($10)\r\n" + + "6: \r\n" + "nop \r\n" /* end */ + : "=m" (*outptr) + : "f" (mmA), "f" (mmB), "f" (mmC), "f" (mmE), "f" (mmF), + "f" (mmG), "r" (col), "r" (outptr) + : "$f4", "$f6", "$f8", "$8", "$9", "$10", "$11", "memory" + ); + } + +#else /* RGB_PIXELSIZE == 4 */ + +#ifdef RGBX_FILLER_0XFF + xe = _mm_cmpeq_pi8(xe, xe); + xo = _mm_cmpeq_pi8(xo, xo); +#else + xe = _mm_xor_si64(xe, xe); + xo = _mm_xor_si64(xo, xo); +#endif + /* mmA=(00 02 04 06 08 0A 0C 0E), mmB=(01 03 05 07 09 0B 0D 0F) */ + /* mmC=(10 12 14 16 18 1A 1C 1E), mmD=(11 13 15 17 19 1B 1D 1F) */ + /* mmE=(20 22 24 26 28 2A 2C 2E), mmF=(21 23 25 27 29 2B 2D 2F) */ + /* mmG=(30 32 34 36 38 3A 3C 3E), mmH=(31 33 35 37 39 3B 3D 3F) */ + + mm8 = _mm_unpacklo_pi8(mmA, mmC); /* (00 10 02 12 04 14 06 16) */ + mm9 = _mm_unpackhi_pi8(mmA, mmC); /* (08 18 0A 1A 0C 1C 0E 1E) */ + mmA = _mm_unpacklo_pi8(mmE, mmG); /* (20 30 22 32 24 34 26 36) */ + mmE = _mm_unpackhi_pi8(mmE, mmG); /* (28 38 2A 3A 2C 3C 2E 3E) */ + + mmG = _mm_unpacklo_pi8(mmB, mmD); /* (01 11 03 13 05 15 07 17) */ + mmB = _mm_unpackhi_pi8(mmB, mmD); /* (09 19 0B 1B 0D 1D 0F 1F) */ + mmD = _mm_unpacklo_pi8(mmF, mmH); /* (21 31 23 33 25 35 27 37) */ + mmF = _mm_unpackhi_pi8(mmF, mmH); /* (29 39 2B 3B 2D 3D 2F 3F) */ + + mmH = _mm_unpacklo_pi16(mm8, mmA); /* (00 10 20 30 02 12 22 32) */ + mm8 = _mm_unpackhi_pi16(mm8, mmA); /* (04 14 24 34 06 16 26 36) */ + mmA = _mm_unpacklo_pi16(mmG, mmD); /* (01 11 21 31 03 13 23 33) */ + mmD = _mm_unpackhi_pi16(mmG, mmD); /* (05 15 25 35 07 17 27 37) */ + + mmG = _mm_unpackhi_pi16(mm9, mmE); /* (0C 1C 2C 3C 0E 1E 2E 3E) */ + mm9 = _mm_unpacklo_pi16(mm9, mmE); /* (08 18 28 38 0A 1A 2A 3A) */ + mmE = _mm_unpacklo_pi16(mmB, mmF); /* (09 19 29 39 0B 1B 2B 3B) */ + mmF = _mm_unpackhi_pi16(mmB, mmF); /* (0D 1D 2D 3D 0F 1F 2F 3F) */ + + mmB = _mm_unpackhi_pi32(mmH, mmA); /* (02 12 22 32 03 13 23 33) */ + mmA = _mm_unpacklo_pi32(mmH, mmA); /* (00 10 20 30 01 11 21 31) */ + mmC = _mm_unpacklo_pi32(mm8, mmD); /* (04 14 24 34 05 15 25 35) */ + mmD = _mm_unpackhi_pi32(mm8, mmD); /* (06 16 26 36 07 17 27 37) */ + + mmH = _mm_unpackhi_pi32(mmG, mmF); /* (0E 1E 2E 3E 0F 1F 2F 3F) */ + mmG = _mm_unpacklo_pi32(mmG, mmF); /* (0C 1C 2C 3C 0D 1D 2D 3D) */ + mmF = _mm_unpackhi_pi32(mm9, mmE); /* (0A 1A 2A 3A 0B 1B 2B 3B) */ + mmE = _mm_unpacklo_pi32(mm9, mmE); /* (08 18 28 38 09 19 29 39) */ + + if (num_cols >= 8) { + if (!(((long)outptr) & 7)) { + _mm_store_si64((__m64 *)outptr, mmA); + _mm_store_si64((__m64 *)(outptr + 8), mmB); + _mm_store_si64((__m64 *)(outptr + 16), mmC); + _mm_store_si64((__m64 *)(outptr + 24), mmD); + _mm_store_si64((__m64 *)(outptr + 32), mmE); + _mm_store_si64((__m64 *)(outptr + 40), mmF); + _mm_store_si64((__m64 *)(outptr + 48), mmG); + _mm_store_si64((__m64 *)(outptr + 56), mmH); + } else { + _mm_storeu_si64((__m64 *)outptr, mmA); + _mm_storeu_si64((__m64 *)(outptr + 8), mmB); + _mm_storeu_si64((__m64 *)(outptr + 16), mmC); + _mm_storeu_si64((__m64 *)(outptr + 24), mmD); + _mm_storeu_si64((__m64 *)(outptr + 32), mmE); + _mm_storeu_si64((__m64 *)(outptr + 40), mmF); + _mm_storeu_si64((__m64 *)(outptr + 48), mmG); + _mm_storeu_si64((__m64 *)(outptr + 56), mmH); + } + outptr += RGB_PIXELSIZE * 16; + } else { + if (output_width & 1) + col = num_cols * 2 + 1; + else + col = num_cols * 2; + asm(".set noreorder\r\n" /* st32 */ + + "li $8, 8\r\n" + "move $9, %10\r\n" + "move $10, %11\r\n" + "mov.s $f4, %2\r\n" + "mov.s $f6, %3\r\n" + "mov.s $f8, %4\r\n" + "mov.s $f10, %5\r\n" + "bltu $9, $8, 1f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "gssdlc1 $f6, 7+8($10)\r\n" + "gssdrc1 $f6, 8($10)\r\n" + "gssdlc1 $f8, 7+16($10)\r\n" + "gssdrc1 $f8, 16($10)\r\n" + "gssdlc1 $f10, 7+24($10)\r\n" + "gssdrc1 $f10, 24($10)\r\n" + "mov.s $f4, %6\r\n" + "mov.s $f6, %7\r\n" + "mov.s $f8, %8\r\n" + "mov.s $f10, %9\r\n" + "subu $9, $9, 8\r\n" + PTR_ADDU "$10, $10, 32\r\n" + + "1: \r\n" + "li $8, 4\r\n" /* st16 */ + "bltu $9, $8, 2f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "gssdlc1 $f6, 7+8($10)\r\n" + "gssdrc1 $f6, 8($10)\r\n" + "mov.s $f4, $f8\r\n" + "mov.s $f6, $f10\r\n" + "subu $9, $9, 4\r\n" + PTR_ADDU "$10, $10, 16\r\n" + + "2: \r\n" + "li $8, 2\r\n" /* st8 */ + "bltu $9, $8, 3f\r\n" + "nop \r\n" + "gssdlc1 $f4, 7($10)\r\n" + "gssdrc1 $f4, 0($10)\r\n" + "mov.s $f4, $f6\r\n" + "subu $9, $9, 2\r\n" + PTR_ADDU "$10, $10, 8\r\n" + + "3: \r\n" + "li $8, 1\r\n" /* st4 */ + "bltu $9, $8, 4f\r\n" + "nop \r\n" + "gsswlc1 $f4, 3($10)\r\n" + "gsswrc1 $f4, 0($10)\r\n" + + "4: \r\n" + "li %1, 0\r\n" /* end */ + : "=m" (*outptr), "=r" (col) + : "f" (mmA), "f" (mmB), "f" (mmC), "f" (mmD), "f" (mmE), "f" (mmF), + "f" (mmG), "f" (mmH), "r" (col), "r" (outptr) + : "$f4", "$f6", "$f8", "$f10", "$8", "$9", "$10", "memory" + ); + } + +#endif + + } + + if (!((output_width >> 1) & 7)) { + if (output_width & 1) { + cb = _mm_load_si64((__m64 *)inptr1); + cr = _mm_load_si64((__m64 *)inptr2); + y = _mm_load_si64((__m64 *)inptr0); + + decenter = 0.0; + decenter = _mm_cmpeq_pi16(decenter, decenter); + decenter = _mm_slli_pi16(decenter, 7); /* {0xFF80 0xFF80 0xFF80 0xFF80} */ + + cbl = _mm_unpacklo_pi8(cb, zero); /* Cb(0123) */ + crl = _mm_unpacklo_pi8(cr, zero); /* Cr(0123) */ + cbl = _mm_add_pi16(cbl, decenter); + crl = _mm_add_pi16(crl, decenter); + + cbl2 = _mm_add_pi16(cbl, cbl); /* 2*CbL */ + crl2 = _mm_add_pi16(crl, crl); /* 2*CrL */ + bl = _mm_mulhi_pi16(cbl2, PW_MF0228); /* (2*CbL * -FIX(0.22800) */ + rl = _mm_mulhi_pi16(crl2, PW_F0402); /* (2*CrL * FIX(0.40200)) */ + + bl = _mm_add_pi16(bl, PW_ONE); + bl = _mm_srai_pi16(bl, 1); /* (CbL * -FIX(0.22800)) */ + rl = _mm_add_pi16(rl, PW_ONE); + rl = _mm_srai_pi16(rl, 1); /* (CrL * FIX(0.40200)) */ + + bl = _mm_add_pi16(bl, cbl); + bl = _mm_add_pi16(bl, cbl); /* (CbL * FIX(1.77200))=(B-Y)L */ + rl = _mm_add_pi16(rl, crl); /* (CrL * FIX(1.40200))=(R-Y)L */ + + gl = _mm_unpacklo_pi16(cbl, crl); + gl = _mm_madd_pi16(gl, PW_MF0344_F0285); + gl = _mm_add_pi32(gl, PD_ONEHALF); + gl = _mm_srai_pi32(gl, SCALEBITS); + gl = _mm_packs_pi32(gl, zero); /* CbL*-FIX(0.344)+CrL*FIX(0.285) */ + gl = _mm_sub_pi16(gl, crl); /* CbL*-FIX(0.344)+CrL*-FIX(0.714)=(G-Y)L */ + + yl = _mm_unpacklo_pi8(y, zero); /* Y(0123) */ + rl = _mm_add_pi16(rl, yl); /* (R0 R1 R2 R3) */ + gl = _mm_add_pi16(gl, yl); /* (G0 G1 G2 G3) */ + bl = _mm_add_pi16(bl, yl); /* (B0 B1 B2 B3) */ + re = _mm_packs_pu16(rl, rl); + ge = _mm_packs_pu16(gl, gl); + be = _mm_packs_pu16(bl, bl); +#if RGB_PIXELSIZE == 3 + mmA = _mm_unpacklo_pi8(mmA, mmC); + mmA = _mm_unpacklo_pi16(mmA, mmE); + asm(".set noreorder\r\n" + + "move $8, %2\r\n" + "mov.s $f4, %1\r\n" + "mfc1 $9, $f4\r\n" + "ush $9, 0($8)\r\n" + "srl $9, 16\r\n" + "sb $9, 2($8)\r\n" + : "=m" (*outptr) + : "f" (mmA), "r" (outptr) + : "$f4", "$8", "$9", "memory" + ); +#else /* RGB_PIXELSIZE == 4 */ + +#ifdef RGBX_FILLER_0XFF + xe = _mm_cmpeq_pi8(xe, xe); +#else + xe = _mm_xor_si64(xe, xe); +#endif + mmA = _mm_unpacklo_pi8(mmA, mmC); + mmE = _mm_unpacklo_pi8(mmE, mmG); + mmA = _mm_unpacklo_pi16(mmA, mmE); + asm(".set noreorder\r\n" + + "move $8, %2\r\n" + "mov.s $f4, %1\r\n" + "gsswlc1 $f4, 3($8)\r\n" + "gsswrc1 $f4, 0($8)\r\n" + : "=m" (*outptr) + : "f" (mmA), "r" (outptr) + : "$f4", "$8", "memory" + ); +#endif + } + } +} + + +void jsimd_h2v2_merged_upsample_mmi(JDIMENSION output_width, + JSAMPIMAGE input_buf, + JDIMENSION in_row_group_ctr, + JSAMPARRAY output_buf) +{ + JSAMPROW inptr, outptr; + + inptr = input_buf[0][in_row_group_ctr]; + outptr = output_buf[0]; + + input_buf[0][in_row_group_ctr] = input_buf[0][in_row_group_ctr * 2]; + jsimd_h2v1_merged_upsample_mmi(output_width, input_buf, in_row_group_ctr, + output_buf); + + input_buf[0][in_row_group_ctr] = input_buf[0][in_row_group_ctr * 2 + 1]; + output_buf[0] = output_buf[1]; + jsimd_h2v1_merged_upsample_mmi(output_width, input_buf, in_row_group_ctr, + output_buf); + + input_buf[0][in_row_group_ctr] = inptr; + output_buf[0] = outptr; +} + + +#undef mmA +#undef mmB +#undef mmC +#undef mmD +#undef mmE +#undef mmF +#undef mmG +#undef mmH diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jdsample-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jdsample-mmi.c new file mode 100644 index 00000000000..8ae94e7dcf9 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jdsample-mmi.c @@ -0,0 +1,304 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright (C) 2015, 2018-2019, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhuChen + * CaiWanwei + * SunZhangzhi + * ZhangLixia + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* CHROMA UPSAMPLING */ + +#include "jsimd_mmi.h" + + +enum const_index { + index_PW_ONE, + index_PW_TWO, + index_PW_THREE, + index_PW_SEVEN, + index_PW_EIGHT, +}; + +static uint64_t const_value[] = { + _uint64_set_pi16(1, 1, 1, 1), + _uint64_set_pi16(2, 2, 2, 2), + _uint64_set_pi16(3, 3, 3, 3), + _uint64_set_pi16(7, 7, 7, 7), + _uint64_set_pi16(8, 8, 8, 8), +}; + +#define PW_ONE get_const_value(index_PW_ONE) +#define PW_TWO get_const_value(index_PW_TWO) +#define PW_THREE get_const_value(index_PW_THREE) +#define PW_SEVEN get_const_value(index_PW_SEVEN) +#define PW_EIGHT get_const_value(index_PW_EIGHT) + + +#define PROCESS_ROW(row, wkoffset, bias1, bias2, shift) { \ + __m64 samp123X, samp3XXX, samp1234, sampX012, samp_1012; \ + __m64 sampXXX4, sampX456, samp3456, samp567X, samp7XXX, samp5678; \ + __m64 outle, outhe, outlo, outho, outl, outh; \ + \ + samp123X = _mm_srli_si64(samp0123, 2 * BYTE_BIT); /* ( 1 2 3 -) */ \ + sampXXX4 = _mm_slli_si64(samp4567, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* ( - - - 4) */ \ + samp3XXX = _mm_srli_si64(samp0123, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* ( 3 - - -) */ \ + sampX456 = _mm_slli_si64(samp4567, 2 * BYTE_BIT); /* ( - 4 5 6) */ \ + \ + samp1234 = _mm_or_si64(samp123X, sampXXX4); /* ( 1 2 3 4) */ \ + samp3456 = _mm_or_si64(samp3XXX, sampX456); /* ( 3 4 5 6) */ \ + \ + sampX012 = _mm_slli_si64(samp0123, 2 * BYTE_BIT); /* ( - 0 1 2) */ \ + samp567X = _mm_srli_si64(samp4567, 2 * BYTE_BIT); /* ( 5 6 7 -) */ \ + samp7XXX = _mm_srli_si64(samp4567, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* ( 7 - - -) */ \ + \ + samp_1012 = _mm_or_si64(sampX012, wk[row]); /* (-1 0 1 2) */ \ + samp5678 = _mm_or_si64(samp567X, wk[row + wkoffset]); /* ( 5 6 7 8) */ \ + \ + wk[row] = samp7XXX; \ + \ + samp0123 = _mm_mullo_pi16(samp0123, PW_THREE); \ + samp4567 = _mm_mullo_pi16(samp4567, PW_THREE); \ + samp_1012 = _mm_add_pi16(samp_1012, bias1); \ + samp3456 = _mm_add_pi16(samp3456, bias1); \ + samp1234 = _mm_add_pi16(samp1234, bias2); \ + samp5678 = _mm_add_pi16(samp5678, bias2); \ + \ + outle = _mm_add_pi16(samp_1012, samp0123); \ + outhe = _mm_add_pi16(samp3456, samp4567); \ + outle = _mm_srli_pi16(outle, shift); /* ( 0 2 4 6) */ \ + outhe = _mm_srli_pi16(outhe, shift); /* ( 8 10 12 14) */ \ + outlo = _mm_add_pi16(samp1234, samp0123); \ + outho = _mm_add_pi16(samp5678, samp4567); \ + outlo = _mm_srli_pi16(outlo, shift); /* ( 1 3 5 7) */ \ + outho = _mm_srli_pi16(outho, shift); /* ( 9 11 13 15) */ \ + \ + outlo = _mm_slli_pi16(outlo, BYTE_BIT); \ + outho = _mm_slli_pi16(outho, BYTE_BIT); \ + outl = _mm_or_si64(outle, outlo); /* ( 0 1 2 3 4 5 6 7) */ \ + outh = _mm_or_si64(outhe, outho); /* ( 8 9 10 11 12 13 14 15) */ \ + \ + _mm_store_si64((__m64 *)outptr##row, outl); \ + _mm_store_si64((__m64 *)outptr##row + 1, outh); \ +} + +void jsimd_h2v2_fancy_upsample_mmi(int max_v_samp_factor, + JDIMENSION downsampled_width, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr) +{ + JSAMPARRAY output_data = *output_data_ptr; + JSAMPROW inptr_1, inptr0, inptr1, outptr0, outptr1; + int inrow, outrow, incol, tmp, tmp1; + __m64 this_1l, this_1h, this_1, thiscolsum_1l, thiscolsum_1h; + __m64 this0l, this0h, this0; + __m64 this1l, this1h, this1, thiscolsum1l, thiscolsum1h; + __m64 next_1l, next_1h, next_1, nextcolsum_1l, nextcolsum_1h; + __m64 next0l, next0h, next0; + __m64 next1l, next1h, next1, nextcolsum1l, nextcolsum1h; + __m64 mask0 = 0.0, masklast, samp0123, samp4567, wk[4], zero = 0.0; + + mask0 = _mm_cmpeq_pi8(mask0, mask0); + masklast = _mm_slli_si64(mask0, (SIZEOF_MMWORD - 2) * BYTE_BIT); + mask0 = _mm_srli_si64(mask0, (SIZEOF_MMWORD - 2) * BYTE_BIT); + + for (inrow = 0, outrow = 0; outrow < max_v_samp_factor; inrow++) { + + inptr_1 = input_data[inrow - 1]; + inptr0 = input_data[inrow]; + inptr1 = input_data[inrow + 1]; + outptr0 = output_data[outrow++]; + outptr1 = output_data[outrow++]; + + if (downsampled_width & 7) { + tmp = (downsampled_width - 1) * sizeof(JSAMPLE); + tmp1 = downsampled_width * sizeof(JSAMPLE); + asm(PTR_ADDU "$8, %3, %6\r\n" + "lb $9, ($8)\r\n" + PTR_ADDU "$8, %3, %7\r\n" + "sb $9, ($8)\r\n" + PTR_ADDU "$8, %4, %6\r\n" + "lb $9, ($8)\r\n" + PTR_ADDU "$8, %4, %7\r\n" + "sb $9, ($8)\r\n" + PTR_ADDU "$8, %5, %6\r\n" + "lb $9, ($8)\r\n" + PTR_ADDU "$8, %5, %7\r\n" + "sb $9, ($8)\r\n" + : "=m" (*inptr_1), "=m" (*inptr0), "=m" (*inptr1) + : "r" (inptr_1), "r" (inptr0), "r" (inptr1), "r" (tmp), "r" (tmp1) + : "$8", "$9" + ); + } + + /* process the first column block */ + this0 = _mm_load_si64((__m64 *)inptr0); /* row[ 0][0] */ + this_1 = _mm_load_si64((__m64 *)inptr_1); /* row[-1][0] */ + this1 = _mm_load_si64((__m64 *)inptr1); /* row[ 1][0] */ + + this0l = _mm_unpacklo_pi8(this0, zero); /* row[ 0][0]( 0 1 2 3) */ + this0h = _mm_unpackhi_pi8(this0, zero); /* row[ 0][0]( 4 5 6 7) */ + this_1l = _mm_unpacklo_pi8(this_1, zero); /* row[-1][0]( 0 1 2 3) */ + this_1h = _mm_unpackhi_pi8(this_1, zero); /* row[-1][0]( 4 5 6 7) */ + this1l = _mm_unpacklo_pi8(this1, zero); /* row[+1][0]( 0 1 2 3) */ + this1h = _mm_unpackhi_pi8(this1, zero); /* row[+1][0]( 4 5 6 7) */ + + this0l = _mm_mullo_pi16(this0l, PW_THREE); + this0h = _mm_mullo_pi16(this0h, PW_THREE); + + thiscolsum_1l = _mm_add_pi16(this_1l, this0l); /* ( 0 1 2 3) */ + thiscolsum_1h = _mm_add_pi16(this_1h, this0h); /* ( 4 5 6 7) */ + thiscolsum1l = _mm_add_pi16(this0l, this1l); /* ( 0 1 2 3) */ + thiscolsum1h = _mm_add_pi16(this0h, this1h); /* ( 4 5 6 7) */ + + /* temporarily save the intermediate data */ + _mm_store_si64((__m64 *)outptr0, thiscolsum_1l); + _mm_store_si64((__m64 *)outptr0 + 1, thiscolsum_1h); + _mm_store_si64((__m64 *)outptr1, thiscolsum1l); + _mm_store_si64((__m64 *)outptr1 + 1, thiscolsum1h); + + wk[0] = _mm_and_si64(thiscolsum_1l, mask0); /* ( 0 - - -) */ + wk[1] = _mm_and_si64(thiscolsum1l, mask0); /* ( 0 - - -) */ + + for (incol = downsampled_width; incol > 0; + incol -= 8, inptr_1 += 8, inptr0 += 8, inptr1 += 8, + outptr0 += 16, outptr1 += 16) { + + if (incol > 8) { + /* process the next column block */ + next0 = _mm_load_si64((__m64 *)inptr0 + 1); /* row[ 0][1] */ + next_1 = _mm_load_si64((__m64 *)inptr_1 + 1); /* row[-1][1] */ + next1 = _mm_load_si64((__m64 *)inptr1 + 1); /* row[+1][1] */ + + next0l = _mm_unpacklo_pi8(next0, zero); /* row[ 0][1]( 0 1 2 3) */ + next0h = _mm_unpackhi_pi8(next0, zero); /* row[ 0][1]( 4 5 6 7) */ + next_1l = _mm_unpacklo_pi8(next_1, zero); /* row[-1][1]( 0 1 2 3) */ + next_1h = _mm_unpackhi_pi8(next_1, zero); /* row[-1][1]( 4 5 6 7) */ + next1l = _mm_unpacklo_pi8(next1, zero); /* row[+1][1]( 0 1 2 3) */ + next1h = _mm_unpackhi_pi8(next1, zero); /* row[+1][1]( 4 5 6 7) */ + + next0l = _mm_mullo_pi16(next0l, PW_THREE); + next0h = _mm_mullo_pi16(next0h, PW_THREE); + + nextcolsum_1l = _mm_add_pi16(next_1l, next0l); /* ( 0 1 2 3) */ + nextcolsum_1h = _mm_add_pi16(next_1h, next0h); /* ( 4 5 6 7) */ + nextcolsum1l = _mm_add_pi16(next0l, next1l); /* ( 0 1 2 3) */ + nextcolsum1h = _mm_add_pi16(next0h, next1h); /* ( 4 5 6 7) */ + + /* temporarily save the intermediate data */ + _mm_store_si64((__m64 *)outptr0 + 2, nextcolsum_1l); + _mm_store_si64((__m64 *)outptr0 + 3, nextcolsum_1h); + _mm_store_si64((__m64 *)outptr1 + 2, nextcolsum1l); + _mm_store_si64((__m64 *)outptr1 + 3, nextcolsum1h); + + wk[2] = _mm_slli_si64(nextcolsum_1l, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* ( - - - 0) */ + wk[3] = _mm_slli_si64(nextcolsum1l, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* ( - - - 0) */ + } else { + __m64 tmp; + + /* process the last column block */ + tmp = _mm_load_si64((__m64 *)outptr0 + 1); + wk[2] = _mm_and_si64(masklast, tmp); /* ( - - - 7) */ + tmp = _mm_load_si64((__m64 *)outptr1 + 1); + wk[3] = _mm_and_si64(masklast, tmp); /* ( - - - 7) */ + } + + /* process the upper row */ + samp0123 = _mm_load_si64((__m64 *)outptr0); /* ( 0 1 2 3) */ \ + samp4567 = _mm_load_si64((__m64 *)outptr0 + 1); /* ( 4 5 6 7) */ \ + PROCESS_ROW(0, 2, PW_EIGHT, PW_SEVEN, 4) + + /* process the lower row */ + samp0123 = _mm_load_si64((__m64 *)outptr1); /* ( 0 1 2 3) */ \ + samp4567 = _mm_load_si64((__m64 *)outptr1 + 1); /* ( 4 5 6 7) */ \ + PROCESS_ROW(1, 2, PW_EIGHT, PW_SEVEN, 4) + } + } +} + + +void jsimd_h2v1_fancy_upsample_mmi(int max_v_samp_factor, + JDIMENSION downsampled_width, + JSAMPARRAY input_data, + JSAMPARRAY *output_data_ptr) +{ + JSAMPARRAY output_data = *output_data_ptr; + JSAMPROW inptr0, outptr0; + int inrow, incol, tmp, tmp1; + __m64 thisl, this, nextl, next; + __m64 mask0 = 0.0, masklast, samp0123, samp4567, wk[2], zero = 0.0; + + mask0 = _mm_cmpeq_pi8(mask0, mask0); + masklast = _mm_slli_si64(mask0, (SIZEOF_MMWORD - 2) * BYTE_BIT); + mask0 = _mm_srli_si64(mask0, (SIZEOF_MMWORD - 2) * BYTE_BIT); + + for (inrow = 0; inrow < max_v_samp_factor; inrow++) { + + inptr0 = input_data[inrow]; + outptr0 = output_data[inrow]; + + if (downsampled_width & 7) { + tmp = (downsampled_width - 1) * sizeof(JSAMPLE); + tmp1 = downsampled_width * sizeof(JSAMPLE); + asm(PTR_ADDU "$8, %1, %2\r\n" + "lb $9, ($8)\r\n" + PTR_ADDU "$8, %1, %3\r\n" + "sb $9, ($8)\r\n" + : "=m" (*inptr0) + : "r" (inptr0), "r" (tmp), "r" (tmp1) + : "$8", "$9" + ); + } + + /* process the first column block */ + this = _mm_load_si64((__m64 *)inptr0); /* row[ 0][0] */ + thisl = _mm_unpacklo_pi8(this, zero); /* row[ 0][0]( 0 1 2 3) */ + wk[0] = _mm_and_si64(thisl, mask0); /* ( 0 - - -) */ + + for (incol = downsampled_width; incol > 0; + incol -= 8, inptr0 += 8, outptr0 += 16) { + + if (incol > 8) { + /* process the next column block */ + next = _mm_load_si64((__m64 *)inptr0 + 1); /* row[ 0][1] */ + nextl = _mm_unpacklo_pi8(next, zero); /* row[ 0][1]( 0 1 2 3) */ + wk[1] = _mm_slli_si64(nextl, (SIZEOF_MMWORD - 2) * BYTE_BIT); /* ( - - - 0) */ + } else { + __m64 thish; + + /* process the last column block */ + this = _mm_load_si64((__m64 *)inptr0); /* row[ 0][0] */ + thish = _mm_unpackhi_pi8(this, zero); /* row[ 0][1]( 4 5 6 7) */ + wk[1] = _mm_and_si64(masklast, thish); /* ( - - - 7) */ + } + + /* process the row */ + this = _mm_load_si64((__m64 *)inptr0); /* row[ 0][0] */ + samp0123 = _mm_unpacklo_pi8(this, zero); /* ( 0 1 2 3) */ + samp4567 = _mm_unpackhi_pi8(this, zero); /* ( 4 5 6 7) */ + PROCESS_ROW(0, 1, PW_ONE, PW_TWO, 2) + } + } +} diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jfdctfst-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jfdctfst-mmi.c new file mode 100644 index 00000000000..f7caf09a886 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jfdctfst-mmi.c @@ -0,0 +1,255 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright (C) 2014, 2018-2019, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: LiuQingfa + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* FAST INTEGER FORWARD DCT */ + +#include "jsimd_mmi.h" + + +#define CONST_BITS 8 + +#define F_0_382 ((short)98) /* FIX(0.382683433) */ +#define F_0_541 ((short)139) /* FIX(0.541196100) */ +#define F_0_707 ((short)181) /* FIX(0.707106781) */ +#define F_1_306 ((short)334) /* FIX(1.306562965) */ + +#define PRE_MULTIPLY_SCALE_BITS 2 +#define CONST_SHIFT (16 - PRE_MULTIPLY_SCALE_BITS - CONST_BITS) + +enum const_index { + index_PW_F0707, + index_PW_F0382, + index_PW_F0541, + index_PW_F1306 +}; + +static uint64_t const_value[] = { + _uint64_set1_pi16(F_0_707), + _uint64_set1_pi16(F_0_382), + _uint64_set1_pi16(F_0_541), + _uint64_set1_pi16(F_1_306) +}; + +#define PW_F0707 get_const_value(index_PW_F0707) +#define PW_F0382 get_const_value(index_PW_F0382) +#define PW_F0541 get_const_value(index_PW_F0541) +#define PW_F1306 get_const_value(index_PW_F1306) + + +#define DO_FDCT_MULTIPLY(out, in, multiplier) { \ + __m64 mulhi, mullo, mul12, mul34; \ + \ + mullo = _mm_mullo_pi16(in, multiplier); \ + mulhi = _mm_mulhi_pi16(in, multiplier); \ + mul12 = _mm_unpacklo_pi16(mullo, mulhi); \ + mul34 = _mm_unpackhi_pi16(mullo, mulhi); \ + mul12 = _mm_srai_pi32(mul12, CONST_BITS); \ + mul34 = _mm_srai_pi32(mul34, CONST_BITS); \ + out = _mm_packs_pi32(mul12, mul34); \ +} + +#define DO_FDCT_COMMON() { \ + \ + /* Even part */ \ + \ + tmp10 = _mm_add_pi16(tmp0, tmp3); \ + tmp13 = _mm_sub_pi16(tmp0, tmp3); \ + tmp11 = _mm_add_pi16(tmp1, tmp2); \ + tmp12 = _mm_sub_pi16(tmp1, tmp2); \ + \ + out0 = _mm_add_pi16(tmp10, tmp11); \ + out4 = _mm_sub_pi16(tmp10, tmp11); \ + \ + z1 = _mm_add_pi16(tmp12, tmp13); \ + DO_FDCT_MULTIPLY(z1, z1, PW_F0707) \ + \ + out2 = _mm_add_pi16(tmp13, z1); \ + out6 = _mm_sub_pi16(tmp13, z1); \ + \ + /* Odd part */ \ + \ + tmp10 = _mm_add_pi16(tmp4, tmp5); \ + tmp11 = _mm_add_pi16(tmp5, tmp6); \ + tmp12 = _mm_add_pi16(tmp6, tmp7); \ + \ + z5 = _mm_sub_pi16(tmp10, tmp12); \ + DO_FDCT_MULTIPLY(z5, z5, PW_F0382) \ + \ + DO_FDCT_MULTIPLY(z2, tmp10, PW_F0541) \ + z2 = _mm_add_pi16(z2, z5); \ + \ + DO_FDCT_MULTIPLY(z4, tmp12, PW_F1306) \ + z4 = _mm_add_pi16(z4, z5); \ + \ + DO_FDCT_MULTIPLY(z3, tmp11, PW_F0707) \ + \ + z11 = _mm_add_pi16(tmp7, z3); \ + z13 = _mm_sub_pi16(tmp7, z3); \ + \ + out5 = _mm_add_pi16(z13, z2); \ + out3 = _mm_sub_pi16(z13, z2); \ + out1 = _mm_add_pi16(z11, z4); \ + out7 = _mm_sub_pi16(z11, z4); \ +} + +#define DO_FDCT_PASS1() { \ + __m64 row0l, row0h, row1l, row1h, row2l, row2h, row3l, row3h; \ + __m64 row01a, row01b, row01c, row01d, row23a, row23b, row23c, row23d; \ + __m64 col0, col1, col2, col3, col4, col5, col6, col7; \ + \ + row0l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 0]); /* (00 01 02 03) */ \ + row0h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 0 + 4]); /* (04 05 06 07) */ \ + row1l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 1]); /* (10 11 12 13) */ \ + row1h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 1 + 4]); /* (14 15 16 17) */ \ + row2l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 2]); /* (20 21 22 23) */ \ + row2h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 2 + 4]); /* (24 25 26 27) */ \ + row3l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 3]); /* (30 31 32 33) */ \ + row3h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 3 + 4]); /* (34 35 36 37) */ \ + \ + /* Transpose coefficients */ \ + \ + row23a = _mm_unpacklo_pi16(row2l, row3l); /* row23a=(20 30 21 31) */ \ + row23b = _mm_unpackhi_pi16(row2l, row3l); /* row23b=(22 32 23 33) */ \ + row23c = _mm_unpacklo_pi16(row2h, row3h); /* row23c=(24 34 25 35) */ \ + row23d = _mm_unpackhi_pi16(row2h, row3h); /* row23d=(26 36 27 37) */ \ + \ + row01a = _mm_unpacklo_pi16(row0l, row1l); /* row01a=(00 10 01 11) */ \ + row01b = _mm_unpackhi_pi16(row0l, row1l); /* row01b=(02 12 03 13) */ \ + row01c = _mm_unpacklo_pi16(row0h, row1h); /* row01c=(04 14 05 15) */ \ + row01d = _mm_unpackhi_pi16(row0h, row1h); /* row01d=(06 16 07 17) */ \ + \ + col0 = _mm_unpacklo_pi32(row01a, row23a); /* col0=(00 10 20 30) */ \ + col1 = _mm_unpackhi_pi32(row01a, row23a); /* col1=(01 11 21 31) */ \ + col6 = _mm_unpacklo_pi32(row01d, row23d); /* col6=(06 16 26 36) */ \ + col7 = _mm_unpackhi_pi32(row01d, row23d); /* col7=(07 17 27 37) */ \ + \ + tmp6 = _mm_sub_pi16(col1, col6); /* tmp6=col1-col6 */ \ + tmp7 = _mm_sub_pi16(col0, col7); /* tmp7=col0-col7 */ \ + tmp1 = _mm_add_pi16(col1, col6); /* tmp1=col1+col6 */ \ + tmp0 = _mm_add_pi16(col0, col7); /* tmp0=col0+col7 */ \ + \ + col2 = _mm_unpacklo_pi32(row01b, row23b); /* col2=(02 12 22 32) */ \ + col3 = _mm_unpackhi_pi32(row01b, row23b); /* col3=(03 13 23 33) */ \ + col4 = _mm_unpacklo_pi32(row01c, row23c); /* col4=(04 14 24 34) */ \ + col5 = _mm_unpackhi_pi32(row01c, row23c); /* col5=(05 15 25 35) */ \ + \ + tmp3 = _mm_add_pi16(col3, col4); /* tmp3=col3+col4 */ \ + tmp2 = _mm_add_pi16(col2, col5); /* tmp2=col2+col5 */ \ + tmp4 = _mm_sub_pi16(col3, col4); /* tmp4=col3-col4 */ \ + tmp5 = _mm_sub_pi16(col2, col5); /* tmp5=col2-col5 */ \ + \ + DO_FDCT_COMMON() \ + \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 0], out0); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 0 + 4], out4); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 1], out1); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 1 + 4], out5); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 2], out2); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 2 + 4], out6); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 3], out3); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 3 + 4], out7); \ +} + +#define DO_FDCT_PASS2() { \ + __m64 col0l, col0h, col1l, col1h, col2l, col2h, col3l, col3h; \ + __m64 col01a, col01b, col01c, col01d, col23a, col23b, col23c, col23d; \ + __m64 row0, row1, row2, row3, row4, row5, row6, row7; \ + \ + col0l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 0]); /* (00 10 20 30) */ \ + col1l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 1]); /* (01 11 21 31) */ \ + col2l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 2]); /* (02 12 22 32) */ \ + col3l = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 3]); /* (03 13 23 33) */ \ + col0h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 4]); /* (40 50 60 70) */ \ + col1h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 5]); /* (41 51 61 71) */ \ + col2h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 6]); /* (42 52 62 72) */ \ + col3h = _mm_load_si64((__m64 *)&dataptr[DCTSIZE * 7]); /* (43 53 63 73) */ \ + \ + /* Transpose coefficients */ \ + \ + col23a = _mm_unpacklo_pi16(col2l, col3l); /* col23a=(02 03 12 13) */ \ + col23b = _mm_unpackhi_pi16(col2l, col3l); /* col23b=(22 23 32 33) */ \ + col23c = _mm_unpacklo_pi16(col2h, col3h); /* col23c=(42 43 52 53) */ \ + col23d = _mm_unpackhi_pi16(col2h, col3h); /* col23d=(62 63 72 73) */ \ + \ + col01a = _mm_unpacklo_pi16(col0l, col1l); /* col01a=(00 01 10 11) */ \ + col01b = _mm_unpackhi_pi16(col0l, col1l); /* col01b=(20 21 30 31) */ \ + col01c = _mm_unpacklo_pi16(col0h, col1h); /* col01c=(40 41 50 51) */ \ + col01d = _mm_unpackhi_pi16(col0h, col1h); /* col01d=(60 61 70 71) */ \ + \ + row0 = _mm_unpacklo_pi32(col01a, col23a); /* row0=(00 01 02 03) */ \ + row1 = _mm_unpackhi_pi32(col01a, col23a); /* row1=(10 11 12 13) */ \ + row6 = _mm_unpacklo_pi32(col01d, col23d); /* row6=(60 61 62 63) */ \ + row7 = _mm_unpackhi_pi32(col01d, col23d); /* row7=(70 71 72 73) */ \ + \ + tmp6 = _mm_sub_pi16(row1, row6); /* tmp6=row1-row6 */ \ + tmp7 = _mm_sub_pi16(row0, row7); /* tmp7=row0-row7 */ \ + tmp1 = _mm_add_pi16(row1, row6); /* tmp1=row1+row6 */ \ + tmp0 = _mm_add_pi16(row0, row7); /* tmp0=row0+row7 */ \ + \ + row2 = _mm_unpacklo_pi32(col01b, col23b); /* row2=(20 21 22 23) */ \ + row3 = _mm_unpackhi_pi32(col01b, col23b); /* row3=(30 31 32 33) */ \ + row4 = _mm_unpacklo_pi32(col01c, col23c); /* row4=(40 41 42 43) */ \ + row5 = _mm_unpackhi_pi32(col01c, col23c); /* row5=(50 51 52 53) */ \ + \ + tmp3 = _mm_add_pi16(row3, row4); /* tmp3=row3+row4 */ \ + tmp2 = _mm_add_pi16(row2, row5); /* tmp2=row2+row5 */ \ + tmp4 = _mm_sub_pi16(row3, row4); /* tmp4=row3-row4 */ \ + tmp5 = _mm_sub_pi16(row2, row5); /* tmp5=row2-row5 */ \ + \ + DO_FDCT_COMMON() \ + \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 0], out0); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 1], out1); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 2], out2); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 3], out3); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 4], out4); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 5], out5); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 6], out6); \ + _mm_store_si64((__m64 *)&dataptr[DCTSIZE * 7], out7); \ +} + +void jsimd_fdct_ifast_mmi(DCTELEM *data) +{ + __m64 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7; + __m64 out0, out1, out2, out3, out4, out5, out6, out7; + __m64 tmp10, tmp11, tmp12, tmp13, z1, z2, z3, z4, z5, z11, z13; + DCTELEM *dataptr = data; + + /* Pass 1: process rows. */ + + DO_FDCT_PASS1() + dataptr += DCTSIZE * 4; + DO_FDCT_PASS1() + + /* Pass 2: process columns. */ + + dataptr = data; + DO_FDCT_PASS2() + dataptr += 4; + DO_FDCT_PASS2() +} diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jfdctint-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jfdctint-mmi.c similarity index 99% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jfdctint-mmi.c rename to third-party/mozjpeg/mozjpeg/simd/mips64/jfdctint-mmi.c index a0ea692a008..7f4dfe91234 100644 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jfdctint-mmi.c +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jfdctint-mmi.c @@ -1,7 +1,7 @@ /* * Loongson MMI optimizations for libjpeg-turbo * - * Copyright (C) 2014, 2018, D. R. Commander. All Rights Reserved. + * Copyright (C) 2014, 2018, 2020, D. R. Commander. All Rights Reserved. * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. * All Rights Reserved. * Authors: ZhuChen @@ -28,7 +28,7 @@ * 3. This notice may not be removed or altered from any source distribution. */ -/* SLOW INTEGER FORWARD DCT */ +/* ACCURATE INTEGER FORWARD DCT */ #include "jsimd_mmi.h" diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jidctfst-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jidctfst-mmi.c new file mode 100644 index 00000000000..503bb35a3cc --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jidctfst-mmi.c @@ -0,0 +1,395 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright (C) 2014-2015, 2018-2019, D. R. Commander. All Rights Reserved. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: LiuQingfa + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* FAST INTEGER INVERSE DCT */ + +#include "jsimd_mmi.h" + + +#define CONST_BITS 8 +#define PASS1_BITS 2 + +#define FIX_1_082 ((short)277) /* FIX(1.082392200) */ +#define FIX_1_414 ((short)362) /* FIX(1.414213562) */ +#define FIX_1_847 ((short)473) /* FIX(1.847759065) */ +#define FIX_2_613 ((short)669) /* FIX(2.613125930) */ +#define FIX_1_613 ((short)(FIX_2_613 - 256 * 3)) /* FIX(2.613125930) - FIX(1) */ + +#define PRE_MULTIPLY_SCALE_BITS 2 +#define CONST_SHIFT (16 - PRE_MULTIPLY_SCALE_BITS - CONST_BITS) + +enum const_index { + index_PW_F1082, + index_PW_F1414, + index_PW_F1847, + index_PW_MF1613, + index_PB_CENTERJSAMP +}; + +static uint64_t const_value[] = { + _uint64_set1_pi16(FIX_1_082 << CONST_SHIFT), + _uint64_set1_pi16(FIX_1_414 << CONST_SHIFT), + _uint64_set1_pi16(FIX_1_847 << CONST_SHIFT), + _uint64_set1_pi16(-FIX_1_613 << CONST_SHIFT), + _uint64_set1_pi8(CENTERJSAMPLE) +}; + +#define PW_F1414 get_const_value(index_PW_F1414) +#define PW_F1847 get_const_value(index_PW_F1847) +#define PW_MF1613 get_const_value(index_PW_MF1613) +#define PW_F1082 get_const_value(index_PW_F1082) +#define PB_CENTERJSAMP get_const_value(index_PB_CENTERJSAMP) + + +#define test_m32_zero(mm32) (!(*(uint32_t *)&mm32)) +#define test_m64_zero(mm64) (!(*(uint64_t *)&mm64)) + + +#define DO_IDCT_COMMON() { \ + tmp7 = _mm_add_pi16(z11, z13); \ + \ + tmp11 = _mm_sub_pi16(z11, z13); \ + tmp11 = _mm_slli_pi16(tmp11, PRE_MULTIPLY_SCALE_BITS); \ + tmp11 = _mm_mulhi_pi16(tmp11, PW_F1414); \ + \ + tmp10 = _mm_slli_pi16(z12, PRE_MULTIPLY_SCALE_BITS); \ + tmp12 = _mm_slli_pi16(z10, PRE_MULTIPLY_SCALE_BITS); \ + \ + /* To avoid overflow... \ + * \ + * (Original) \ + * tmp12 = -2.613125930 * z10 + z5; \ + * \ + * (This implementation) \ + * tmp12 = (-1.613125930 - 1) * z10 + z5; \ + * = -1.613125930 * z10 - z10 + z5; \ + */ \ + \ + z5 = _mm_add_pi16(tmp10, tmp12); \ + z5 = _mm_mulhi_pi16(z5, PW_F1847); \ + \ + tmp10 = _mm_mulhi_pi16(tmp10, PW_F1082); \ + tmp10 = _mm_sub_pi16(tmp10, z5); \ + tmp12 = _mm_mulhi_pi16(tmp12, PW_MF1613); \ + tmp12 = _mm_sub_pi16(tmp12, z10); \ + tmp12 = _mm_sub_pi16(tmp12, z10); \ + tmp12 = _mm_sub_pi16(tmp12, z10); \ + tmp12 = _mm_add_pi16(tmp12, z5); \ + \ + /* Final output stage */ \ + \ + tmp6 = _mm_sub_pi16(tmp12, tmp7); \ + tmp5 = _mm_sub_pi16(tmp11, tmp6); \ + tmp4 = _mm_add_pi16(tmp10, tmp5); \ + \ + out0 = _mm_add_pi16(tmp0, tmp7); \ + out7 = _mm_sub_pi16(tmp0, tmp7); \ + out1 = _mm_add_pi16(tmp1, tmp6); \ + out6 = _mm_sub_pi16(tmp1, tmp6); \ + \ + out2 = _mm_add_pi16(tmp2, tmp5); \ + out5 = _mm_sub_pi16(tmp2, tmp5); \ + out4 = _mm_add_pi16(tmp3, tmp4); \ + out3 = _mm_sub_pi16(tmp3, tmp4); \ +} + +#define DO_IDCT_PASS1(iter) { \ + __m64 col0l, col1l, col2l, col3l, col4l, col5l, col6l, col7l; \ + __m64 quant0l, quant1l, quant2l, quant3l; \ + __m64 quant4l, quant5l, quant6l, quant7l; \ + __m64 row01a, row01b, row01c, row01d, row23a, row23b, row23c, row23d; \ + __m64 row0l, row0h, row1l, row1h, row2l, row2h, row3l, row3h; \ + __m32 col0a, col1a, mm0; \ + \ + col0a = _mm_load_si32((__m32 *)&inptr[DCTSIZE * 1]); \ + col1a = _mm_load_si32((__m32 *)&inptr[DCTSIZE * 2]); \ + mm0 = _mm_or_si32(col0a, col1a); \ + \ + if (test_m32_zero(mm0)) { \ + __m64 mm1, mm2; \ + \ + col0l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 0]); \ + col1l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 1]); \ + col2l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 2]); \ + col3l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 3]); \ + col4l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 4]); \ + col5l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 5]); \ + col6l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 6]); \ + col7l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 7]); \ + \ + mm1 = _mm_or_si64(col1l, col3l); \ + mm2 = _mm_or_si64(col2l, col4l); \ + mm1 = _mm_or_si64(mm1, col5l); \ + mm2 = _mm_or_si64(mm2, col6l); \ + mm1 = _mm_or_si64(mm1, col7l); \ + mm1 = _mm_or_si64(mm1, mm2); \ + \ + if (test_m64_zero(mm1)) { \ + __m64 dcval, dcvall, dcvalh, row0, row1, row2, row3; \ + \ + /* AC terms all zero */ \ + \ + quant0l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 0]); \ + \ + dcval = _mm_mullo_pi16(col0l, quant0l); /* dcval=(00 10 20 30) */ \ + \ + dcvall = _mm_unpacklo_pi16(dcval, dcval); /* dcvall=(00 00 10 10) */ \ + dcvalh = _mm_unpackhi_pi16(dcval, dcval); /* dcvalh=(20 20 30 30) */ \ + \ + row0 = _mm_unpacklo_pi32(dcvall, dcvall); /* row0=(00 00 00 00) */ \ + row1 = _mm_unpackhi_pi32(dcvall, dcvall); /* row1=(10 10 10 10) */ \ + row2 = _mm_unpacklo_pi32(dcvalh, dcvalh); /* row2=(20 20 20 20) */ \ + row3 = _mm_unpackhi_pi32(dcvalh, dcvalh); /* row3=(30 30 30 30) */ \ + \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 0], row0); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 0 + 4], row0); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 1], row1); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 1 + 4], row1); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 2], row2); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 2 + 4], row2); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 3], row3); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 3 + 4], row3); \ + \ + goto nextcolumn##iter; \ + } \ + } \ + \ + /* Even part */ \ + \ + col0l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 0]); /* (00 10 20 30) */ \ + col2l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 2]); /* (02 12 22 32) */ \ + col4l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 4]); /* (04 14 24 34) */ \ + col6l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 6]); /* (06 16 26 36) */ \ + \ + quant0l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 0]); \ + quant2l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 2]); \ + quant4l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 4]); \ + quant6l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 6]); \ + \ + tmp0 = _mm_mullo_pi16(col0l, quant0l); \ + tmp1 = _mm_mullo_pi16(col2l, quant2l); \ + tmp2 = _mm_mullo_pi16(col4l, quant4l); \ + tmp3 = _mm_mullo_pi16(col6l, quant6l); \ + \ + tmp10 = _mm_add_pi16(tmp0, tmp2); \ + tmp11 = _mm_sub_pi16(tmp0, tmp2); \ + tmp13 = _mm_add_pi16(tmp1, tmp3); \ + \ + tmp12 = _mm_sub_pi16(tmp1, tmp3); \ + tmp12 = _mm_slli_pi16(tmp12, PRE_MULTIPLY_SCALE_BITS); \ + tmp12 = _mm_mulhi_pi16(tmp12, PW_F1414); \ + tmp12 = _mm_sub_pi16(tmp12, tmp13); \ + \ + tmp0 = _mm_add_pi16(tmp10, tmp13); \ + tmp3 = _mm_sub_pi16(tmp10, tmp13); \ + tmp1 = _mm_add_pi16(tmp11, tmp12); \ + tmp2 = _mm_sub_pi16(tmp11, tmp12); \ + \ + /* Odd part */ \ + \ + col1l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 1]); /* (01 11 21 31) */ \ + col3l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 3]); /* (03 13 23 33) */ \ + col5l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 5]); /* (05 15 25 35) */ \ + col7l = _mm_load_si64((__m64 *)&inptr[DCTSIZE * 7]); /* (07 17 27 37) */ \ + \ + quant1l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 1]); \ + quant3l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 3]); \ + quant5l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 5]); \ + quant7l = _mm_load_si64((__m64 *)&quantptr[DCTSIZE * 7]); \ + \ + tmp4 = _mm_mullo_pi16(col1l, quant1l); \ + tmp5 = _mm_mullo_pi16(col3l, quant3l); \ + tmp6 = _mm_mullo_pi16(col5l, quant5l); \ + tmp7 = _mm_mullo_pi16(col7l, quant7l); \ + \ + z13 = _mm_add_pi16(tmp6, tmp5); \ + z10 = _mm_sub_pi16(tmp6, tmp5); \ + z11 = _mm_add_pi16(tmp4, tmp7); \ + z12 = _mm_sub_pi16(tmp4, tmp7); \ + \ + DO_IDCT_COMMON() \ + \ + /* out0=(00 10 20 30), out1=(01 11 21 31) */ \ + /* out2=(02 12 22 32), out3=(03 13 23 33) */ \ + /* out4=(04 14 24 34), out5=(05 15 25 35) */ \ + /* out6=(06 16 26 36), out7=(07 17 27 37) */ \ + \ + /* Transpose coefficients */ \ + \ + row01a = _mm_unpacklo_pi16(out0, out1); /* row01a=(00 01 10 11) */ \ + row23a = _mm_unpackhi_pi16(out0, out1); /* row23a=(20 21 30 31) */ \ + row01d = _mm_unpacklo_pi16(out6, out7); /* row01d=(06 07 16 17) */ \ + row23d = _mm_unpackhi_pi16(out6, out7); /* row23d=(26 27 36 37) */ \ + \ + row01b = _mm_unpacklo_pi16(out2, out3); /* row01b=(02 03 12 13) */ \ + row23b = _mm_unpackhi_pi16(out2, out3); /* row23b=(22 23 32 33) */ \ + row01c = _mm_unpacklo_pi16(out4, out5); /* row01c=(04 05 14 15) */ \ + row23c = _mm_unpackhi_pi16(out4, out5); /* row23c=(24 25 34 35) */ \ + \ + row0l = _mm_unpacklo_pi32(row01a, row01b); /* row0l=(00 01 02 03) */ \ + row1l = _mm_unpackhi_pi32(row01a, row01b); /* row1l=(10 11 12 13) */ \ + row2l = _mm_unpacklo_pi32(row23a, row23b); /* row2l=(20 21 22 23) */ \ + row3l = _mm_unpackhi_pi32(row23a, row23b); /* row3l=(30 31 32 33) */ \ + \ + row0h = _mm_unpacklo_pi32(row01c, row01d); /* row0h=(04 05 06 07) */ \ + row1h = _mm_unpackhi_pi32(row01c, row01d); /* row1h=(14 15 16 17) */ \ + row2h = _mm_unpacklo_pi32(row23c, row23d); /* row2h=(24 25 26 27) */ \ + row3h = _mm_unpackhi_pi32(row23c, row23d); /* row3h=(34 35 36 37) */ \ + \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 0], row0l); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 0 + 4], row0h); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 1], row1l); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 1 + 4], row1h); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 2], row2l); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 2 + 4], row2h); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 3], row3l); \ + _mm_store_si64((__m64 *)&wsptr[DCTSIZE * 3 + 4], row3h); \ +} + +#define DO_IDCT_PASS2(ctr) { \ + __m64 row0l, row1l, row2l, row3l, row4l, row5l, row6l, row7l; \ + __m64 col0123a, col0123b, col0123c, col0123d; \ + __m64 col01l, col01h, col23l, col23h; \ + __m64 col0, col1, col2, col3; \ + __m64 row06, row17, row24, row35; \ + \ + row0l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 0]); /* (00 01 02 03) */ \ + row1l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 1]); /* (10 11 12 13) */ \ + row2l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 2]); /* (20 21 22 23) */ \ + row3l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 3]); /* (30 31 32 33) */ \ + row4l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 4]); /* (40 41 42 43) */ \ + row5l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 5]); /* (50 51 52 53) */ \ + row6l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 6]); /* (60 61 62 63) */ \ + row7l = _mm_load_si64((__m64 *)&wsptr[DCTSIZE * 7]); /* (70 71 72 73) */ \ + \ + /* Even part */ \ + \ + tmp10 = _mm_add_pi16(row0l, row4l); \ + tmp11 = _mm_sub_pi16(row0l, row4l); \ + tmp13 = _mm_add_pi16(row2l, row6l); \ + \ + tmp12 = _mm_sub_pi16(row2l, row6l); \ + tmp12 = _mm_slli_pi16(tmp12, PRE_MULTIPLY_SCALE_BITS); \ + tmp12 = _mm_mulhi_pi16(tmp12, PW_F1414); \ + tmp12 = _mm_sub_pi16(tmp12, tmp13); \ + \ + tmp0 = _mm_add_pi16(tmp10, tmp13); \ + tmp3 = _mm_sub_pi16(tmp10, tmp13); \ + tmp1 = _mm_add_pi16(tmp11, tmp12); \ + tmp2 = _mm_sub_pi16(tmp11, tmp12); \ + \ + /* Odd part */ \ + \ + z13 = _mm_add_pi16(row5l, row3l); \ + z10 = _mm_sub_pi16(row5l, row3l); \ + z11 = _mm_add_pi16(row1l, row7l); \ + z12 = _mm_sub_pi16(row1l, row7l); \ + \ + DO_IDCT_COMMON() \ + \ + /* out0=(00 01 02 03), out1=(10 11 12 13) */ \ + /* out2=(20 21 22 23), out3=(30 31 32 33) */ \ + /* out4=(40 41 42 43), out5=(50 51 52 53) */ \ + /* out6=(60 61 62 63), out7=(70 71 72 73) */ \ + \ + out0 = _mm_srai_pi16(out0, PASS1_BITS + 3); \ + out1 = _mm_srai_pi16(out1, PASS1_BITS + 3); \ + out2 = _mm_srai_pi16(out2, PASS1_BITS + 3); \ + out3 = _mm_srai_pi16(out3, PASS1_BITS + 3); \ + out4 = _mm_srai_pi16(out4, PASS1_BITS + 3); \ + out5 = _mm_srai_pi16(out5, PASS1_BITS + 3); \ + out6 = _mm_srai_pi16(out6, PASS1_BITS + 3); \ + out7 = _mm_srai_pi16(out7, PASS1_BITS + 3); \ + \ + row06 = _mm_packs_pi16(out0, out6); /* row06=(00 01 02 03 60 61 62 63) */ \ + row17 = _mm_packs_pi16(out1, out7); /* row17=(10 11 12 13 70 71 72 73) */ \ + row24 = _mm_packs_pi16(out2, out4); /* row24=(20 21 22 23 40 41 42 43) */ \ + row35 = _mm_packs_pi16(out3, out5); /* row35=(30 31 32 33 50 51 52 53) */ \ + \ + row06 = _mm_add_pi8(row06, PB_CENTERJSAMP); \ + row17 = _mm_add_pi8(row17, PB_CENTERJSAMP); \ + row24 = _mm_add_pi8(row24, PB_CENTERJSAMP); \ + row35 = _mm_add_pi8(row35, PB_CENTERJSAMP); \ + \ + /* Transpose coefficients */ \ + \ + col0123a = _mm_unpacklo_pi8(row06, row17); /* col0123a=(00 10 01 11 02 12 03 13) */ \ + col0123d = _mm_unpackhi_pi8(row06, row17); /* col0123d=(60 70 61 71 62 72 63 73) */ \ + col0123b = _mm_unpacklo_pi8(row24, row35); /* col0123b=(20 30 21 31 22 32 23 33) */ \ + col0123c = _mm_unpackhi_pi8(row24, row35); /* col0123c=(40 50 41 51 42 52 43 53) */ \ + \ + col01l = _mm_unpacklo_pi16(col0123a, col0123b); /* col01l=(00 10 20 30 01 11 21 31) */ \ + col23l = _mm_unpackhi_pi16(col0123a, col0123b); /* col23l=(02 12 22 32 03 13 23 33) */ \ + col01h = _mm_unpacklo_pi16(col0123c, col0123d); /* col01h=(40 50 60 70 41 51 61 71) */ \ + col23h = _mm_unpackhi_pi16(col0123c, col0123d); /* col23h=(42 52 62 72 43 53 63 73) */ \ + \ + col0 = _mm_unpacklo_pi32(col01l, col01h); /* col0=(00 10 20 30 40 50 60 70) */ \ + col1 = _mm_unpackhi_pi32(col01l, col01h); /* col1=(01 11 21 31 41 51 61 71) */ \ + col2 = _mm_unpacklo_pi32(col23l, col23h); /* col2=(02 12 22 32 42 52 62 72) */ \ + col3 = _mm_unpackhi_pi32(col23l, col23h); /* col3=(03 13 23 33 43 53 63 73) */ \ + \ + _mm_store_si64((__m64 *)(output_buf[ctr + 0] + output_col), col0); \ + _mm_store_si64((__m64 *)(output_buf[ctr + 1] + output_col), col1); \ + _mm_store_si64((__m64 *)(output_buf[ctr + 2] + output_col), col2); \ + _mm_store_si64((__m64 *)(output_buf[ctr + 3] + output_col), col3); \ +} + +void jsimd_idct_ifast_mmi(void *dct_table, JCOEFPTR coef_block, + JSAMPARRAY output_buf, JDIMENSION output_col) +{ + __m64 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7; + __m64 tmp10, tmp11, tmp12, tmp13; + __m64 out0, out1, out2, out3, out4, out5, out6, out7; + __m64 z5, z10, z11, z12, z13; + JCOEFPTR inptr; + ISLOW_MULT_TYPE *quantptr; + JCOEF *wsptr; + JCOEF workspace[DCTSIZE2]; /* buffers data between passes */ + + /* Pass 1: process columns. */ + + inptr = coef_block; + quantptr = (ISLOW_MULT_TYPE *)dct_table; + wsptr = workspace; + + DO_IDCT_PASS1(1) +nextcolumn1: + inptr += 4; + quantptr += 4; + wsptr += DCTSIZE * 4; + DO_IDCT_PASS1(2) +nextcolumn2: + + /* Pass 2: process rows. */ + + wsptr = workspace; + + DO_IDCT_PASS2(0) + wsptr += 4; + DO_IDCT_PASS2(4) +} diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jidctint-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jidctint-mmi.c similarity index 99% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jidctint-mmi.c rename to third-party/mozjpeg/mozjpeg/simd/mips64/jidctint-mmi.c index 419c638db6e..cd3db980c5d 100644 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jidctint-mmi.c +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jidctint-mmi.c @@ -1,7 +1,7 @@ /* * Loongson MMI optimizations for libjpeg-turbo * - * Copyright (C) 2014-2015, 2018, D. R. Commander. All Rights Reserved. + * Copyright (C) 2014-2015, 2018, 2020, D. R. Commander. All Rights Reserved. * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. * All Rights Reserved. * Authors: ZhuChen @@ -28,7 +28,7 @@ * 3. This notice may not be removed or altered from any source distribution. */ -/* SLOW INTEGER INVERSE DCT */ +/* ACCUATE INTEGER INVERSE DCT */ #include "jsimd_mmi.h" diff --git a/third-party/mozjpeg/mozjpeg/simd/mips64/jquanti-mmi.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jquanti-mmi.c new file mode 100644 index 00000000000..339002fd804 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jquanti-mmi.c @@ -0,0 +1,124 @@ +/* + * Loongson MMI optimizations for libjpeg-turbo + * + * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. + * All Rights Reserved. + * Authors: ZhuChen + * CaiWanwei + * SunZhangzhi + * Copyright (C) 2018-2019, D. R. Commander. All Rights Reserved. + * + * Based on the x86 SIMD extension for IJG JPEG library + * Copyright (C) 1999-2006, MIYASAKA Masaru. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* INTEGER QUANTIZATION AND SAMPLE CONVERSION */ + +#include "jsimd_mmi.h" + + +#define DO_QUANT() { \ + __m64 rowl, rowh, rowls, rowhs, rowlsave, rowhsave; \ + __m64 corrl, corrh, recipl, reciph, scalel, scaleh; \ + \ + rowl = _mm_load_si64((__m64 *)&workspace[0]); \ + rowh = _mm_load_si64((__m64 *)&workspace[4]); \ + \ + /* Branch-less absolute value */ \ + rowls = _mm_srai_pi16(rowl, (WORD_BIT - 1)); /* -1 if value < 0, */ \ + /* 0 otherwise */ \ + rowhs = _mm_srai_pi16(rowh, (WORD_BIT - 1)); \ + \ + rowl = _mm_xor_si64(rowl, rowls); /* val = -val */ \ + rowh = _mm_xor_si64(rowh, rowhs); \ + rowl = _mm_sub_pi16(rowl, rowls); \ + rowh = _mm_sub_pi16(rowh, rowhs); \ + \ + corrl = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 1]); /* correction */ \ + corrh = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 1 + 4]); \ + \ + rowlsave = rowl = _mm_add_pi16(rowl, corrl); /* correction + roundfactor */ \ + rowhsave = rowh = _mm_add_pi16(rowh, corrh); \ + \ + recipl = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 0]); /* reciprocal */ \ + reciph = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 0 + 4]); \ + \ + rowl = _mm_mulhi_pi16(rowl, recipl); \ + rowh = _mm_mulhi_pi16(rowh, reciph); \ + \ + /* reciprocal is always negative (MSB=1), so we always need to add the */ \ + /* initial value (input value is never negative as we inverted it at the */ \ + /* start of this routine) */ \ + rowlsave = rowl = _mm_add_pi16(rowl, rowlsave); \ + rowhsave = rowh = _mm_add_pi16(rowh, rowhsave); \ + \ + scalel = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 2]); /* scale */ \ + scaleh = _mm_load_si64((__m64 *)&divisors[DCTSIZE2 * 2 + 4]); \ + \ + rowl = _mm_mulhi_pi16(rowl, scalel); \ + rowh = _mm_mulhi_pi16(rowh, scaleh); \ + \ + /* determine if scale is negative */ \ + scalel = _mm_srai_pi16(scalel, (WORD_BIT - 1)); \ + scaleh = _mm_srai_pi16(scaleh, (WORD_BIT - 1)); \ + \ + /* and add input if it is */ \ + scalel = _mm_and_si64(scalel, rowlsave); \ + scaleh = _mm_and_si64(scaleh, rowhsave); \ + rowl = _mm_add_pi16(rowl, scalel); \ + rowh = _mm_add_pi16(rowh, scaleh); \ + \ + /* then check if negative input */ \ + rowlsave = _mm_srai_pi16(rowlsave, (WORD_BIT - 1)); \ + rowhsave = _mm_srai_pi16(rowhsave, (WORD_BIT - 1)); \ + \ + /* and add scale if it is */ \ + rowlsave = _mm_and_si64(rowlsave, scalel); \ + rowhsave = _mm_and_si64(rowhsave, scaleh); \ + rowl = _mm_add_pi16(rowl, rowlsave); \ + rowh = _mm_add_pi16(rowh, rowhsave); \ + \ + rowl = _mm_xor_si64(rowl, rowls); /* val = -val */ \ + rowh = _mm_xor_si64(rowh, rowhs); \ + rowl = _mm_sub_pi16(rowl, rowls); \ + rowh = _mm_sub_pi16(rowh, rowhs); \ + \ + _mm_store_si64((__m64 *)&output_ptr[0], rowl); \ + _mm_store_si64((__m64 *)&output_ptr[4], rowh); \ + \ + workspace += DCTSIZE; \ + divisors += DCTSIZE; \ + output_ptr += DCTSIZE; \ +} + + +void jsimd_quantize_mmi(JCOEFPTR coef_block, DCTELEM *divisors, + DCTELEM *workspace) +{ + JCOEFPTR output_ptr = coef_block; + + DO_QUANT() + DO_QUANT() + DO_QUANT() + DO_QUANT() + DO_QUANT() + DO_QUANT() + DO_QUANT() + DO_QUANT() +} diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jsimd.c b/third-party/mozjpeg/mozjpeg/simd/mips64/jsimd.c similarity index 64% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jsimd.c rename to third-party/mozjpeg/mozjpeg/simd/mips64/jsimd.c index e8b18322537..917440b43bf 100644 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jsimd.c +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jsimd.c @@ -1,11 +1,11 @@ /* - * jsimd_loongson.c + * jsimd_mips64.c * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009-2011, 2014, 2016, 2018, D. R. Commander. + * Copyright (C) 2009-2011, 2014, 2016, 2018, 2022, D. R. Commander. * Copyright (C) 2013-2014, MIPS Technologies, Inc., California. - * Copyright (C) 2015, 2018, Matthieu Darbois. - * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. + * Copyright (C) 2015, 2018, 2022, Matthieu Darbois. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -13,7 +13,7 @@ * * This file contains the interface between the "normal" portions * of the library and the SIMD implementations when running on a - * Loongson architecture. + * 64-bit MIPS architecture. */ #define JPEG_INTERNALS @@ -24,12 +24,76 @@ #include "../../jsimddct.h" #include "../jsimd.h" -static unsigned int simd_support = ~0; +#include + +static THREAD_LOCAL unsigned int simd_support = ~0; + +#if defined(__linux__) + +#define SOMEWHAT_SANE_PROC_CPUINFO_SIZE_LIMIT (1024 * 1024) + +LOCAL(int) +check_feature(char *buffer, char *feature) +{ + char *p; + + if (*feature == 0) + return 0; + if (strncmp(buffer, "ASEs implemented", 16) != 0) + return 0; + buffer += 16; + while (isspace(*buffer)) + buffer++; + + /* Check if 'feature' is present in the buffer as a separate word */ + while ((p = strstr(buffer, feature))) { + if (p > buffer && !isspace(*(p - 1))) { + buffer++; + continue; + } + p += strlen(feature); + if (*p != 0 && !isspace(*p)) { + buffer++; + continue; + } + return 1; + } + return 0; +} + +LOCAL(int) +parse_proc_cpuinfo(int bufsize) +{ + char *buffer = (char *)malloc(bufsize); + FILE *fd; + + simd_support = 0; + + if (!buffer) + return 0; + + fd = fopen("/proc/cpuinfo", "r"); + if (fd) { + while (fgets(buffer, bufsize, fd)) { + if (!strchr(buffer, '\n') && !feof(fd)) { + /* "impossible" happened - insufficient size of the buffer! */ + fclose(fd); + free(buffer); + return 0; + } + if (check_feature(buffer, "loongson-mmi")) + simd_support |= JSIMD_MMI; + } + fclose(fd); + } + free(buffer); + return 1; +} + +#endif /* * Check what SIMD accelerations are supported. - * - * FIXME: This code is racy under a multi-threaded environment. */ LOCAL(void) init_simd(void) @@ -37,14 +101,32 @@ init_simd(void) #ifndef NO_GETENV char *env = NULL; #endif +#if defined(__linux__) + int bufsize = 1024; /* an initial guess for the line buffer size limit */ +#endif if (simd_support != ~0U) return; + simd_support = 0; + +#if defined(__linux__) + while (!parse_proc_cpuinfo(bufsize)) { + bufsize *= 2; + if (bufsize > SOMEWHAT_SANE_PROC_CPUINFO_SIZE_LIMIT) + break; + } +#elif defined(__mips_loongson_vector_rev) + /* Only enable MMI by default on non-Linux platforms when the compiler flags + * support it. */ simd_support |= JSIMD_MMI; +#endif #ifndef NO_GETENV /* Force different settings through environment variables */ + env = getenv("JSIMD_FORCEMMI"); + if ((env != NULL) && (strcmp(env, "1") == 0)) + simd_support = JSIMD_MMI; env = getenv("JSIMD_FORCENONE"); if ((env != NULL) && (strcmp(env, "1") == 0)) simd_support = 0; @@ -73,6 +155,19 @@ jsimd_can_rgb_ycc(void) GLOBAL(int) jsimd_can_rgb_gray(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + if ((RGB_PIXELSIZE != 3) && (RGB_PIXELSIZE != 4)) + return 0; + + if (simd_support & JSIMD_MMI) + return 1; + return 0; } @@ -150,6 +245,37 @@ jsimd_rgb_gray_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, JSAMPIMAGE output_buf, JDIMENSION output_row, int num_rows) { + void (*mmifct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); + + switch (cinfo->in_color_space) { + case JCS_EXT_RGB: + mmifct = jsimd_extrgb_gray_convert_mmi; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + mmifct = jsimd_extrgbx_gray_convert_mmi; + break; + case JCS_EXT_BGR: + mmifct = jsimd_extbgr_gray_convert_mmi; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + mmifct = jsimd_extbgrx_gray_convert_mmi; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + mmifct = jsimd_extxbgr_gray_convert_mmi; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + mmifct = jsimd_extxrgb_gray_convert_mmi; + break; + default: + mmifct = jsimd_rgb_gray_convert_mmi; + break; + } + + mmifct(cinfo->image_width, input_buf, output_buf, output_row, num_rows); } GLOBAL(void) @@ -311,6 +437,17 @@ jsimd_can_h2v2_fancy_upsample(void) GLOBAL(int) jsimd_can_h2v1_fancy_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_MMI) + return 1; + return 0; } @@ -327,17 +464,42 @@ GLOBAL(void) jsimd_h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + jsimd_h2v1_fancy_upsample_mmi(cinfo->max_v_samp_factor, + compptr->downsampled_width, input_data, + output_data_ptr); } GLOBAL(int) jsimd_can_h2v2_merged_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_MMI) + return 1; + return 0; } GLOBAL(int) jsimd_can_h2v1_merged_upsample(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + + if (simd_support & JSIMD_MMI) + return 1; + return 0; } @@ -345,12 +507,74 @@ GLOBAL(void) jsimd_h2v2_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { + void (*mmifct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + + switch (cinfo->out_color_space) { + case JCS_EXT_RGB: + mmifct = jsimd_h2v2_extrgb_merged_upsample_mmi; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + mmifct = jsimd_h2v2_extrgbx_merged_upsample_mmi; + break; + case JCS_EXT_BGR: + mmifct = jsimd_h2v2_extbgr_merged_upsample_mmi; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + mmifct = jsimd_h2v2_extbgrx_merged_upsample_mmi; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + mmifct = jsimd_h2v2_extxbgr_merged_upsample_mmi; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + mmifct = jsimd_h2v2_extxrgb_merged_upsample_mmi; + break; + default: + mmifct = jsimd_h2v2_merged_upsample_mmi; + break; + } + + mmifct(cinfo->output_width, input_buf, in_row_group_ctr, output_buf); } GLOBAL(void) jsimd_h2v1_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, JDIMENSION in_row_group_ctr, JSAMPARRAY output_buf) { + void (*mmifct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + + switch (cinfo->out_color_space) { + case JCS_EXT_RGB: + mmifct = jsimd_h2v1_extrgb_merged_upsample_mmi; + break; + case JCS_EXT_RGBX: + case JCS_EXT_RGBA: + mmifct = jsimd_h2v1_extrgbx_merged_upsample_mmi; + break; + case JCS_EXT_BGR: + mmifct = jsimd_h2v1_extbgr_merged_upsample_mmi; + break; + case JCS_EXT_BGRX: + case JCS_EXT_BGRA: + mmifct = jsimd_h2v1_extbgrx_merged_upsample_mmi; + break; + case JCS_EXT_XBGR: + case JCS_EXT_ABGR: + mmifct = jsimd_h2v1_extxbgr_merged_upsample_mmi; + break; + case JCS_EXT_XRGB: + case JCS_EXT_ARGB: + mmifct = jsimd_h2v1_extxrgb_merged_upsample_mmi; + break; + default: + mmifct = jsimd_h2v1_merged_upsample_mmi; + break; + } + + mmifct(cinfo->output_width, input_buf, in_row_group_ctr, output_buf); } GLOBAL(int) @@ -397,6 +621,17 @@ jsimd_can_fdct_islow(void) GLOBAL(int) jsimd_can_fdct_ifast(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (DCTSIZE != 8) + return 0; + if (sizeof(DCTELEM) != 2) + return 0; + + if (simd_support & JSIMD_MMI) + return 1; + return 0; } @@ -415,6 +650,7 @@ jsimd_fdct_islow(DCTELEM *data) GLOBAL(void) jsimd_fdct_ifast(DCTELEM *data) { + jsimd_fdct_ifast_mmi(data); } GLOBAL(void) @@ -537,6 +773,25 @@ jsimd_can_idct_islow(void) GLOBAL(int) jsimd_can_idct_ifast(void) { + init_simd(); + + /* The code is optimised for these values only */ + if (DCTSIZE != 8) + return 0; + if (sizeof(JCOEF) != 2) + return 0; + if (BITS_IN_JSAMPLE != 8) + return 0; + if (sizeof(JDIMENSION) != 4) + return 0; + if (sizeof(IFAST_MULT_TYPE) != 2) + return 0; + if (IFAST_SCALE_BITS != 2) + return 0; + + if (simd_support & JSIMD_MMI) + return 1; + return 0; } @@ -559,6 +814,7 @@ jsimd_idct_ifast(j_decompress_ptr cinfo, jpeg_component_info *compptr, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col) { + jsimd_idct_ifast_mmi(compptr->dct_table, coef_block, output_buf, output_col); } GLOBAL(void) @@ -591,7 +847,7 @@ jsimd_can_encode_mcu_AC_first_prepare(void) GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { } @@ -604,7 +860,7 @@ jsimd_can_encode_mcu_AC_refine_prepare(void) GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { return 0; } diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/jsimd_mmi.h b/third-party/mozjpeg/mozjpeg/simd/mips64/jsimd_mmi.h similarity index 72% rename from third-party/mozjpeg/mozjpeg/simd/loongson/jsimd_mmi.h rename to third-party/mozjpeg/mozjpeg/simd/mips64/jsimd_mmi.h index 2506aa86119..5e4261c9d98 100644 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/jsimd_mmi.h +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/jsimd_mmi.h @@ -1,11 +1,12 @@ /* * Loongson MMI optimizations for libjpeg-turbo * - * Copyright (C) 2016-2017, Loongson Technology Corporation Limited, BeiJing. + * Copyright (C) 2016-2018, Loongson Technology Corporation Limited, BeiJing. * All Rights Reserved. * Authors: ZhuChen * CaiWanwei * SunZhangzhi + * QingfaLiu * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages @@ -32,6 +33,13 @@ /* Common code */ +#if defined(_ABI64) && _MIPS_SIM == _ABI64 +# define PTR_ADDU "daddu " +# define PTR_SLL "dsll " +#else +# define PTR_ADDU "addu " +# define PTR_SLL "sll " +#endif #define SIZEOF_MMWORD 8 #define BYTE_BIT 8 @@ -47,11 +55,15 @@ ((uint64_t)(uint8_t)f << 16) | \ ((uint64_t)(uint8_t)g << 8) | \ ((uint64_t)(uint8_t)h)) -#define _uint64_set_pi16(a, b, c, d) (((uint64_t)(uint16_t)a << 48) | \ - ((uint64_t)(uint16_t)b << 32) | \ - ((uint64_t)(uint16_t)c << 16) | \ - ((uint64_t)(uint16_t)d)) -#define _uint64_set_pi32(a, b) (((uint64_t)(uint32_t)a << 32) | \ - ((uint64_t)(uint32_t)b)) +#define _uint64_set1_pi8(a) _uint64_set_pi8(a, a, a, a, a, a, a, a) +#define _uint64_set_pi16(a, b, c, d) \ + (((uint64_t)(uint16_t)a << 48) | \ + ((uint64_t)(uint16_t)b << 32) | \ + ((uint64_t)(uint16_t)c << 16) | \ + ((uint64_t)(uint16_t)d)) +#define _uint64_set1_pi16(a) _uint64_set_pi16(a, a, a, a) +#define _uint64_set_pi32(a, b) \ + (((uint64_t)(uint32_t)a << 32) | \ + ((uint64_t)(uint32_t)b)) #define get_const_value(index) (*(__m64 *)&const_value[index]) diff --git a/third-party/mozjpeg/mozjpeg/simd/loongson/loongson-mmintrin.h b/third-party/mozjpeg/mozjpeg/simd/mips64/loongson-mmintrin.h similarity index 98% rename from third-party/mozjpeg/mozjpeg/simd/loongson/loongson-mmintrin.h rename to third-party/mozjpeg/mozjpeg/simd/mips64/loongson-mmintrin.h index 50d166b7532..db9b35ab606 100644 --- a/third-party/mozjpeg/mozjpeg/simd/loongson/loongson-mmintrin.h +++ b/third-party/mozjpeg/mozjpeg/simd/mips64/loongson-mmintrin.h @@ -1217,14 +1217,24 @@ _mm_store_pi32(__m32 *dest, __m64 src) extern __inline void FUNCTION_ATTRIBS _mm_store_si64(__m64 *dest, __m64 src) { - asm("gssdlc1 %1, 7+%0\n\t" - "gssdrc1 %1, %0\n\t" + asm("sdc1 %1, %0 \n\t" : "=m" (*dest) : "f" (src) : "memory" ); } +extern __inline void FUNCTION_ATTRIBS +_mm_storeu_si64(__m64 *dest, __m64 src) +{ + asm("gssdlc1 %1, 7(%0) \n\t" + "gssdrc1 %1, 0(%0) \n\t" + : + : "r" (dest), "f" (src) + : "memory" + ); +} + extern __inline __m64 FUNCTION_ATTRIBS _mm_load_si32(const __m32 *src) { diff --git a/third-party/mozjpeg/mozjpeg/simd/nasm/jpeg_nbits_table.inc b/third-party/mozjpeg/mozjpeg/simd/nasm/jpeg_nbits_table.inc deleted file mode 100644 index 2ce6c284d9f..00000000000 --- a/third-party/mozjpeg/mozjpeg/simd/nasm/jpeg_nbits_table.inc +++ /dev/null @@ -1,4097 +0,0 @@ -jpeg_nbits_table db \ - 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, \ - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, \ - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, \ - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, \ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, \ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, \ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, \ - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, \ - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 diff --git a/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdcfg.inc.h b/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdcfg.inc.h index 7ff7e29296d..bf2a45ad50c 100644 --- a/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdcfg.inc.h +++ b/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdcfg.inc.h @@ -1,8 +1,10 @@ -// This file generates the include file for the assembly -// implementations by abusing the C preprocessor. -// -// Note: Some things are manually defined as they need to -// be mapped to NASM types. +/* + * This file generates the include file for the assembly + * implementations by abusing the C preprocessor. + * + * Note: Some things are manually defined as they need to + * be mapped to NASM types. + */ ; ; Automatically generated include file from jsimdcfg.inc.h diff --git a/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdext.inc b/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdext.inc index 9930d80c2ab..e8d50b03497 100644 --- a/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdext.inc +++ b/third-party/mozjpeg/mozjpeg/simd/nasm/jsimdext.inc @@ -2,8 +2,9 @@ ; jsimdext.inc - common declarations ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2010, 2016, 2019, D. R. Commander. +; Copyright (C) 2010, 2016, 2018-2019, D. R. Commander. ; Copyright (C) 2018, Matthieu Darbois. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library - version 1.02 ; @@ -130,13 +131,53 @@ section .note.GNU-stack noalloc noexec nowrite progbits ; Common types ; %ifdef __x86_64__ +%ifnidn __OUTPUT_FORMAT__, elfx32 %define POINTER qword ; general pointer type %define SIZEOF_POINTER SIZEOF_QWORD ; sizeof(POINTER) %define POINTER_BIT QWORD_BIT ; sizeof(POINTER)*BYTE_BIT -%else +%define resp resq +%define dp dq +%define raxp rax +%define rbxp rbx +%define rcxp rcx +%define rdxp rdx +%define rsip rsi +%define rdip rdi +%define rbpp rbp +%define rspp rsp +%define r8p r8 +%define r9p r9 +%define r10p r10 +%define r11p r11 +%define r12p r12 +%define r13p r13 +%define r14p r14 +%define r15p r15 +%endif +%endif +%ifndef raxp %define POINTER dword ; general pointer type %define SIZEOF_POINTER SIZEOF_DWORD ; sizeof(POINTER) %define POINTER_BIT DWORD_BIT ; sizeof(POINTER)*BYTE_BIT +%define resp resd +%define dp dd +; x86_64 ILP32 ABI (x32) +%define raxp eax +%define rbxp ebx +%define rcxp ecx +%define rdxp edx +%define rsip esi +%define rdip edi +%define rbpp ebp +%define rspp esp +%define r8p r8d +%define r9p r9d +%define r10p r10d +%define r11p r11d +%define r12p r12d +%define r13p r13d +%define r14p r14d +%define r15p r15d %endif %define INT dword ; signed integer type diff --git a/third-party/mozjpeg/mozjpeg/simd/powerpc/jcsample.h b/third-party/mozjpeg/mozjpeg/simd/powerpc/jcsample.h index 2ac48167fc2..bd07fcc4ed4 100644 --- a/third-party/mozjpeg/mozjpeg/simd/powerpc/jcsample.h +++ b/third-party/mozjpeg/mozjpeg/simd/powerpc/jcsample.h @@ -20,7 +20,7 @@ expand_right_edge(JSAMPARRAY image_data, int num_rows, JDIMENSION input_cols, if (numcols > 0) { for (row = 0; row < num_rows; row++) { ptr = image_data[row] + input_cols; - pixval = ptr[-1]; /* don't need GETJSAMPLE() here */ + pixval = ptr[-1]; for (count = numcols; count > 0; count--) *ptr++ = pixval; } diff --git a/third-party/mozjpeg/mozjpeg/simd/powerpc/jfdctint-altivec.c b/third-party/mozjpeg/mozjpeg/simd/powerpc/jfdctint-altivec.c index 6e63cc1e721..3d4f017103d 100644 --- a/third-party/mozjpeg/mozjpeg/simd/powerpc/jfdctint-altivec.c +++ b/third-party/mozjpeg/mozjpeg/simd/powerpc/jfdctint-altivec.c @@ -1,7 +1,7 @@ /* * AltiVec optimizations for libjpeg-turbo * - * Copyright (C) 2014, D. R. Commander. All Rights Reserved. + * Copyright (C) 2014, 2020, D. R. Commander. All Rights Reserved. * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages @@ -20,7 +20,7 @@ * 3. This notice may not be removed or altered from any source distribution. */ -/* SLOW INTEGER FORWARD DCT */ +/* ACCURATE INTEGER FORWARD DCT */ #include "jsimd_altivec.h" diff --git a/third-party/mozjpeg/mozjpeg/simd/powerpc/jidctint-altivec.c b/third-party/mozjpeg/mozjpeg/simd/powerpc/jidctint-altivec.c index 0e5dd58ccc8..60e619f11db 100644 --- a/third-party/mozjpeg/mozjpeg/simd/powerpc/jidctint-altivec.c +++ b/third-party/mozjpeg/mozjpeg/simd/powerpc/jidctint-altivec.c @@ -1,7 +1,7 @@ /* * AltiVec optimizations for libjpeg-turbo * - * Copyright (C) 2014-2015, D. R. Commander. All Rights Reserved. + * Copyright (C) 2014-2015, 2020, D. R. Commander. All Rights Reserved. * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages @@ -20,7 +20,7 @@ * 3. This notice may not be removed or altered from any source distribution. */ -/* SLOW INTEGER INVERSE DCT */ +/* ACCURATE INTEGER INVERSE DCT */ #include "jsimd_altivec.h" diff --git a/third-party/mozjpeg/mozjpeg/simd/powerpc/jsimd.c b/third-party/mozjpeg/mozjpeg/simd/powerpc/jsimd.c index d0d3981e066..461f603633a 100644 --- a/third-party/mozjpeg/mozjpeg/simd/powerpc/jsimd.c +++ b/third-party/mozjpeg/mozjpeg/simd/powerpc/jsimd.c @@ -2,8 +2,8 @@ * jsimd_powerpc.c * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009-2011, 2014-2016, 2018, D. R. Commander. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2009-2011, 2014-2016, 2018, 2022, D. R. Commander. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -27,17 +27,21 @@ #include "../../jsimddct.h" #include "../jsimd.h" -#include -#include #include -#if defined(__OpenBSD__) +#if defined(__APPLE__) +#include +#include +#elif defined(__OpenBSD__) #include #include #include +#elif defined(__FreeBSD__) +#include +#include #endif -static unsigned int simd_support = ~0; +static THREAD_LOCAL unsigned int simd_support = ~0; #if !defined(__ALTIVEC__) && (defined(__linux__) || defined(ANDROID) || defined(__ANDROID__)) @@ -105,8 +109,6 @@ parse_proc_cpuinfo(int bufsize) /* * Check what SIMD accelerations are supported. - * - * FIXME: This code is racy under a multi-threaded environment. */ LOCAL(void) init_simd(void) @@ -118,10 +120,16 @@ init_simd(void) int bufsize = 1024; /* an initial guess for the line buffer size limit */ #elif defined(__amigaos4__) uint32 altivec = 0; +#elif defined(__APPLE__) + int mib[2] = { CTL_HW, HW_VECTORUNIT }; + int altivec; + size_t len = sizeof(altivec); #elif defined(__OpenBSD__) int mib[2] = { CTL_MACHDEP, CPU_ALTIVEC }; int altivec; size_t len = sizeof(altivec); +#elif defined(__FreeBSD__) + unsigned long cpufeatures = 0; #endif if (simd_support != ~0U) @@ -129,7 +137,7 @@ init_simd(void) simd_support = 0; -#if defined(__ALTIVEC__) || defined(__APPLE__) +#if defined(__ALTIVEC__) simd_support |= JSIMD_ALTIVEC; #elif defined(__linux__) || defined(ANDROID) || defined(__ANDROID__) while (!parse_proc_cpuinfo(bufsize)) { @@ -141,9 +149,13 @@ init_simd(void) IExec->GetCPUInfoTags(GCIT_VectorUnit, &altivec, TAG_DONE); if (altivec == VECTORTYPE_ALTIVEC) simd_support |= JSIMD_ALTIVEC; -#elif defined(__OpenBSD__) +#elif defined(__APPLE__) || defined(__OpenBSD__) if (sysctl(mib, 2, &altivec, &len, NULL, 0) == 0 && altivec != 0) simd_support |= JSIMD_ALTIVEC; +#elif defined(__FreeBSD__) + elf_aux_info(AT_HWCAP, &cpufeatures, sizeof(cpufeatures)); + if (cpufeatures & PPC_FEATURE_HAS_ALTIVEC) + simd_support |= JSIMD_ALTIVEC; #endif #ifndef NO_GETENV @@ -853,7 +865,7 @@ jsimd_can_encode_mcu_AC_first_prepare(void) GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { } @@ -866,7 +878,7 @@ jsimd_can_encode_mcu_AC_refine_prepare(void) GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { return 0; } diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-avx2.asm index 10d28348a96..ffb527db00e 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-avx2.asm @@ -3,6 +3,7 @@ ; ; Copyright (C) 2009, 2016, D. R. Commander. ; Copyright (C) 2015, Intel Corporation. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -57,9 +58,9 @@ EXTN(jsimd_rgb_ycc_convert_avx2): mov rsi, r12 mov ecx, r13d - mov rdi, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rsi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rsi+2*SIZEOF_JSAMPARRAY] + mov rdip, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rsi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rsi+2*SIZEOF_JSAMPARRAY] lea rdi, [rdi+rcx*SIZEOF_JSAMPROW] lea rbx, [rbx+rcx*SIZEOF_JSAMPROW] lea rdx, [rdx+rcx*SIZEOF_JSAMPROW] @@ -77,10 +78,10 @@ EXTN(jsimd_rgb_ycc_convert_avx2): push rsi push rcx ; col - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr0 - mov rbx, JSAMPROW [rbx] ; outptr1 - mov rdx, JSAMPROW [rdx] ; outptr2 + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr0 + mov rbxp, JSAMPROW [rbx] ; outptr1 + mov rdxp, JSAMPROW [rdx] ; outptr2 cmp rcx, byte SIZEOF_YMMWORD jae near .columnloop diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-sse2.asm index 2c914d31838..af70ed6010f 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jccolext-sse2.asm @@ -2,6 +2,7 @@ ; jccolext.asm - colorspace conversion (64-bit SSE2) ; ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -56,9 +57,9 @@ EXTN(jsimd_rgb_ycc_convert_sse2): mov rsi, r12 mov ecx, r13d - mov rdi, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rsi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rsi+2*SIZEOF_JSAMPARRAY] + mov rdip, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rsi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rsi+2*SIZEOF_JSAMPARRAY] lea rdi, [rdi+rcx*SIZEOF_JSAMPROW] lea rbx, [rbx+rcx*SIZEOF_JSAMPROW] lea rdx, [rdx+rcx*SIZEOF_JSAMPROW] @@ -76,10 +77,10 @@ EXTN(jsimd_rgb_ycc_convert_sse2): push rsi push rcx ; col - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr0 - mov rbx, JSAMPROW [rbx] ; outptr1 - mov rdx, JSAMPROW [rdx] ; outptr2 + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr0 + mov rbxp, JSAMPROW [rbx] ; outptr1 + mov rdxp, JSAMPROW [rdx] ; outptr2 cmp rcx, byte SIZEOF_XMMWORD jae near .columnloop diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-avx2.asm index 175b60de613..ddcc2c0a2fe 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-avx2.asm @@ -3,6 +3,7 @@ ; ; Copyright (C) 2011, 2016, D. R. Commander. ; Copyright (C) 2015, Intel Corporation. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -57,7 +58,7 @@ EXTN(jsimd_rgb_gray_convert_avx2): mov rsi, r12 mov ecx, r13d - mov rdi, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] + mov rdip, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] lea rdi, [rdi+rcx*SIZEOF_JSAMPROW] pop rcx @@ -71,8 +72,8 @@ EXTN(jsimd_rgb_gray_convert_avx2): push rsi push rcx ; col - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr0 + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr0 cmp rcx, byte SIZEOF_YMMWORD jae near .columnloop diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-sse2.asm index 873be80564a..f1d399a63b8 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcgryext-sse2.asm @@ -2,6 +2,7 @@ ; jcgryext.asm - grayscale colorspace conversion (64-bit SSE2) ; ; Copyright (C) 2011, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -56,7 +57,7 @@ EXTN(jsimd_rgb_gray_convert_sse2): mov rsi, r12 mov ecx, r13d - mov rdi, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] + mov rdip, JSAMPARRAY [rsi+0*SIZEOF_JSAMPARRAY] lea rdi, [rdi+rcx*SIZEOF_JSAMPROW] pop rcx @@ -70,8 +71,8 @@ EXTN(jsimd_rgb_gray_convert_sse2): push rsi push rcx ; col - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr0 + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr0 cmp rcx, byte SIZEOF_XMMWORD jae near .columnloop diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jchuff-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jchuff-sse2.asm index aa78fd5cd5e..9ea6df946ef 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jchuff-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jchuff-sse2.asm @@ -1,8 +1,9 @@ ; ; jchuff-sse2.asm - Huffman entropy encoding (64-bit SSE2) ; -; Copyright (C) 2009-2011, 2014-2016, D. R. Commander. +; Copyright (C) 2009-2011, 2014-2016, 2019, 2021, D. R. Commander. ; Copyright (C) 2015, Matthieu Darbois. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -15,11 +16,25 @@ ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; ; This file contains an SSE2 implementation for Huffman coding of one block. -; The following code is based directly on jchuff.c; see jchuff.c for more -; details. +; The following code is based on jchuff.c; see jchuff.c for more details. %include "jsimdext.inc" +struc working_state +.next_output_byte: resp 1 ; => next byte to write in buffer +.free_in_buffer: resp 1 ; # of byte spaces remaining in buffer +.cur.put_buffer.simd resq 1 ; current bit accumulation buffer +.cur.free_bits resd 1 ; # of bits available in it +.cur.last_dc_val resd 4 ; last DC coef for each component +.cinfo: resp 1 ; dump_buffer needs access to this +endstruc + +struc c_derived_tbl +.ehufco: resd 256 ; code for each symbol +.ehufsi: resb 256 ; length of code for each symbol +; If no code has been allocated for a symbol S, ehufsi[S] contains 0 +endstruc + ; -------------------------------------------------------------------------- SECTION SEG_CONST @@ -28,134 +43,138 @@ EXTN(jconst_huff_encode_one_block): -%include "jpeg_nbits_table.inc" +jpeg_mask_bits dd 0x0000, 0x0001, 0x0003, 0x0007 + dd 0x000f, 0x001f, 0x003f, 0x007f + dd 0x00ff, 0x01ff, 0x03ff, 0x07ff + dd 0x0fff, 0x1fff, 0x3fff, 0x7fff alignz 32 -; -------------------------------------------------------------------------- - SECTION SEG_TEXT - BITS 64 - -; These macros perform the same task as the emit_bits() function in the -; original libjpeg code. In addition to reducing overhead by explicitly -; inlining the code, additional performance is achieved by taking into -; account the size of the bit buffer and waiting until it is almost full -; before emptying it. This mostly benefits 64-bit platforms, since 6 -; bytes can be stored in a 64-bit bit buffer before it has to be emptied. - -%macro EMIT_BYTE 0 - sub put_bits, 8 ; put_bits -= 8; - mov rdx, put_buffer - mov ecx, put_bits - shr rdx, cl ; c = (JOCTET)GETJOCTET(put_buffer >> put_bits); - mov byte [buffer], dl ; *buffer++ = c; - add buffer, 1 - cmp dl, 0xFF ; need to stuff a zero byte? - jne %%.EMIT_BYTE_END - mov byte [buffer], 0 ; *buffer++ = 0; - add buffer, 1 -%%.EMIT_BYTE_END: -%endmacro +times 1 << 14 db 15 +times 1 << 13 db 14 +times 1 << 12 db 13 +times 1 << 11 db 12 +times 1 << 10 db 11 +times 1 << 9 db 10 +times 1 << 8 db 9 +times 1 << 7 db 8 +times 1 << 6 db 7 +times 1 << 5 db 6 +times 1 << 4 db 5 +times 1 << 3 db 4 +times 1 << 2 db 3 +times 1 << 1 db 2 +times 1 << 0 db 1 +times 1 db 0 +jpeg_nbits_table: +times 1 db 0 +times 1 << 0 db 1 +times 1 << 1 db 2 +times 1 << 2 db 3 +times 1 << 3 db 4 +times 1 << 4 db 5 +times 1 << 5 db 6 +times 1 << 6 db 7 +times 1 << 7 db 8 +times 1 << 8 db 9 +times 1 << 9 db 10 +times 1 << 10 db 11 +times 1 << 11 db 12 +times 1 << 12 db 13 +times 1 << 13 db 14 +times 1 << 14 db 15 +times 1 << 15 db 16 -%macro PUT_BITS 1 - add put_bits, ecx ; put_bits += size; - shl put_buffer, cl ; put_buffer = (put_buffer << size); - or put_buffer, %1 -%endmacro + alignz 32 -%macro CHECKBUF31 0 - cmp put_bits, 32 ; if (put_bits > 31) { - jl %%.CHECKBUF31_END - EMIT_BYTE - EMIT_BYTE - EMIT_BYTE - EMIT_BYTE -%%.CHECKBUF31_END: -%endmacro +%define NBITS(x) nbits_base + x +%define MASK_BITS(x) NBITS((x) * 4) + (jpeg_mask_bits - jpeg_nbits_table) -%macro CHECKBUF47 0 - cmp put_bits, 48 ; if (put_bits > 47) { - jl %%.CHECKBUF47_END - EMIT_BYTE - EMIT_BYTE - EMIT_BYTE - EMIT_BYTE - EMIT_BYTE - EMIT_BYTE -%%.CHECKBUF47_END: -%endmacro +; -------------------------------------------------------------------------- + SECTION SEG_TEXT + BITS 64 -%macro EMIT_BITS 2 - CHECKBUF47 - mov ecx, %2 - PUT_BITS %1 -%endmacro +; Shorthand used to describe SIMD operations: +; wN: xmmN treated as eight signed 16-bit values +; wN[i]: perform the same operation on all eight signed 16-bit values, i=0..7 +; bN: xmmN treated as 16 unsigned 8-bit values +; bN[i]: perform the same operation on all 16 unsigned 8-bit values, i=0..15 +; Contents of SIMD registers are shown in memory order. -%macro kloop_prepare 37 ;(ko, jno0, ..., jno31, xmm0, xmm1, xmm2, xmm3) - pxor xmm8, xmm8 ; __m128i neg = _mm_setzero_si128(); - pxor xmm9, xmm9 ; __m128i neg = _mm_setzero_si128(); - pxor xmm10, xmm10 ; __m128i neg = _mm_setzero_si128(); - pxor xmm11, xmm11 ; __m128i neg = _mm_setzero_si128(); - pinsrw %34, word [r12 + %2 * SIZEOF_WORD], 0 ; xmm_shadow[0] = block[jno0]; - pinsrw %35, word [r12 + %10 * SIZEOF_WORD], 0 ; xmm_shadow[8] = block[jno8]; - pinsrw %36, word [r12 + %18 * SIZEOF_WORD], 0 ; xmm_shadow[16] = block[jno16]; - pinsrw %37, word [r12 + %26 * SIZEOF_WORD], 0 ; xmm_shadow[24] = block[jno24]; - pinsrw %34, word [r12 + %3 * SIZEOF_WORD], 1 ; xmm_shadow[1] = block[jno1]; - pinsrw %35, word [r12 + %11 * SIZEOF_WORD], 1 ; xmm_shadow[9] = block[jno9]; - pinsrw %36, word [r12 + %19 * SIZEOF_WORD], 1 ; xmm_shadow[17] = block[jno17]; - pinsrw %37, word [r12 + %27 * SIZEOF_WORD], 1 ; xmm_shadow[25] = block[jno25]; - pinsrw %34, word [r12 + %4 * SIZEOF_WORD], 2 ; xmm_shadow[2] = block[jno2]; - pinsrw %35, word [r12 + %12 * SIZEOF_WORD], 2 ; xmm_shadow[10] = block[jno10]; - pinsrw %36, word [r12 + %20 * SIZEOF_WORD], 2 ; xmm_shadow[18] = block[jno18]; - pinsrw %37, word [r12 + %28 * SIZEOF_WORD], 2 ; xmm_shadow[26] = block[jno26]; - pinsrw %34, word [r12 + %5 * SIZEOF_WORD], 3 ; xmm_shadow[3] = block[jno3]; - pinsrw %35, word [r12 + %13 * SIZEOF_WORD], 3 ; xmm_shadow[11] = block[jno11]; - pinsrw %36, word [r12 + %21 * SIZEOF_WORD], 3 ; xmm_shadow[19] = block[jno19]; - pinsrw %37, word [r12 + %29 * SIZEOF_WORD], 3 ; xmm_shadow[27] = block[jno27]; - pinsrw %34, word [r12 + %6 * SIZEOF_WORD], 4 ; xmm_shadow[4] = block[jno4]; - pinsrw %35, word [r12 + %14 * SIZEOF_WORD], 4 ; xmm_shadow[12] = block[jno12]; - pinsrw %36, word [r12 + %22 * SIZEOF_WORD], 4 ; xmm_shadow[20] = block[jno20]; - pinsrw %37, word [r12 + %30 * SIZEOF_WORD], 4 ; xmm_shadow[28] = block[jno28]; - pinsrw %34, word [r12 + %7 * SIZEOF_WORD], 5 ; xmm_shadow[5] = block[jno5]; - pinsrw %35, word [r12 + %15 * SIZEOF_WORD], 5 ; xmm_shadow[13] = block[jno13]; - pinsrw %36, word [r12 + %23 * SIZEOF_WORD], 5 ; xmm_shadow[21] = block[jno21]; - pinsrw %37, word [r12 + %31 * SIZEOF_WORD], 5 ; xmm_shadow[29] = block[jno29]; - pinsrw %34, word [r12 + %8 * SIZEOF_WORD], 6 ; xmm_shadow[6] = block[jno6]; - pinsrw %35, word [r12 + %16 * SIZEOF_WORD], 6 ; xmm_shadow[14] = block[jno14]; - pinsrw %36, word [r12 + %24 * SIZEOF_WORD], 6 ; xmm_shadow[22] = block[jno22]; - pinsrw %37, word [r12 + %32 * SIZEOF_WORD], 6 ; xmm_shadow[30] = block[jno30]; - pinsrw %34, word [r12 + %9 * SIZEOF_WORD], 7 ; xmm_shadow[7] = block[jno7]; - pinsrw %35, word [r12 + %17 * SIZEOF_WORD], 7 ; xmm_shadow[15] = block[jno15]; - pinsrw %36, word [r12 + %25 * SIZEOF_WORD], 7 ; xmm_shadow[23] = block[jno23]; -%if %1 != 32 - pinsrw %37, word [r12 + %33 * SIZEOF_WORD], 7 ; xmm_shadow[31] = block[jno31]; -%else - pinsrw %37, ebx, 7 ; xmm_shadow[31] = block[jno31]; -%endif - pcmpgtw xmm8, %34 ; neg = _mm_cmpgt_epi16(neg, x1); - pcmpgtw xmm9, %35 ; neg = _mm_cmpgt_epi16(neg, x1); - pcmpgtw xmm10, %36 ; neg = _mm_cmpgt_epi16(neg, x1); - pcmpgtw xmm11, %37 ; neg = _mm_cmpgt_epi16(neg, x1); - paddw %34, xmm8 ; x1 = _mm_add_epi16(x1, neg); - paddw %35, xmm9 ; x1 = _mm_add_epi16(x1, neg); - paddw %36, xmm10 ; x1 = _mm_add_epi16(x1, neg); - paddw %37, xmm11 ; x1 = _mm_add_epi16(x1, neg); - pxor %34, xmm8 ; x1 = _mm_xor_si128(x1, neg); - pxor %35, xmm9 ; x1 = _mm_xor_si128(x1, neg); - pxor %36, xmm10 ; x1 = _mm_xor_si128(x1, neg); - pxor %37, xmm11 ; x1 = _mm_xor_si128(x1, neg); - pxor xmm8, %34 ; neg = _mm_xor_si128(neg, x1); - pxor xmm9, %35 ; neg = _mm_xor_si128(neg, x1); - pxor xmm10, %36 ; neg = _mm_xor_si128(neg, x1); - pxor xmm11, %37 ; neg = _mm_xor_si128(neg, x1); - movdqa XMMWORD [t1 + %1 * SIZEOF_WORD], %34 ; _mm_storeu_si128((__m128i *)(t1 + ko), x1); - movdqa XMMWORD [t1 + (%1 + 8) * SIZEOF_WORD], %35 ; _mm_storeu_si128((__m128i *)(t1 + ko + 8), x1); - movdqa XMMWORD [t1 + (%1 + 16) * SIZEOF_WORD], %36 ; _mm_storeu_si128((__m128i *)(t1 + ko + 16), x1); - movdqa XMMWORD [t1 + (%1 + 24) * SIZEOF_WORD], %37 ; _mm_storeu_si128((__m128i *)(t1 + ko + 24), x1); - movdqa XMMWORD [t2 + %1 * SIZEOF_WORD], xmm8 ; _mm_storeu_si128((__m128i *)(t2 + ko), neg); - movdqa XMMWORD [t2 + (%1 + 8) * SIZEOF_WORD], xmm9 ; _mm_storeu_si128((__m128i *)(t2 + ko + 8), neg); - movdqa XMMWORD [t2 + (%1 + 16) * SIZEOF_WORD], xmm10 ; _mm_storeu_si128((__m128i *)(t2 + ko + 16), neg); - movdqa XMMWORD [t2 + (%1 + 24) * SIZEOF_WORD], xmm11 ; _mm_storeu_si128((__m128i *)(t2 + ko + 24), neg); +; Fill the bit buffer to capacity with the leading bits from code, then output +; the bit buffer and put the remaining bits from code into the bit buffer. +; +; Usage: +; code - contains the bits to shift into the bit buffer (LSB-aligned) +; %1 - the label to which to jump when the macro completes +; %2 (optional) - extra instructions to execute after nbits has been set +; +; Upon completion, free_bits will be set to the number of remaining bits from +; code, and put_buffer will contain those remaining bits. temp and code will +; be clobbered. +; +; This macro encodes any 0xFF bytes as 0xFF 0x00, as does the EMIT_BYTE() +; macro in jchuff.c. + +%macro EMIT_QWORD 1-2 + add nbitsb, free_bitsb ; nbits += free_bits; + neg free_bitsb ; free_bits = -free_bits; + mov tempd, code ; temp = code; + shl put_buffer, nbitsb ; put_buffer <<= nbits; + mov nbitsb, free_bitsb ; nbits = free_bits; + neg free_bitsb ; free_bits = -free_bits; + shr tempd, nbitsb ; temp >>= nbits; + or tempq, put_buffer ; temp |= put_buffer; + movq xmm0, tempq ; xmm0.u64 = { temp, 0 }; + bswap tempq ; temp = htonl(temp); + mov put_buffer, codeq ; put_buffer = code; + pcmpeqb xmm0, xmm1 ; b0[i] = (b0[i] == 0xFF ? 0xFF : 0); + %2 + pmovmskb code, xmm0 ; code = 0; code |= ((b0[i] >> 7) << i); + mov qword [buffer], tempq ; memcpy(buffer, &temp, 8); + ; (speculative; will be overwritten if + ; code contains any 0xFF bytes) + add free_bitsb, 64 ; free_bits += 64; + add bufferp, 8 ; buffer += 8; + test code, code ; if (code == 0) /* No 0xFF bytes */ + jz %1 ; return; + ; Execute the equivalent of the EMIT_BYTE() macro in jchuff.c for all 8 + ; bytes in the qword. + cmp tempb, 0xFF ; Set CF if temp[0] < 0xFF + mov byte [buffer-7], 0 ; buffer[-7] = 0; + sbb bufferp, 6 ; buffer -= (6 + (temp[0] < 0xFF ? 1 : 0)); + mov byte [buffer], temph ; buffer[0] = temp[1]; + cmp temph, 0xFF ; Set CF if temp[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb bufferp, -2 ; buffer -= (-2 + (temp[1] < 0xFF ? 1 : 0)); + shr tempq, 16 ; temp >>= 16; + mov byte [buffer], tempb ; buffer[0] = temp[0]; + cmp tempb, 0xFF ; Set CF if temp[0] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb bufferp, -2 ; buffer -= (-2 + (temp[0] < 0xFF ? 1 : 0)); + mov byte [buffer], temph ; buffer[0] = temp[1]; + cmp temph, 0xFF ; Set CF if temp[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb bufferp, -2 ; buffer -= (-2 + (temp[1] < 0xFF ? 1 : 0)); + shr tempq, 16 ; temp >>= 16; + mov byte [buffer], tempb ; buffer[0] = temp[0]; + cmp tempb, 0xFF ; Set CF if temp[0] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb bufferp, -2 ; buffer -= (-2 + (temp[0] < 0xFF ? 1 : 0)); + mov byte [buffer], temph ; buffer[0] = temp[1]; + cmp temph, 0xFF ; Set CF if temp[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb bufferp, -2 ; buffer -= (-2 + (temp[1] < 0xFF ? 1 : 0)); + shr tempd, 16 ; temp >>= 16; + mov byte [buffer], tempb ; buffer[0] = temp[0]; + cmp tempb, 0xFF ; Set CF if temp[0] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb bufferp, -2 ; buffer -= (-2 + (temp[0] < 0xFF ? 1 : 0)); + mov byte [buffer], temph ; buffer[0] = temp[1]; + cmp temph, 0xFF ; Set CF if temp[1] < 0xFF + mov byte [buffer+1], 0 ; buffer[1] = 0; + sbb bufferp, -2 ; buffer -= (-2 + (temp[1] < 0xFF ? 1 : 0)); + jmp %1 ; return; %endmacro ; @@ -166,181 +185,399 @@ EXTN(jconst_huff_encode_one_block): ; JCOEFPTR block, int last_dc_val, ; c_derived_tbl *dctbl, c_derived_tbl *actbl) ; - -; r10 = working_state *state -; r11 = JOCTET *buffer -; r12 = JCOEFPTR block -; r13d = int last_dc_val -; r14 = c_derived_tbl *dctbl -; r15 = c_derived_tbl *actbl - -%define t1 rbp - (DCTSIZE2 * SIZEOF_WORD) -%define t2 t1 - (DCTSIZE2 * SIZEOF_WORD) -%define put_buffer r8 -%define put_bits r9d -%define buffer rax +; NOTES: +; When shuffling data, we try to avoid pinsrw as much as possible, since it is +; slow on many CPUs. Its reciprocal throughput (issue latency) is 1 even on +; modern CPUs, so chains of pinsrw instructions (even with different outputs) +; can limit performance. pinsrw is a VectorPath instruction on AMD K8 and +; requires 2 µops (with memory operand) on Intel. In either case, only one +; pinsrw instruction can be decoded per cycle (and nothing else if they are +; back-to-back), so out-of-order execution cannot be used to work around long +; pinsrw chains (though for Sandy Bridge and later, this may be less of a +; problem if the code runs from the µop cache.) +; +; We use tzcnt instead of bsf without checking for support. The instruction is +; executed as bsf on CPUs that don't support tzcnt (encoding is equivalent to +; rep bsf.) The destination (first) operand of bsf (and tzcnt on some CPUs) is +; an input dependency (although the behavior is not formally defined, Intel +; CPUs usually leave the destination unmodified if the source is zero.) This +; can prevent out-of-order execution, so we clear the destination before +; invoking tzcnt. +; +; Initial register allocation +; rax - buffer +; rbx - temp +; rcx - nbits +; rdx - block --> free_bits +; rsi - nbits_base +; rdi - t +; rbp - code +; r8 - dctbl --> code_temp +; r9 - actbl +; r10 - state +; r11 - index +; r12 - put_buffer + +%define buffer rax +%ifdef WIN64 +%define bufferp rax +%else +%define bufferp raxp +%endif +%define tempq rbx +%define tempd ebx +%define tempb bl +%define temph bh +%define nbitsq rcx +%define nbits ecx +%define nbitsb cl +%define block rdx +%define nbits_base rsi +%define t rdi +%define td edi +%define codeq rbp +%define code ebp +%define dctbl r8 +%define actbl r9 +%define state r10 +%define index r11 +%define indexd r11d +%define put_buffer r12 +%define put_bufferd r12d + +; Step 1: Re-arrange input data according to jpeg_natural_order +; xx 01 02 03 04 05 06 07 xx 01 08 16 09 02 03 10 +; 08 09 10 11 12 13 14 15 17 24 32 25 18 11 04 05 +; 16 17 18 19 20 21 22 23 12 19 26 33 40 48 41 34 +; 24 25 26 27 28 29 30 31 ==> 27 20 13 06 07 14 21 28 +; 32 33 34 35 36 37 38 39 35 42 49 56 57 50 43 36 +; 40 41 42 43 44 45 46 47 29 22 15 23 30 37 44 51 +; 48 49 50 51 52 53 54 55 58 59 52 45 38 31 39 46 +; 56 57 58 59 60 61 62 63 53 60 61 54 47 55 62 63 align 32 GLOBAL_FUNCTION(jsimd_huff_encode_one_block_sse2) EXTN(jsimd_huff_encode_one_block_sse2): + +%ifdef WIN64 + +; rcx = working_state *state +; rdx = JOCTET *buffer +; r8 = JCOEFPTR block +; r9 = int last_dc_val +; [rax+48] = c_derived_tbl *dctbl +; [rax+56] = c_derived_tbl *actbl + + ;X: X = code stream + mov buffer, rdx + mov block, r8 + movups xmm3, XMMWORD [block + 0 * SIZEOF_WORD] ;D: w3 = xx 01 02 03 04 05 06 07 + push rbx push rbp - mov rax, rsp ; rax = original rbp - sub rsp, byte 4 - and rsp, byte (-SIZEOF_XMMWORD) ; align to 128 bits - mov [rsp], rax - mov rbp, rsp ; rbp = aligned rbp - lea rsp, [t2] - push_xmm 4 - collect_args 6 + movdqa xmm0, xmm3 ;A: w0 = xx 01 02 03 04 05 06 07 + push rsi + push rdi + push r12 + movups xmm1, XMMWORD [block + 8 * SIZEOF_WORD] ;B: w1 = 08 09 10 11 12 13 14 15 + mov state, rcx + movsx code, word [block] ;Z: code = block[0]; + pxor xmm4, xmm4 ;A: w4[i] = 0; + sub code, r9d ;Z: code -= last_dc_val; + mov dctbl, POINTER [rsp+6*8+4*8] + mov actbl, POINTER [rsp+6*8+5*8] + punpckldq xmm0, xmm1 ;A: w0 = xx 01 08 09 02 03 10 11 + lea nbits_base, [rel jpeg_nbits_table] + add rsp, -DCTSIZE2 * SIZEOF_WORD + mov t, rsp + +%else + +; rdi = working_state *state +; rsi = JOCTET *buffer +; rdx = JCOEFPTR block +; rcx = int last_dc_val +; r8 = c_derived_tbl *dctbl +; r9 = c_derived_tbl *actbl + + ;X: X = code stream + movups xmm3, XMMWORD [block + 0 * SIZEOF_WORD] ;D: w3 = xx 01 02 03 04 05 06 07 push rbx + push rbp + movdqa xmm0, xmm3 ;A: w0 = xx 01 02 03 04 05 06 07 + push r12 + mov state, rdi + mov buffer, rsi + movups xmm1, XMMWORD [block + 8 * SIZEOF_WORD] ;B: w1 = 08 09 10 11 12 13 14 15 + movsx codeq, word [block] ;Z: code = block[0]; + lea nbits_base, [rel jpeg_nbits_table] + pxor xmm4, xmm4 ;A: w4[i] = 0; + sub codeq, rcx ;Z: code -= last_dc_val; + punpckldq xmm0, xmm1 ;A: w0 = xx 01 08 09 02 03 10 11 + lea t, [rsp - DCTSIZE2 * SIZEOF_WORD] ; use red zone for t_ - mov buffer, r11 ; r11 is now sratch - - mov put_buffer, MMWORD [r10+16] ; put_buffer = state->cur.put_buffer; - mov put_bits, dword [r10+24] ; put_bits = state->cur.put_bits; - push r10 ; r10 is now scratch - - ; Encode the DC coefficient difference per section F.1.2.1 - movsx edi, word [r12] ; temp = temp2 = block[0] - last_dc_val; - sub edi, r13d ; r13 is not used anymore - mov ebx, edi - - ; This is a well-known technique for obtaining the absolute value - ; without a branch. It is derived from an assembly language technique - ; presented in "How to Optimize for the Pentium Processors", - ; Copyright (c) 1996, 1997 by Agner Fog. - mov esi, edi - sar esi, 31 ; temp3 = temp >> (CHAR_BIT * sizeof(int) - 1); - xor edi, esi ; temp ^= temp3; - sub edi, esi ; temp -= temp3; - - ; For a negative input, want temp2 = bitwise complement of abs(input) - ; This code assumes we are on a two's complement machine - add ebx, esi ; temp2 += temp3; - - ; Find the number of bits needed for the magnitude of the coefficient - lea r11, [rel jpeg_nbits_table] - movzx rdi, byte [r11 + rdi] ; nbits = JPEG_NBITS(temp); - ; Emit the Huffman-coded symbol for the number of bits - mov r11d, INT [r14 + rdi * 4] ; code = dctbl->ehufco[nbits]; - movzx esi, byte [r14 + rdi + 1024] ; size = dctbl->ehufsi[nbits]; - EMIT_BITS r11, esi ; EMIT_BITS(code, size) - - ; Mask off any extra bits in code - mov esi, 1 - mov ecx, edi - shl esi, cl - dec esi - and ebx, esi ; temp2 &= (((JLONG)1)<ehufco[0xf0]; - movzx r14d, byte [r15 + 1024 + 240] ; size_0xf0 = actbl->ehufsi[0xf0]; - lea rsi, [t1] -.BLOOP: - bsf r12, r11 ; r = __builtin_ctzl(index); - jz .ELOOP - mov rcx, r12 - lea rsi, [rsi+r12*2] ; k += r; - shr r11, cl ; index >>= r; - movzx rdi, word [rsi] ; temp = t1[k]; - lea rbx, [rel jpeg_nbits_table] - movzx rdi, byte [rbx + rdi] ; nbits = JPEG_NBITS(temp); -.BRLOOP: - cmp r12, 16 ; while (r > 15) { - jl .ERLOOP - EMIT_BITS r13, r14d ; EMIT_BITS(code_0xf0, size_0xf0) - sub r12, 16 ; r -= 16; - jmp .BRLOOP -.ERLOOP: - ; Emit Huffman symbol for run length / number of bits - CHECKBUF31 ; uses rcx, rdx - - shl r12, 4 ; temp3 = (r << 4) + nbits; - add r12, rdi - mov ebx, INT [r15 + r12 * 4] ; code = actbl->ehufco[temp3]; - movzx ecx, byte [r15 + r12 + 1024] ; size = actbl->ehufsi[temp3]; - PUT_BITS rbx - - ;EMIT_CODE(code, size) - - movsx ebx, word [rsi-DCTSIZE2*2] ; temp2 = t2[k]; - ; Mask off any extra bits in code - mov rcx, rdi - mov rdx, 1 - shl rdx, cl - dec rdx - and rbx, rdx ; temp2 &= (((JLONG)1)<>= 1; - add rsi, 2 ; ++k; - jmp .BLOOP -.ELOOP: - ; If the last coef(s) were zero, emit an end-of-block code - lea rdi, [t1 + (DCTSIZE2-1) * 2] ; r = DCTSIZE2-1-k; - cmp rdi, rsi ; if (r > 0) { - je .EFN - mov ebx, INT [r15] ; code = actbl->ehufco[0]; - movzx r12d, byte [r15 + 1024] ; size = actbl->ehufsi[0]; - EMIT_BITS rbx, r12d -.EFN: - pop r10 - ; Save put_buffer & put_bits - mov MMWORD [r10+16], put_buffer ; state->cur.put_buffer = put_buffer; - mov dword [r10+24], put_bits ; state->cur.put_bits = put_bits; +%endif + pshuflw xmm0, xmm0, 11001001b ;A: w0 = 01 08 xx 09 02 03 10 11 + pinsrw xmm0, word [block + 16 * SIZEOF_WORD], 2 ;A: w0 = 01 08 16 09 02 03 10 11 + punpckhdq xmm3, xmm1 ;D: w3 = 04 05 12 13 06 07 14 15 + punpcklqdq xmm1, xmm3 ;B: w1 = 08 09 10 11 04 05 12 13 + pinsrw xmm0, word [block + 17 * SIZEOF_WORD], 7 ;A: w0 = 01 08 16 09 02 03 10 17 + ;A: (Row 0, offset 1) + pcmpgtw xmm4, xmm0 ;A: w4[i] = (w0[i] < 0 ? -1 : 0); + paddw xmm0, xmm4 ;A: w0[i] += w4[i]; + movaps XMMWORD [t + 0 * SIZEOF_WORD], xmm0 ;A: t[i] = w0[i]; + + movq xmm2, qword [block + 24 * SIZEOF_WORD] ;B: w2 = 24 25 26 27 -- -- -- -- + pshuflw xmm2, xmm2, 11011000b ;B: w2 = 24 26 25 27 -- -- -- -- + pslldq xmm1, 1 * SIZEOF_WORD ;B: w1 = -- 08 09 10 11 04 05 12 + movups xmm5, XMMWORD [block + 48 * SIZEOF_WORD] ;H: w5 = 48 49 50 51 52 53 54 55 + movsd xmm1, xmm2 ;B: w1 = 24 26 25 27 11 04 05 12 + punpcklqdq xmm2, xmm5 ;C: w2 = 24 26 25 27 48 49 50 51 + pinsrw xmm1, word [block + 32 * SIZEOF_WORD], 1 ;B: w1 = 24 32 25 27 11 04 05 12 + pxor xmm4, xmm4 ;A: w4[i] = 0; + psrldq xmm3, 2 * SIZEOF_WORD ;D: w3 = 12 13 06 07 14 15 -- -- + pcmpeqw xmm0, xmm4 ;A: w0[i] = (w0[i] == 0 ? -1 : 0); + pinsrw xmm1, word [block + 18 * SIZEOF_WORD], 3 ;B: w1 = 24 32 25 18 11 04 05 12 + ; (Row 1, offset 1) + pcmpgtw xmm4, xmm1 ;B: w4[i] = (w1[i] < 0 ? -1 : 0); + paddw xmm1, xmm4 ;B: w1[i] += w4[i]; + movaps XMMWORD [t + 8 * SIZEOF_WORD], xmm1 ;B: t[i+8] = w1[i]; + pxor xmm4, xmm4 ;B: w4[i] = 0; + pcmpeqw xmm1, xmm4 ;B: w1[i] = (w1[i] == 0 ? -1 : 0); + + packsswb xmm0, xmm1 ;AB: b0[i] = w0[i], b0[i+8] = w1[i] + ; w/ signed saturation + + pinsrw xmm3, word [block + 20 * SIZEOF_WORD], 0 ;D: w3 = 20 13 06 07 14 15 -- -- + pinsrw xmm3, word [block + 21 * SIZEOF_WORD], 5 ;D: w3 = 20 13 06 07 14 21 -- -- + pinsrw xmm3, word [block + 28 * SIZEOF_WORD], 6 ;D: w3 = 20 13 06 07 14 21 28 -- + pinsrw xmm3, word [block + 35 * SIZEOF_WORD], 7 ;D: w3 = 20 13 06 07 14 21 28 35 + ; (Row 3, offset 1) + pcmpgtw xmm4, xmm3 ;D: w4[i] = (w3[i] < 0 ? -1 : 0); + paddw xmm3, xmm4 ;D: w3[i] += w4[i]; + movaps XMMWORD [t + 24 * SIZEOF_WORD], xmm3 ;D: t[i+24] = w3[i]; + pxor xmm4, xmm4 ;D: w4[i] = 0; + pcmpeqw xmm3, xmm4 ;D: w3[i] = (w3[i] == 0 ? -1 : 0); + + pinsrw xmm2, word [block + 19 * SIZEOF_WORD], 0 ;C: w2 = 19 26 25 27 48 49 50 51 + cmp code, 1 << 31 ;Z: Set CF if code < 0x80000000, + ;Z: i.e. if code is positive + pinsrw xmm2, word [block + 33 * SIZEOF_WORD], 2 ;C: w2 = 19 26 33 27 48 49 50 51 + pinsrw xmm2, word [block + 40 * SIZEOF_WORD], 3 ;C: w2 = 19 26 33 40 48 49 50 51 + adc code, -1 ;Z: code += -1 + (code >= 0 ? 1 : 0); + pinsrw xmm2, word [block + 41 * SIZEOF_WORD], 5 ;C: w2 = 19 26 33 40 48 41 50 51 + pinsrw xmm2, word [block + 34 * SIZEOF_WORD], 6 ;C: w2 = 19 26 33 40 48 41 34 51 + movsxd codeq, code ;Z: sign extend code + pinsrw xmm2, word [block + 27 * SIZEOF_WORD], 7 ;C: w2 = 19 26 33 40 48 41 34 27 + ; (Row 2, offset 1) + pcmpgtw xmm4, xmm2 ;C: w4[i] = (w2[i] < 0 ? -1 : 0); + paddw xmm2, xmm4 ;C: w2[i] += w4[i]; + movaps XMMWORD [t + 16 * SIZEOF_WORD], xmm2 ;C: t[i+16] = w2[i]; + pxor xmm4, xmm4 ;C: w4[i] = 0; + pcmpeqw xmm2, xmm4 ;C: w2[i] = (w2[i] == 0 ? -1 : 0); + + packsswb xmm2, xmm3 ;CD: b2[i] = w2[i], b2[i+8] = w3[i] + ; w/ signed saturation + + movzx nbitsq, byte [NBITS(codeq)] ;Z: nbits = JPEG_NBITS(code); + movdqa xmm3, xmm5 ;H: w3 = 48 49 50 51 52 53 54 55 + pmovmskb tempd, xmm2 ;Z: temp = 0; temp |= ((b2[i] >> 7) << i); + pmovmskb put_bufferd, xmm0 ;Z: put_buffer = 0; put_buffer |= ((b0[i] >> 7) << i); + movups xmm0, XMMWORD [block + 56 * SIZEOF_WORD] ;H: w0 = 56 57 58 59 60 61 62 63 + punpckhdq xmm3, xmm0 ;H: w3 = 52 53 60 61 54 55 62 63 + shl tempd, 16 ;Z: temp <<= 16; + psrldq xmm3, 1 * SIZEOF_WORD ;H: w3 = 53 60 61 54 55 62 63 -- + pxor xmm2, xmm2 ;H: w2[i] = 0; + or put_bufferd, tempd ;Z: put_buffer |= temp; + pshuflw xmm3, xmm3, 00111001b ;H: w3 = 60 61 54 53 55 62 63 -- + movq xmm1, qword [block + 44 * SIZEOF_WORD] ;G: w1 = 44 45 46 47 -- -- -- -- + unpcklps xmm5, xmm0 ;E: w5 = 48 49 56 57 50 51 58 59 + pxor xmm0, xmm0 ;H: w0[i] = 0; + pinsrw xmm3, word [block + 47 * SIZEOF_WORD], 3 ;H: w3 = 60 61 54 47 55 62 63 -- + ; (Row 7, offset 1) + pcmpgtw xmm2, xmm3 ;H: w2[i] = (w3[i] < 0 ? -1 : 0); + paddw xmm3, xmm2 ;H: w3[i] += w2[i]; + movaps XMMWORD [t + 56 * SIZEOF_WORD], xmm3 ;H: t[i+56] = w3[i]; + movq xmm4, qword [block + 36 * SIZEOF_WORD] ;G: w4 = 36 37 38 39 -- -- -- -- + pcmpeqw xmm3, xmm0 ;H: w3[i] = (w3[i] == 0 ? -1 : 0); + punpckldq xmm4, xmm1 ;G: w4 = 36 37 44 45 38 39 46 47 + mov tempd, [dctbl + c_derived_tbl.ehufco + nbitsq * 4] + ;Z: temp = dctbl->ehufco[nbits]; + movdqa xmm1, xmm4 ;F: w1 = 36 37 44 45 38 39 46 47 + psrldq xmm4, 1 * SIZEOF_WORD ;G: w4 = 37 44 45 38 39 46 47 -- + shufpd xmm1, xmm5, 10b ;F: w1 = 36 37 44 45 50 51 58 59 + and code, dword [MASK_BITS(nbitsq)] ;Z: code &= (1 << nbits) - 1; + pshufhw xmm4, xmm4, 11010011b ;G: w4 = 37 44 45 38 -- 39 46 -- + pslldq xmm1, 1 * SIZEOF_WORD ;F: w1 = -- 36 37 44 45 50 51 58 + shl tempq, nbitsb ;Z: temp <<= nbits; + pinsrw xmm4, word [block + 59 * SIZEOF_WORD], 0 ;G: w4 = 59 44 45 38 -- 39 46 -- + pshufd xmm1, xmm1, 11011000b ;F: w1 = -- 36 45 50 37 44 51 58 + pinsrw xmm4, word [block + 52 * SIZEOF_WORD], 1 ;G: w4 = 59 52 45 38 -- 39 46 -- + or code, tempd ;Z: code |= temp; + movlps xmm1, qword [block + 20 * SIZEOF_WORD] ;F: w1 = 20 21 22 23 37 44 51 58 + pinsrw xmm4, word [block + 31 * SIZEOF_WORD], 4 ;G: w4 = 59 52 45 38 31 39 46 -- + pshuflw xmm1, xmm1, 01110010b ;F: w1 = 22 20 23 21 37 44 51 58 + pinsrw xmm4, word [block + 53 * SIZEOF_WORD], 7 ;G: w4 = 59 52 45 38 31 39 46 53 + ; (Row 6, offset 1) + pxor xmm2, xmm2 ;G: w2[i] = 0; + pcmpgtw xmm0, xmm4 ;G: w0[i] = (w4[i] < 0 ? -1 : 0); + pinsrw xmm1, word [block + 15 * SIZEOF_WORD], 1 ;F: w1 = 22 15 23 21 37 44 51 58 + paddw xmm4, xmm0 ;G: w4[i] += w0[i]; + movaps XMMWORD [t + 48 * SIZEOF_WORD], xmm4 ;G: t[48+i] = w4[i]; + pinsrw xmm1, word [block + 30 * SIZEOF_WORD], 3 ;F: w1 = 22 15 23 30 37 44 51 58 + ; (Row 5, offset 1) + pcmpeqw xmm4, xmm2 ;G: w4[i] = (w4[i] == 0 ? -1 : 0); + pinsrw xmm5, word [block + 42 * SIZEOF_WORD], 0 ;E: w5 = 42 49 56 57 50 51 58 59 + + packsswb xmm4, xmm3 ;GH: b4[i] = w4[i], b4[i+8] = w3[i] + ; w/ signed saturation + + pxor xmm0, xmm0 ;F: w0[i] = 0; + pinsrw xmm5, word [block + 43 * SIZEOF_WORD], 5 ;E: w5 = 42 49 56 57 50 43 58 59 + pcmpgtw xmm2, xmm1 ;F: w2[i] = (w1[i] < 0 ? -1 : 0); + pmovmskb tempd, xmm4 ;Z: temp = 0; temp |= ((b4[i] >> 7) << i); + pinsrw xmm5, word [block + 36 * SIZEOF_WORD], 6 ;E: w5 = 42 49 56 57 50 43 36 59 + paddw xmm1, xmm2 ;F: w1[i] += w2[i]; + movaps XMMWORD [t + 40 * SIZEOF_WORD], xmm1 ;F: t[40+i] = w1[i]; + pinsrw xmm5, word [block + 29 * SIZEOF_WORD], 7 ;E: w5 = 42 49 56 57 50 43 36 29 + ; (Row 4, offset 1) +%undef block +%define free_bitsq rdx +%define free_bitsd edx +%define free_bitsb dl + pcmpeqw xmm1, xmm0 ;F: w1[i] = (w1[i] == 0 ? -1 : 0); + shl tempq, 48 ;Z: temp <<= 48; + pxor xmm2, xmm2 ;E: w2[i] = 0; + pcmpgtw xmm0, xmm5 ;E: w0[i] = (w5[i] < 0 ? -1 : 0); + paddw xmm5, xmm0 ;E: w5[i] += w0[i]; + or tempq, put_buffer ;Z: temp |= put_buffer; + movaps XMMWORD [t + 32 * SIZEOF_WORD], xmm5 ;E: t[32+i] = w5[i]; + lea t, [dword t - 2] ;Z: t = &t[-1]; + pcmpeqw xmm5, xmm2 ;E: w5[i] = (w5[i] == 0 ? -1 : 0); + + packsswb xmm5, xmm1 ;EF: b5[i] = w5[i], b5[i+8] = w1[i] + ; w/ signed saturation + + add nbitsb, byte [dctbl + c_derived_tbl.ehufsi + nbitsq] + ;Z: nbits += dctbl->ehufsi[nbits]; +%undef dctbl +%define code_temp r8d + pmovmskb indexd, xmm5 ;Z: index = 0; index |= ((b5[i] >> 7) << i); + mov free_bitsd, [state+working_state.cur.free_bits] + ;Z: free_bits = state->cur.free_bits; + pcmpeqw xmm1, xmm1 ;Z: b1[i] = 0xFF; + shl index, 32 ;Z: index <<= 32; + mov put_buffer, [state+working_state.cur.put_buffer.simd] + ;Z: put_buffer = state->cur.put_buffer.simd; + or index, tempq ;Z: index |= temp; + not index ;Z: index = ~index; + sub free_bitsb, nbitsb ;Z: if ((free_bits -= nbits) >= 0) + jnl .ENTRY_SKIP_EMIT_CODE ;Z: goto .ENTRY_SKIP_EMIT_CODE; + align 16 +.EMIT_CODE: ;Z: .EMIT_CODE: + EMIT_QWORD .BLOOP_COND ;Z: insert code, flush buffer, goto .BLOOP_COND + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +.BRLOOP: ; do { + lea code_temp, [nbitsq - 16] ; code_temp = nbits - 16; + movzx nbits, byte [actbl + c_derived_tbl.ehufsi + 0xf0] + ; nbits = actbl->ehufsi[0xf0]; + mov code, [actbl + c_derived_tbl.ehufco + 0xf0 * 4] + ; code = actbl->ehufco[0xf0]; + sub free_bitsb, nbitsb ; if ((free_bits -= nbits) <= 0) + jle .EMIT_BRLOOP_CODE ; goto .EMIT_BRLOOP_CODE; + shl put_buffer, nbitsb ; put_buffer <<= nbits; + mov nbits, code_temp ; nbits = code_temp; + or put_buffer, codeq ; put_buffer |= code; + cmp nbits, 16 ; if (nbits <= 16) + jle .ERLOOP ; break; + jmp .BRLOOP ; } while (1); + +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 + times 5 nop +.ENTRY_SKIP_EMIT_CODE: ; .ENTRY_SKIP_EMIT_CODE: + shl put_buffer, nbitsb ; put_buffer <<= nbits; + or put_buffer, codeq ; put_buffer |= code; +.BLOOP_COND: ; .BLOOP_COND: + test index, index ; if (index != 0) + jz .ELOOP ; { +.BLOOP: ; do { + xor nbits, nbits ; nbits = 0; /* kill tzcnt input dependency */ + tzcnt nbitsq, index ; nbits = # of trailing 0 bits in index + inc nbits ; ++nbits; + lea t, [t + nbitsq * 2] ; t = &t[nbits]; + shr index, nbitsb ; index >>= nbits; +.EMIT_BRLOOP_CODE_END: ; .EMIT_BRLOOP_CODE_END: + cmp nbits, 16 ; if (nbits > 16) + jg .BRLOOP ; goto .BRLOOP; +.ERLOOP: ; .ERLOOP: + movsx codeq, word [t] ; code = *t; + lea tempd, [nbitsq * 2] ; temp = nbits * 2; + movzx nbits, byte [NBITS(codeq)] ; nbits = JPEG_NBITS(code); + lea tempd, [nbitsq + tempq * 8] ; temp = temp * 8 + nbits; + mov code_temp, [actbl + c_derived_tbl.ehufco + (tempq - 16) * 4] + ; code_temp = actbl->ehufco[temp-16]; + shl code_temp, nbitsb ; code_temp <<= nbits; + and code, dword [MASK_BITS(nbitsq)] ; code &= (1 << nbits) - 1; + add nbitsb, [actbl + c_derived_tbl.ehufsi + (tempq - 16)] + ; free_bits -= actbl->ehufsi[temp-16]; + or code, code_temp ; code |= code_temp; + sub free_bitsb, nbitsb ; if ((free_bits -= nbits) <= 0) + jle .EMIT_CODE ; goto .EMIT_CODE; + shl put_buffer, nbitsb ; put_buffer <<= nbits; + or put_buffer, codeq ; put_buffer |= code; + test index, index + jnz .BLOOP ; } while (index != 0); +.ELOOP: ; } /* index != 0 */ + sub td, esp ; t -= (WIN64: &t_[0], UNIX: &t_[64]); +%ifdef WIN64 + cmp td, (DCTSIZE2 - 2) * SIZEOF_WORD ; if (t != 62) +%else + cmp td, -2 * SIZEOF_WORD ; if (t != -2) +%endif + je .EFN ; { + movzx nbits, byte [actbl + c_derived_tbl.ehufsi + 0] + ; nbits = actbl->ehufsi[0]; + mov code, [actbl + c_derived_tbl.ehufco + 0] ; code = actbl->ehufco[0]; + sub free_bitsb, nbitsb ; if ((free_bits -= nbits) <= 0) + jg .EFN_SKIP_EMIT_CODE ; { + EMIT_QWORD .EFN ; insert code, flush buffer + align 16 +.EFN_SKIP_EMIT_CODE: ; } else { + shl put_buffer, nbitsb ; put_buffer <<= nbits; + or put_buffer, codeq ; put_buffer |= code; +.EFN: ; } } + mov [state + working_state.cur.put_buffer.simd], put_buffer + ; state->cur.put_buffer.simd = put_buffer; + mov byte [state + working_state.cur.free_bits], free_bitsb + ; state->cur.free_bits = free_bits; +%ifdef WIN64 + sub rsp, -DCTSIZE2 * SIZEOF_WORD + pop r12 + pop rdi + pop rsi + pop rbp pop rbx - uncollect_args 6 - pop_xmm 4 - mov rsp, rbp ; rsp <- aligned rbp - pop rsp ; rsp <- original rbp +%else + pop r12 pop rbp + pop rbx +%endif ret +; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + align 16 +.EMIT_BRLOOP_CODE: + EMIT_QWORD .EMIT_BRLOOP_CODE_END, { mov nbits, code_temp } + ; insert code, flush buffer, + ; nbits = code_temp, goto .EMIT_BRLOOP_CODE_END + ; For some reason, the OS X linker does not honor the request to align the ; segment unless we do this. align 32 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcphuff-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcphuff-sse2.asm index 8ed44728fed..01b5c0235fa 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcphuff-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcphuff-sse2.asm @@ -504,6 +504,8 @@ EXTN(jsimd_encode_mcu_AC_refine_prepare_sse2): add KK, 16 dec K jnz .BLOOPR16 + test LEN, 15 + je .PADDINGR .ELOOPR16: test LEN, 8 jz .TRYR7 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-avx2.asm index d9922bb4cbf..b32527aebea 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-avx2.asm @@ -4,6 +4,7 @@ ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. ; Copyright (C) 2015, Intel Corporation. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -71,7 +72,7 @@ EXTN(jsimd_h2v1_downsample_avx2): push rax push rcx - mov rdi, JSAMPROW [rsi] + mov rdip, JSAMPROW [rsi] add rdi, rdx mov al, JSAMPLE [rdi-1] @@ -107,8 +108,8 @@ EXTN(jsimd_h2v1_downsample_avx2): push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr cmp rcx, byte SIZEOF_YMMWORD jae short .columnloop @@ -233,7 +234,7 @@ EXTN(jsimd_h2v2_downsample_avx2): push rax push rcx - mov rdi, JSAMPROW [rsi] + mov rdip, JSAMPROW [rsi] add rdi, rdx mov al, JSAMPLE [rdi-1] @@ -269,9 +270,9 @@ EXTN(jsimd_h2v2_downsample_avx2): push rdi push rsi - mov rdx, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 - mov rsi, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1 - mov rdi, JSAMPROW [rdi] ; outptr + mov rdxp, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 + mov rsip, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1 + mov rdip, JSAMPROW [rdi] ; outptr cmp rcx, byte SIZEOF_YMMWORD jae short .columnloop diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-sse2.asm index 0f107e9a07f..2fcfe4567ab 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jcsample-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -70,7 +71,7 @@ EXTN(jsimd_h2v1_downsample_sse2): push rax push rcx - mov rdi, JSAMPROW [rsi] + mov rdip, JSAMPROW [rsi] add rdi, rdx mov al, JSAMPLE [rdi-1] @@ -105,8 +106,8 @@ EXTN(jsimd_h2v1_downsample_sse2): push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr cmp rcx, byte SIZEOF_XMMWORD jae short .columnloop @@ -215,7 +216,7 @@ EXTN(jsimd_h2v2_downsample_sse2): push rax push rcx - mov rdi, JSAMPROW [rsi] + mov rdip, JSAMPROW [rsi] add rdi, rdx mov al, JSAMPLE [rdi-1] @@ -250,9 +251,9 @@ EXTN(jsimd_h2v2_downsample_sse2): push rdi push rsi - mov rdx, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 - mov rsi, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1 - mov rdi, JSAMPROW [rdi] ; outptr + mov rdxp, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 + mov rsip, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1 + mov rdip, JSAMPROW [rdi] ; outptr cmp rcx, byte SIZEOF_XMMWORD jae short .columnloop diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-avx2.asm index 677b8ed84e4..2370fda6424 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-avx2.asm @@ -4,6 +4,7 @@ ; Copyright 2009, 2012 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2012, 2016, D. R. Commander. ; Copyright (C) 2015, Intel Corporation. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -58,9 +59,9 @@ EXTN(jsimd_ycc_rgb_convert_avx2): mov rdi, r11 mov ecx, r12d - mov rsi, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] + mov rsip, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] lea rsi, [rsi+rcx*SIZEOF_JSAMPROW] lea rbx, [rbx+rcx*SIZEOF_JSAMPROW] lea rdx, [rdx+rcx*SIZEOF_JSAMPROW] @@ -79,10 +80,10 @@ EXTN(jsimd_ycc_rgb_convert_avx2): push rsi push rcx ; col - mov rsi, JSAMPROW [rsi] ; inptr0 - mov rbx, JSAMPROW [rbx] ; inptr1 - mov rdx, JSAMPROW [rdx] ; inptr2 - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr0 + mov rbxp, JSAMPROW [rbx] ; inptr1 + mov rdxp, JSAMPROW [rdx] ; inptr2 + mov rdip, JSAMPROW [rdi] ; outptr .columnloop: vmovdqu ymm5, YMMWORD [rbx] ; ymm5=Cb(0123456789ABCDEFGHIJKLMNOPQRSTUV) diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-sse2.asm index 071aa629133..e07c8d75188 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdcolext-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009, 2012 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2012, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -57,9 +58,9 @@ EXTN(jsimd_ycc_rgb_convert_sse2): mov rdi, r11 mov ecx, r12d - mov rsi, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] + mov rsip, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] lea rsi, [rsi+rcx*SIZEOF_JSAMPROW] lea rbx, [rbx+rcx*SIZEOF_JSAMPROW] lea rdx, [rdx+rcx*SIZEOF_JSAMPROW] @@ -78,10 +79,10 @@ EXTN(jsimd_ycc_rgb_convert_sse2): push rsi push rcx ; col - mov rsi, JSAMPROW [rsi] ; inptr0 - mov rbx, JSAMPROW [rbx] ; inptr1 - mov rdx, JSAMPROW [rdx] ; inptr2 - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr0 + mov rbxp, JSAMPROW [rbx] ; inptr1 + mov rdxp, JSAMPROW [rdx] ; inptr2 + mov rdip, JSAMPROW [rdi] ; outptr .columnloop: movdqa xmm5, XMMWORD [rbx] ; xmm5=Cb(0123456789ABCDEF) diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-avx2.asm index bb733c587a4..8b264b4f039 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-avx2.asm @@ -4,6 +4,7 @@ ; Copyright 2009, 2012 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2012, 2016, D. R. Commander. ; Copyright (C) 2015, Intel Corporation. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -58,14 +59,14 @@ EXTN(jsimd_h2v1_merged_upsample_avx2): mov rdi, r11 mov ecx, r12d - mov rsi, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] + mov rsip, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] mov rdi, r13 - mov rsi, JSAMPROW [rsi+rcx*SIZEOF_JSAMPROW] ; inptr0 - mov rbx, JSAMPROW [rbx+rcx*SIZEOF_JSAMPROW] ; inptr1 - mov rdx, JSAMPROW [rdx+rcx*SIZEOF_JSAMPROW] ; inptr2 - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi+rcx*SIZEOF_JSAMPROW] ; inptr0 + mov rbxp, JSAMPROW [rbx+rcx*SIZEOF_JSAMPROW] ; inptr1 + mov rdxp, JSAMPROW [rdx+rcx*SIZEOF_JSAMPROW] ; inptr2 + mov rdip, JSAMPROW [rdi] ; outptr pop rcx ; col @@ -514,15 +515,16 @@ EXTN(jsimd_h2v2_merged_upsample_avx2): mov rdi, r11 mov ecx, r12d - mov rsi, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] + mov rsip, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] mov rdi, r13 lea rsi, [rsi+rcx*SIZEOF_JSAMPROW] - push rdx ; inptr2 - push rbx ; inptr1 - push rsi ; inptr00 + sub rsp, SIZEOF_JSAMPARRAY*4 + mov JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY], rsip ; intpr00 + mov JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY], rbxp ; intpr1 + mov JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY], rdxp ; intpr2 mov rbx, rsp push rdi @@ -546,16 +548,16 @@ EXTN(jsimd_h2v2_merged_upsample_avx2): pop rax pop rcx pop rdi - pop rsi - pop rbx - pop rdx + mov rsip, JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY] add rdi, byte SIZEOF_JSAMPROW ; outptr1 add rsi, byte SIZEOF_JSAMPROW ; inptr01 - push rdx ; inptr2 - push rbx ; inptr1 - push rsi ; inptr00 + mov JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY], rsip ; intpr00 + mov JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY], rbxp ; intpr1 + mov JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY], rdxp ; intpr2 mov rbx, rsp push rdi @@ -579,9 +581,10 @@ EXTN(jsimd_h2v2_merged_upsample_avx2): pop rax pop rcx pop rdi - pop rsi - pop rbx - pop rdx + mov rsip, JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY] + add rsp, SIZEOF_JSAMPARRAY*4 pop rbx uncollect_args 4 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-sse2.asm index b176a4cd4f9..eb3ab9dbd94 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdmrgext-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009, 2012 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2012, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -57,14 +58,14 @@ EXTN(jsimd_h2v1_merged_upsample_sse2): mov rdi, r11 mov ecx, r12d - mov rsi, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] + mov rsip, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] mov rdi, r13 - mov rsi, JSAMPROW [rsi+rcx*SIZEOF_JSAMPROW] ; inptr0 - mov rbx, JSAMPROW [rbx+rcx*SIZEOF_JSAMPROW] ; inptr1 - mov rdx, JSAMPROW [rdx+rcx*SIZEOF_JSAMPROW] ; inptr2 - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi+rcx*SIZEOF_JSAMPROW] ; inptr0 + mov rbxp, JSAMPROW [rbx+rcx*SIZEOF_JSAMPROW] ; inptr1 + mov rdxp, JSAMPROW [rdx+rcx*SIZEOF_JSAMPROW] ; inptr2 + mov rdip, JSAMPROW [rdi] ; outptr pop rcx ; col @@ -456,15 +457,16 @@ EXTN(jsimd_h2v2_merged_upsample_sse2): mov rdi, r11 mov ecx, r12d - mov rsi, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] - mov rbx, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] - mov rdx, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] + mov rsip, JSAMPARRAY [rdi+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rdi+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rdi+2*SIZEOF_JSAMPARRAY] mov rdi, r13 lea rsi, [rsi+rcx*SIZEOF_JSAMPROW] - push rdx ; inptr2 - push rbx ; inptr1 - push rsi ; inptr00 + sub rsp, SIZEOF_JSAMPARRAY*4 + mov JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY], rsip ; intpr00 + mov JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY], rbxp ; intpr1 + mov JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY], rdxp ; intpr2 mov rbx, rsp push rdi @@ -488,16 +490,16 @@ EXTN(jsimd_h2v2_merged_upsample_sse2): pop rax pop rcx pop rdi - pop rsi - pop rbx - pop rdx + mov rsip, JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY] add rdi, byte SIZEOF_JSAMPROW ; outptr1 add rsi, byte SIZEOF_JSAMPROW ; inptr01 - push rdx ; inptr2 - push rbx ; inptr1 - push rsi ; inptr00 + mov JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY], rsip ; intpr00 + mov JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY], rbxp ; intpr1 + mov JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY], rdxp ; intpr2 mov rbx, rsp push rdi @@ -521,9 +523,10 @@ EXTN(jsimd_h2v2_merged_upsample_sse2): pop rax pop rcx pop rdi - pop rsi - pop rbx - pop rdx + mov rsip, JSAMPARRAY [rsp+0*SIZEOF_JSAMPARRAY] + mov rbxp, JSAMPARRAY [rsp+1*SIZEOF_JSAMPARRAY] + mov rdxp, JSAMPARRAY [rsp+2*SIZEOF_JSAMPARRAY] + add rsp, SIZEOF_JSAMPARRAY*4 pop rbx uncollect_args 4 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-avx2.asm index fc274a95ea3..1e4979f933e 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-avx2.asm @@ -4,6 +4,7 @@ ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. ; Copyright (C) 2015, Intel Corporation. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -76,7 +77,7 @@ EXTN(jsimd_h2v1_fancy_upsample_avx2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data vpxor ymm0, ymm0, ymm0 ; ymm0=(all 0's) vpcmpeqb xmm9, xmm9, xmm9 @@ -90,8 +91,8 @@ EXTN(jsimd_h2v1_fancy_upsample_avx2): push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr test rax, SIZEOF_YMMWORD-1 jz short .skip @@ -235,18 +236,18 @@ EXTN(jsimd_h2v2_fancy_upsample_avx2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data .rowloop: push rax ; colctr push rcx push rdi push rsi - mov rcx, JSAMPROW [rsi-1*SIZEOF_JSAMPROW] ; inptr1(above) - mov rbx, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 - mov rsi, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1(below) - mov rdx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 - mov rdi, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 + mov rcxp, JSAMPROW [rsi-1*SIZEOF_JSAMPROW] ; inptr1(above) + mov rbxp, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 + mov rsip, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1(below) + mov rdxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 + mov rdip, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 vpxor ymm8, ymm8, ymm8 ; ymm8=(all 0's) vpcmpeqb xmm9, xmm9, xmm9 @@ -539,13 +540,13 @@ EXTN(jsimd_h2v1_upsample_avx2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data .rowloop: push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr mov rax, rdx ; colctr .columnloop: @@ -629,14 +630,14 @@ EXTN(jsimd_h2v2_upsample_avx2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data .rowloop: push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rbx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 - mov rdi, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 + mov rsip, JSAMPROW [rsi] ; inptr + mov rbxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 + mov rdip, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 mov rax, rdx ; colctr .columnloop: diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-sse2.asm index 20e07670e91..38dbceec269 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jdsample-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -74,14 +75,14 @@ EXTN(jsimd_h2v1_fancy_upsample_sse2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data .rowloop: push rax ; colctr push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr test rax, SIZEOF_XMMWORD-1 jz short .skip @@ -221,18 +222,18 @@ EXTN(jsimd_h2v2_fancy_upsample_sse2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data .rowloop: push rax ; colctr push rcx push rdi push rsi - mov rcx, JSAMPROW [rsi-1*SIZEOF_JSAMPROW] ; inptr1(above) - mov rbx, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 - mov rsi, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1(below) - mov rdx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 - mov rdi, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 + mov rcxp, JSAMPROW [rsi-1*SIZEOF_JSAMPROW] ; inptr1(above) + mov rbxp, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; inptr0 + mov rsip, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; inptr1(below) + mov rdxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 + mov rdip, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 test rax, SIZEOF_XMMWORD-1 jz short .skip @@ -512,13 +513,13 @@ EXTN(jsimd_h2v1_upsample_sse2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data .rowloop: push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rdi, JSAMPROW [rdi] ; outptr + mov rsip, JSAMPROW [rsi] ; inptr + mov rdip, JSAMPROW [rdi] ; outptr mov rax, rdx ; colctr .columnloop: @@ -600,14 +601,14 @@ EXTN(jsimd_h2v2_upsample_sse2): mov rsi, r12 ; input_data mov rdi, r13 - mov rdi, JSAMPARRAY [rdi] ; output_data + mov rdip, JSAMPARRAY [rdi] ; output_data .rowloop: push rdi push rsi - mov rsi, JSAMPROW [rsi] ; inptr - mov rbx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 - mov rdi, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 + mov rsip, JSAMPROW [rsi] ; inptr + mov rbxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] ; outptr0 + mov rdip, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] ; outptr1 mov rax, rdx ; colctr .columnloop: diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-avx2.asm index 6ad4cf0bbf7..e56258b48aa 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-avx2.asm @@ -2,7 +2,7 @@ ; jfdctint.asm - accurate integer FDCT (64-bit AVX2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2009, 2016, 2018, D. R. Commander. +; Copyright (C) 2009, 2016, 2018, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; forward DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jfdctint.c; see the jfdctint.c for ; more details. @@ -103,7 +103,7 @@ F_3_072 equ DESCALE(3299298341, 30 - CONST_BITS) ; FIX(3.072711026) %endmacro ; -------------------------------------------------------------------------- -; In-place 8x8x16-bit slow integer forward DCT using AVX2 instructions +; In-place 8x8x16-bit accurate integer forward DCT using AVX2 instructions ; %1-%4: Input/output registers ; %5-%8: Temp registers ; %9: Pass (1 or 2) diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-sse2.asm index 5d0de3cf41b..ec1f383ccb7 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jfdctint-sse2.asm @@ -2,7 +2,7 @@ ; jfdctint.asm - accurate integer FDCT (64-bit SSE2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2009, 2016, 2020, D. R. Commander. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +14,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; forward DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jfdctint.c; see the jfdctint.c for ; more details. diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctflt-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctflt-sse2.asm index ab95e1a6d66..60bf9618961 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctflt-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctflt-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -455,12 +456,12 @@ EXTN(jsimd_idct_float_sse2): pshufd xmm5, xmm6, 0x4E ; xmm5=(10 11 12 13 14 15 16 17 00 01 02 03 04 05 06 07) pshufd xmm3, xmm7, 0x4E ; xmm3=(30 31 32 33 34 35 36 37 20 21 22 23 24 25 26 27) - mov rdx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] - mov rbx, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] + mov rbxp, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm6 movq XMM_MMWORD [rbx+rax*SIZEOF_JSAMPLE], xmm7 - mov rdx, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] - mov rbx, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] + mov rbxp, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm5 movq XMM_MMWORD [rbx+rax*SIZEOF_JSAMPLE], xmm3 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctfst-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctfst-sse2.asm index a66a6811e9d..cb97fdfbb24 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctfst-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctfst-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -460,21 +461,21 @@ EXTN(jsimd_idct_ifast_sse2): pshufd xmm6, xmm4, 0x4E ; xmm6=(50 51 52 53 54 55 56 57 40 41 42 43 44 45 46 47) pshufd xmm2, xmm7, 0x4E ; xmm2=(70 71 72 73 74 75 76 77 60 61 62 63 64 65 66 67) - mov rdx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm1 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm3 - mov rdx, JSAMPROW [rdi+4*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+6*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+4*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+6*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm4 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm7 - mov rdx, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm5 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm0 - mov rdx, JSAMPROW [rdi+5*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+7*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+5*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+7*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm6 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm2 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-avx2.asm index 50270f47e22..ca7e317f6e1 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-avx2.asm @@ -2,7 +2,8 @@ ; jidctint.asm - accurate integer IDCT (64-bit AVX2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2009, 2016, 2018, D. R. Commander. +; Copyright (C) 2009, 2016, 2018, 2020, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +15,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; inverse DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jidctint.c; see the jidctint.c for ; more details. @@ -113,7 +114,7 @@ F_3_072 equ DESCALE(3299298341, 30 - CONST_BITS) ; FIX(3.072711026) %endmacro ; -------------------------------------------------------------------------- -; In-place 8x8x16-bit slow integer inverse DCT using AVX2 instructions +; In-place 8x8x16-bit accurate integer inverse DCT using AVX2 instructions ; %1-%4: Input/output registers ; %5-%12: Temp registers ; %9: Pass (1 or 2) @@ -387,23 +388,23 @@ EXTN(jsimd_idct_islow_avx2): mov eax, r13d - mov rdx, JSAMPROW [r12+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rsi, JSAMPROW [r12+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdxp, JSAMPROW [r12+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r12+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm0 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm1 - mov rdx, JSAMPROW [r12+2*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rsi, JSAMPROW [r12+3*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdxp, JSAMPROW [r12+2*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r12+3*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm2 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm3 - mov rdx, JSAMPROW [r12+4*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rsi, JSAMPROW [r12+5*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdxp, JSAMPROW [r12+4*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r12+5*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm4 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm5 - mov rdx, JSAMPROW [r12+6*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rsi, JSAMPROW [r12+7*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdxp, JSAMPROW [r12+6*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r12+7*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm6 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm7 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-sse2.asm index 034530c2b8e..7aa869bc0b5 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctint-sse2.asm @@ -2,7 +2,8 @@ ; jidctint.asm - accurate integer IDCT (64-bit SSE2) ; ; Copyright 2009 Pierre Ossman for Cendio AB -; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2009, 2016, 2020, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -14,7 +15,7 @@ ; NASM is available from http://nasm.sourceforge.net/ or ; http://sourceforge.net/project/showfiles.php?group_id=6208 ; -; This file contains a slow-but-accurate integer implementation of the +; This file contains a slower but more accurate integer implementation of the ; inverse DCT (Discrete Cosine Transform). The following code is based ; directly on the IJG's original jidctint.c; see the jidctint.c for ; more details. @@ -817,21 +818,21 @@ EXTN(jsimd_idct_islow_sse2): pshufd xmm2, xmm4, 0x4E ; xmm2=(50 51 52 53 54 55 56 57 40 41 42 43 44 45 46 47) pshufd xmm5, xmm3, 0x4E ; xmm5=(70 71 72 73 74 75 76 77 60 61 62 63 64 65 66 67) - mov rdx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm7 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm1 - mov rdx, JSAMPROW [rdi+4*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+6*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+4*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+6*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm4 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm3 - mov rdx, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm6 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm0 - mov rdx, JSAMPROW [rdi+5*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+7*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+5*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+7*SIZEOF_JSAMPROW] movq XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE], xmm2 movq XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE], xmm5 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctred-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctred-sse2.asm index 7fbfcc519dd..4ece9d891cb 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctred-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jidctred-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -379,12 +380,12 @@ EXTN(jsimd_idct_4x4_sse2): pshufd xmm1, xmm4, 0x4E ; xmm1=(20 21 22 23 30 31 32 33 00 ..) pshufd xmm3, xmm4, 0x93 ; xmm3=(30 31 32 33 00 01 02 03 10 ..) - mov rdx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] movd XMM_DWORD [rdx+rax*SIZEOF_JSAMPLE], xmm4 movd XMM_DWORD [rsi+rax*SIZEOF_JSAMPLE], xmm2 - mov rdx, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+2*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+3*SIZEOF_JSAMPROW] movd XMM_DWORD [rdx+rax*SIZEOF_JSAMPLE], xmm1 movd XMM_DWORD [rsi+rax*SIZEOF_JSAMPLE], xmm3 @@ -558,8 +559,8 @@ EXTN(jsimd_idct_2x2_sse2): pextrw ebx, xmm6, 0x00 ; ebx=(C0 D0 -- --) pextrw ecx, xmm6, 0x01 ; ecx=(C1 D1 -- --) - mov rdx, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] - mov rsi, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] + mov rdxp, JSAMPROW [rdi+0*SIZEOF_JSAMPROW] + mov rsip, JSAMPROW [rdi+1*SIZEOF_JSAMPROW] mov word [rdx+rax*SIZEOF_JSAMPLE], bx mov word [rsi+rax*SIZEOF_JSAMPLE], cx diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jquantf-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jquantf-sse2.asm index 83596a915b0..ab2e3954f63 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jquantf-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jquantf-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -51,8 +52,8 @@ EXTN(jsimd_convsamp_float_sse2): mov rdi, r12 mov rcx, DCTSIZE/2 .convloop: - mov rbx, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rdx, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rbxp, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdxp, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq xmm0, XMM_MMWORD [rbx+rax*SIZEOF_JSAMPLE] movq xmm1, XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE] diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-avx2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-avx2.asm index 5f04d223305..70fe81139cc 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-avx2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-avx2.asm @@ -4,6 +4,7 @@ ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, 2018, D. R. Commander. ; Copyright (C) 2016, Matthieu Darbois. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -44,23 +45,23 @@ EXTN(jsimd_convsamp_avx2): mov eax, r11d - mov rsi, JSAMPROW [r10+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rdi, JSAMPROW [r10+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r10+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdip, JSAMPROW [r10+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq xmm0, XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE] pinsrq xmm0, XMM_MMWORD [rdi+rax*SIZEOF_JSAMPLE], 1 - mov rsi, JSAMPROW [r10+2*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rdi, JSAMPROW [r10+3*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r10+2*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdip, JSAMPROW [r10+3*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq xmm1, XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE] pinsrq xmm1, XMM_MMWORD [rdi+rax*SIZEOF_JSAMPLE], 1 - mov rsi, JSAMPROW [r10+4*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rdi, JSAMPROW [r10+5*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r10+4*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdip, JSAMPROW [r10+5*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq xmm2, XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE] pinsrq xmm2, XMM_MMWORD [rdi+rax*SIZEOF_JSAMPLE], 1 - mov rsi, JSAMPROW [r10+6*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rdi, JSAMPROW [r10+7*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rsip, JSAMPROW [r10+6*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdip, JSAMPROW [r10+7*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq xmm3, XMM_MMWORD [rsi+rax*SIZEOF_JSAMPLE] pinsrq xmm3, XMM_MMWORD [rdi+rax*SIZEOF_JSAMPLE], 1 diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-sse2.asm b/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-sse2.asm index bb6fa69ea3c..3ee442027a5 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-sse2.asm +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jquanti-sse2.asm @@ -3,6 +3,7 @@ ; ; Copyright 2009 Pierre Ossman for Cendio AB ; Copyright (C) 2009, 2016, D. R. Commander. +; Copyright (C) 2018, Matthias Räncker. ; ; Based on the x86 SIMD extension for IJG JPEG library ; Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -51,14 +52,14 @@ EXTN(jsimd_convsamp_sse2): mov rdi, r12 mov rcx, DCTSIZE/4 .convloop: - mov rbx, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rdx, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rbxp, JSAMPROW [rsi+0*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdxp, JSAMPROW [rsi+1*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq xmm0, XMM_MMWORD [rbx+rax*SIZEOF_JSAMPLE] ; xmm0=(01234567) movq xmm1, XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE] ; xmm1=(89ABCDEF) - mov rbx, JSAMPROW [rsi+2*SIZEOF_JSAMPROW] ; (JSAMPLE *) - mov rdx, JSAMPROW [rsi+3*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rbxp, JSAMPROW [rsi+2*SIZEOF_JSAMPROW] ; (JSAMPLE *) + mov rdxp, JSAMPROW [rsi+3*SIZEOF_JSAMPROW] ; (JSAMPLE *) movq xmm2, XMM_MMWORD [rbx+rax*SIZEOF_JSAMPLE] ; xmm2=(GHIJKLMN) movq xmm3, XMM_MMWORD [rdx+rax*SIZEOF_JSAMPLE] ; xmm3=(OPQRSTUV) diff --git a/third-party/mozjpeg/mozjpeg/simd/x86_64/jsimd.c b/third-party/mozjpeg/mozjpeg/simd/x86_64/jsimd.c index 1e5698b3a4b..3f5ee77eb99 100644 --- a/third-party/mozjpeg/mozjpeg/simd/x86_64/jsimd.c +++ b/third-party/mozjpeg/mozjpeg/simd/x86_64/jsimd.c @@ -2,8 +2,8 @@ * jsimd_x86_64.c * * Copyright 2009 Pierre Ossman for Cendio AB - * Copyright (C) 2009-2011, 2014, 2016, 2018, D. R. Commander. - * Copyright (C) 2015-2016, 2018, Matthieu Darbois. + * Copyright (C) 2009-2011, 2014, 2016, 2018, 2022-2023, D. R. Commander. + * Copyright (C) 2015-2016, 2018, 2022, Matthieu Darbois. * * Based on the x86 SIMD extension for IJG JPEG library, * Copyright (C) 1999-2006, MIYASAKA Masaru. @@ -21,7 +21,6 @@ #include "../../jdct.h" #include "../../jsimddct.h" #include "../jsimd.h" -#include "jconfigint.h" /* * In the PIC cases, we have no guarantee that constants will keep @@ -32,19 +31,17 @@ #define IS_ALIGNED_SSE(ptr) (IS_ALIGNED(ptr, 4)) /* 16 byte alignment */ #define IS_ALIGNED_AVX(ptr) (IS_ALIGNED(ptr, 5)) /* 32 byte alignment */ -static unsigned int simd_support = (unsigned int)(~0); -static unsigned int simd_huffman = 1; +static THREAD_LOCAL unsigned int simd_support = (unsigned int)(~0); +static THREAD_LOCAL unsigned int simd_huffman = 1; /* * Check what SIMD accelerations are supported. - * - * FIXME: This code is racy under a multi-threaded environment. */ LOCAL(void) init_simd(void) { #ifndef NO_GETENV - char *env = NULL; + char env[2] = { 0 }; #endif if (simd_support != ~0U) @@ -54,17 +51,13 @@ init_simd(void) #ifndef NO_GETENV /* Force different settings through environment variables */ - env = getenv("JSIMD_FORCESSE2"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCESSE2") && !strcmp(env, "1")) simd_support &= JSIMD_SSE2; - env = getenv("JSIMD_FORCEAVX2"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCEAVX2") && !strcmp(env, "1")) simd_support &= JSIMD_AVX2; - env = getenv("JSIMD_FORCENONE"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_FORCENONE") && !strcmp(env, "1")) simd_support = 0; - env = getenv("JSIMD_NOHUFFENC"); - if ((env != NULL) && (strcmp(env, "1") == 0)) + if (!GETENV_S(env, 2, "JSIMD_NOHUFFENC") && !strcmp(env, "1")) simd_huffman = 0; #endif } @@ -152,6 +145,9 @@ jsimd_rgb_ycc_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, void (*avx2fct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); void (*sse2fct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->in_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_extrgb_ycc_convert_avx2; @@ -201,6 +197,9 @@ jsimd_rgb_gray_convert(j_compress_ptr cinfo, JSAMPARRAY input_buf, void (*avx2fct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); void (*sse2fct) (JDIMENSION, JSAMPARRAY, JSAMPIMAGE, JDIMENSION, int); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->in_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_extrgb_gray_convert_avx2; @@ -250,6 +249,9 @@ jsimd_ycc_rgb_convert(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, void (*avx2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY, int); void (*sse2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY, int); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->out_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_ycc_extrgb_convert_avx2; @@ -340,6 +342,9 @@ GLOBAL(void) jsimd_h2v2_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY output_data) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v2_downsample_avx2(cinfo->image_width, cinfo->max_v_samp_factor, compptr->v_samp_factor, @@ -356,6 +361,9 @@ GLOBAL(void) jsimd_h2v1_downsample(j_compress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY output_data) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v1_downsample_avx2(cinfo->image_width, cinfo->max_v_samp_factor, compptr->v_samp_factor, @@ -410,6 +418,9 @@ GLOBAL(void) jsimd_h2v2_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v2_upsample_avx2(cinfo->max_v_samp_factor, cinfo->output_width, input_data, output_data_ptr); @@ -422,6 +433,9 @@ GLOBAL(void) jsimd_h2v1_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v1_upsample_avx2(cinfo->max_v_samp_factor, cinfo->output_width, input_data, output_data_ptr); @@ -476,6 +490,9 @@ GLOBAL(void) jsimd_h2v2_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v2_fancy_upsample_avx2(cinfo->max_v_samp_factor, compptr->downsampled_width, input_data, @@ -490,6 +507,9 @@ GLOBAL(void) jsimd_h2v1_fancy_upsample(j_decompress_ptr cinfo, jpeg_component_info *compptr, JSAMPARRAY input_data, JSAMPARRAY *output_data_ptr) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_h2v1_fancy_upsample_avx2(cinfo->max_v_samp_factor, compptr->downsampled_width, input_data, @@ -549,6 +569,9 @@ jsimd_h2v2_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, void (*avx2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); void (*sse2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->out_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_h2v2_extrgb_merged_upsample_avx2; @@ -597,6 +620,9 @@ jsimd_h2v1_merged_upsample(j_decompress_ptr cinfo, JSAMPIMAGE input_buf, void (*avx2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); void (*sse2fct) (JDIMENSION, JSAMPIMAGE, JDIMENSION, JSAMPARRAY); + if (simd_support == ~0U) + init_simd(); + switch (cinfo->out_color_space) { case JCS_EXT_RGB: avx2fct = jsimd_h2v1_extrgb_merged_upsample_avx2; @@ -686,6 +712,9 @@ GLOBAL(void) jsimd_convsamp(JSAMPARRAY sample_data, JDIMENSION start_col, DCTELEM *workspace) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_convsamp_avx2(sample_data, start_col, workspace); else @@ -755,6 +784,9 @@ jsimd_can_fdct_float(void) GLOBAL(void) jsimd_fdct_islow(DCTELEM *data) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_fdct_islow_avx2(data); else @@ -816,6 +848,9 @@ jsimd_can_quantize_float(void) GLOBAL(void) jsimd_quantize(JCOEFPTR coef_block, DCTELEM *divisors, DCTELEM *workspace) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_quantize_avx2(coef_block, divisors, workspace); else @@ -970,6 +1005,9 @@ jsimd_idct_islow(j_decompress_ptr cinfo, jpeg_component_info *compptr, JCOEFPTR coef_block, JSAMPARRAY output_buf, JDIMENSION output_col) { + if (simd_support == ~0U) + init_simd(); + if (simd_support & JSIMD_AVX2) jsimd_idct_islow_avx2(compptr->dct_table, coef_block, output_buf, output_col); @@ -1031,8 +1069,6 @@ jsimd_can_encode_mcu_AC_first_prepare(void) return 0; if (sizeof(JCOEF) != 2) return 0; - if (SIZEOF_SIZE_T != 8) - return 0; if (simd_support & JSIMD_SSE2) return 1; @@ -1042,7 +1078,7 @@ jsimd_can_encode_mcu_AC_first_prepare(void) GLOBAL(void) jsimd_encode_mcu_AC_first_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *values, size_t *zerobits) + int Al, UJCOEF *values, size_t *zerobits) { jsimd_encode_mcu_AC_first_prepare_sse2(block, jpeg_natural_order_start, Sl, Al, values, zerobits); @@ -1057,8 +1093,6 @@ jsimd_can_encode_mcu_AC_refine_prepare(void) return 0; if (sizeof(JCOEF) != 2) return 0; - if (SIZEOF_SIZE_T != 8) - return 0; if (simd_support & JSIMD_SSE2) return 1; @@ -1068,7 +1102,7 @@ jsimd_can_encode_mcu_AC_refine_prepare(void) GLOBAL(int) jsimd_encode_mcu_AC_refine_prepare(const JCOEF *block, const int *jpeg_natural_order_start, int Sl, - int Al, JCOEF *absvalues, size_t *bits) + int Al, UJCOEF *absvalues, size_t *bits) { return jsimd_encode_mcu_AC_refine_prepare_sse2(block, jpeg_natural_order_start, diff --git a/third-party/mozjpeg/mozjpeg/src/jconfig.h b/third-party/mozjpeg/mozjpeg/src/jconfig.h deleted file mode 100644 index beccc163e6d..00000000000 --- a/third-party/mozjpeg/mozjpeg/src/jconfig.h +++ /dev/null @@ -1,73 +0,0 @@ -/* Version ID for the JPEG library. - * Might be useful for tests like "#if JPEG_LIB_VERSION >= 60". - */ -#define JPEG_LIB_VERSION 80 - -/* libjpeg-turbo version */ -#define LIBJPEG_TURBO_VERSION 4.0.0 - -/* libjpeg-turbo version in integer form */ -#define LIBJPEG_TURBO_VERSION_NUMBER 4000000 - -/* Support arithmetic encoding */ -#define C_ARITH_CODING_SUPPORTED 1 - -/* Support arithmetic decoding */ -#define D_ARITH_CODING_SUPPORTED 1 - -/* Support in-memory source/destination managers */ -/* #undef MEM_SRCDST_SUPPORTED */ - -/* Use accelerated SIMD routines. */ -#define WITH_SIMD 1 - -/* - * Define BITS_IN_JSAMPLE as either - * 8 for 8-bit sample values (the usual setting) - * 12 for 12-bit sample values - * Only 8 and 12 are legal data precisions for lossy JPEG according to the - * JPEG standard, and the IJG code does not support anything else! - * We do not support run-time selection of data precision, sorry. - */ - -#define BITS_IN_JSAMPLE 8 /* use 8 or 12 */ - -/* Define to 1 if you have the header file. */ -#define HAVE_LOCALE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDDEF_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDLIB_H 1 - -/* Define if you need to include to get size_t. */ -#define NEED_SYS_TYPES_H 1 - -/* Define if you have BSD-like bzero and bcopy in rather than - memset/memcpy in . */ -/* #undef NEED_BSD_STRINGS */ - -/* Define to 1 if the system has the type `unsigned char'. */ -#define HAVE_UNSIGNED_CHAR 1 - -/* Define to 1 if the system has the type `unsigned short'. */ -#define HAVE_UNSIGNED_SHORT 1 - -/* Compiler does not support pointers to undefined structures. */ -/* #undef INCOMPLETE_TYPES_BROKEN */ - -/* Define if your (broken) compiler shifts signed values as if they were - unsigned. */ -/* #undef RIGHT_SHIFT_IS_UNSIGNED */ - -/* Define to 1 if type `char' is unsigned and you are not using gcc. */ -#ifndef __CHAR_UNSIGNED__ -/* #undef __CHAR_UNSIGNED__ */ -#endif - -/* Define to empty if `const' does not conform to ANSI C. */ -/* #undef const */ - -/* Define to `unsigned int' if does not define. */ -/* #undef size_t */ diff --git a/third-party/mozjpeg/mozjpeg/src/jconfigint.h b/third-party/mozjpeg/mozjpeg/src/jconfigint.h deleted file mode 100644 index 4d0059156ab..00000000000 --- a/third-party/mozjpeg/mozjpeg/src/jconfigint.h +++ /dev/null @@ -1,31 +0,0 @@ -/* libjpeg-turbo build number */ -#define BUILD "20201129" - -/* Compiler's inline keyword */ -#undef inline - -/* How to obtain function inlining. */ -#define INLINE __inline__ __attribute__((always_inline)) - -/* Define to the full name of this package. */ -#define PACKAGE_NAME "mozjpeg" - -/* Version number of package */ -#define VERSION "4.0.0" - -/* The size of `size_t', as computed by sizeof. */ -#define SIZEOF_SIZE_T 8 - -/* Define if your compiler has __builtin_ctzl() and sizeof(unsigned long) == sizeof(size_t). */ -#define HAVE_BUILTIN_CTZL - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_INTRIN_H */ - -#if defined(_MSC_VER) && defined(HAVE_INTRIN_H) -#if (SIZEOF_SIZE_T == 8) -#define HAVE_BITSCANFORWARD64 -#elif (SIZEOF_SIZE_T == 4) -#define HAVE_BITSCANFORWARD -#endif -#endif diff --git a/third-party/mozjpeg/mozjpeg/strtest.c b/third-party/mozjpeg/mozjpeg/strtest.c new file mode 100644 index 00000000000..3d5e004c6fc --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/strtest.c @@ -0,0 +1,170 @@ +/* + * Copyright (C)2022-2023 D. R. Commander. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the libjpeg-turbo Project nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "jinclude.h" +#include + + +#define CHECK_VALUE(actual, expected, desc) \ + if (actual != expected) { \ + printf("ERROR in line %d: " desc " is %d, should be %d\n", \ + __LINE__, actual, expected); \ + return -1; \ + } + +#define CHECK_ERRNO(errno_return, expected_errno) \ + CHECK_VALUE(errno_return, expected_errno, "Return value") \ + CHECK_VALUE(errno, expected_errno, "errno") \ + + +#ifdef _MSC_VER + +void invalid_parameter_handler(const wchar_t *expression, + const wchar_t *function, const wchar_t *file, + unsigned int line, uintptr_t pReserved) +{ +} + +#endif + + +int main(int argc, char **argv) +{ +#if !defined(NO_GETENV) || !defined(NO_PUTENV) + int err; +#endif +#ifndef NO_GETENV + char env[3]; +#endif + +#ifdef _MSC_VER + _set_invalid_parameter_handler(invalid_parameter_handler); +#endif + + /***************************************************************************/ + +#ifndef NO_PUTENV + + printf("PUTENV_S():\n"); + + errno = 0; + err = PUTENV_S(NULL, "12"); + CHECK_ERRNO(err, EINVAL); + + errno = 0; + err = PUTENV_S("TESTENV", NULL); + CHECK_ERRNO(err, EINVAL); + + errno = 0; + err = PUTENV_S("TESTENV", "12"); + CHECK_ERRNO(err, 0); + + printf("SUCCESS!\n\n"); + +#endif + + /***************************************************************************/ + +#ifndef NO_GETENV + + printf("GETENV_S():\n"); + + errno = 0; + env[0] = 1; + env[1] = 2; + env[2] = 3; + err = GETENV_S(env, 3, NULL); + CHECK_ERRNO(err, 0); + CHECK_VALUE(env[0], 0, "env[0]"); + CHECK_VALUE(env[1], 2, "env[1]"); + CHECK_VALUE(env[2], 3, "env[2]"); + + errno = 0; + env[0] = 1; + env[1] = 2; + env[2] = 3; + err = GETENV_S(env, 3, "TESTENV2"); + CHECK_ERRNO(err, 0); + CHECK_VALUE(env[0], 0, "env[0]"); + CHECK_VALUE(env[1], 2, "env[1]"); + CHECK_VALUE(env[2], 3, "env[2]"); + + errno = 0; + err = GETENV_S(NULL, 3, "TESTENV"); + CHECK_ERRNO(err, EINVAL); + + errno = 0; + err = GETENV_S(NULL, 0, "TESTENV"); + CHECK_ERRNO(err, 0); + + errno = 0; + env[0] = 1; + err = GETENV_S(env, 0, "TESTENV"); + CHECK_ERRNO(err, EINVAL); + CHECK_VALUE(env[0], 1, "env[0]"); + + errno = 0; + env[0] = 1; + env[1] = 2; + env[2] = 3; + err = GETENV_S(env, 1, "TESTENV"); + CHECK_VALUE(err, ERANGE, "Return value"); + CHECK_VALUE(errno, 0, "errno"); + CHECK_VALUE(env[0], 0, "env[0]"); + CHECK_VALUE(env[1], 2, "env[1]"); + CHECK_VALUE(env[2], 3, "env[2]"); + + errno = 0; + env[0] = 1; + env[1] = 2; + env[2] = 3; + err = GETENV_S(env, 2, "TESTENV"); + CHECK_VALUE(err, ERANGE, "Return value"); + CHECK_VALUE(errno, 0, "errno"); + CHECK_VALUE(env[0], 0, "env[0]"); + CHECK_VALUE(env[1], 2, "env[1]"); + CHECK_VALUE(env[2], 3, "env[2]"); + + errno = 0; + env[0] = 1; + env[1] = 2; + env[2] = 3; + err = GETENV_S(env, 3, "TESTENV"); + CHECK_ERRNO(err, 0); + CHECK_VALUE(env[0], '1', "env[0]"); + CHECK_VALUE(env[1], '2', "env[1]"); + CHECK_VALUE(env[2], 0, "env[2]"); + + printf("SUCCESS!\n\n"); + +#endif + + /***************************************************************************/ + + return 0; +} diff --git a/third-party/mozjpeg/mozjpeg/structure.txt b/third-party/mozjpeg/mozjpeg/structure.txt index c0792a367ea..15b8d378564 100644 --- a/third-party/mozjpeg/mozjpeg/structure.txt +++ b/third-party/mozjpeg/mozjpeg/structure.txt @@ -548,13 +548,9 @@ Arrays of pixel sample values use the following data structure: typedef JSAMPROW *JSAMPARRAY; ptr to a list of rows typedef JSAMPARRAY *JSAMPIMAGE; ptr to a list of color-component arrays -The basic element type JSAMPLE will typically be one of unsigned char, -(signed) char, or short. Short will be used if samples wider than 8 bits are -to be supported (this is a compile-time option). Otherwise, unsigned char is -used if possible. If the compiler only supports signed chars, then it is -necessary to mask off the value when reading. Thus, all reads of JSAMPLE -values must be coded as "GETJSAMPLE(value)", where the macro will be defined -as "((value) & 0xFF)" on signed-char machines and "((int) (value))" elsewhere. +The basic element type JSAMPLE will be one of unsigned char or short. Short +will be used if samples wider than 8 bits are to be supported (this is a +compile-time option). Otherwise, unsigned char is used. With these conventions, JSAMPLE values can be assumed to be >= 0. This helps simplify correct rounding during downsampling, etc. The JPEG standard's @@ -587,7 +583,7 @@ we can read or write each component to a temporary file independently, which is helpful when dealing with noninterleaved JPEG files. In general, a specific sample value is accessed by code such as - GETJSAMPLE(image[colorcomponent][row][col]) + image[colorcomponent][row][col] where col is measured from the image left edge, but row is measured from the first sample row currently in memory. Either of the first two indexings can be precomputed by copying the relevant pointer. diff --git a/third-party/mozjpeg/mozjpeg/testimages/test.scan b/third-party/mozjpeg/mozjpeg/testimages/test.scan index 563446da3ce..2a8ea7f2e12 100644 --- a/third-party/mozjpeg/mozjpeg/testimages/test.scan +++ b/third-party/mozjpeg/mozjpeg/testimages/test.scan @@ -1,5 +1,8 @@ 0 1 2: 0 0 0 0; -0: 1 16 0 0; -0: 17 63 0 0; +0: 1 9 0 0; +0: 10 41 0 2; +0: 10 41 2 1; +0: 10 41 1 0; +0: 42 63 0 0; 1: 1 63 0 0; 2: 1 63 0 0; diff --git a/third-party/mozjpeg/mozjpeg/tjbench.c b/third-party/mozjpeg/mozjpeg/tjbench.c index faad9784b82..7d901b8aed2 100644 --- a/third-party/mozjpeg/mozjpeg/tjbench.c +++ b/third-party/mozjpeg/mozjpeg/tjbench.c @@ -1,5 +1,5 @@ /* - * Copyright (C)2009-2019 D. R. Commander. All Rights Reserved. + * Copyright (C)2009-2019, 2021-2023 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -26,6 +26,10 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #include #include #include @@ -61,8 +65,10 @@ int tjErrorLine = -1, tjErrorCode = -1; if (strncmp(tjErrorStr, _tjErrorStr, JMSG_LENGTH_MAX) || \ strncmp(tjErrorMsg, m, JMSG_LENGTH_MAX) || \ tjErrorCode != _tjErrorCode || tjErrorLine != __LINE__) { \ - strncpy(tjErrorStr, _tjErrorStr, JMSG_LENGTH_MAX - 1); \ - strncpy(tjErrorMsg, m, JMSG_LENGTH_MAX - 1); \ + strncpy(tjErrorStr, _tjErrorStr, JMSG_LENGTH_MAX); \ + tjErrorStr[JMSG_LENGTH_MAX - 1] = '\0'; \ + strncpy(tjErrorMsg, m, JMSG_LENGTH_MAX); \ + tjErrorMsg[JMSG_LENGTH_MAX - 1] = '\0'; \ tjErrorCode = _tjErrorCode; \ tjErrorLine = __LINE__; \ printf("WARNING in line %d while %s:\n%s\n", __LINE__, m, _tjErrorStr); \ @@ -76,7 +82,7 @@ int tjErrorLine = -1, tjErrorCode = -1; } int flags = TJFLAG_NOREALLOC, compOnly = 0, decompOnly = 0, doYUV = 0, - quiet = 0, doTile = 0, pf = TJPF_BGR, yuvPad = 1, doWrite = 1; + quiet = 0, doTile = 0, pf = TJPF_BGR, yuvAlign = 1, doWrite = 1; char *ext = "ppm"; const char *pixFormatStr[TJ_NUMPF] = { "RGB", "BGR", "RGBX", "BGRX", "XBGR", "XRGB", "GRAY", "", "", "", "", "CMYK" @@ -101,7 +107,7 @@ static char *formatName(int subsamp, int cs, char *buf) if (cs == TJCS_YCbCr) return (char *)subNameLong[subsamp]; else if (cs == TJCS_YCCK || cs == TJCS_CMYK) { - snprintf(buf, 80, "%s %s", csName[cs], subNameLong[subsamp]); + SNPRINTF(buf, 80, "%s %s", csName[cs], subNameLong[subsamp]); return buf; } else return (char *)csName[cs]; @@ -114,10 +120,10 @@ static char *sigfig(double val, int figs, char *buf, int len) int digitsAfterDecimal = figs - (int)ceil(log10(fabs(val))); if (digitsAfterDecimal < 1) - snprintf(format, 80, "%%.0f"); + SNPRINTF(format, 80, "%%.0f"); else - snprintf(format, 80, "%%.%df", digitsAfterDecimal); - snprintf(buf, len, format, val); + SNPRINTF(format, 80, "%%.%df", digitsAfterDecimal); + SNPRINTF(buf, len, format, val); return buf; } @@ -154,7 +160,7 @@ static int decomp(unsigned char *srcBuf, unsigned char **jpegBuf, unsigned char *dstPtr, *dstPtr2, *yuvBuf = NULL; if (jpegQual > 0) { - snprintf(qualStr, 13, "_Q%d", jpegQual); + SNPRINTF(qualStr, 13, "_Q%d", jpegQual); qualStr[12] = 0; } @@ -176,7 +182,7 @@ static int decomp(unsigned char *srcBuf, unsigned char **jpegBuf, if (doYUV) { int width = doTile ? tilew : scaledw; int height = doTile ? tileh : scaledh; - unsigned long yuvSize = tjBufSizeYUV2(width, yuvPad, height, subsamp); + unsigned long yuvSize = tjBufSizeYUV2(width, yuvAlign, height, subsamp); if (yuvSize == (unsigned long)-1) THROW_TJ("allocating YUV buffer"); @@ -203,10 +209,10 @@ static int decomp(unsigned char *srcBuf, unsigned char **jpegBuf, double startDecode; if (tjDecompressToYUV2(handle, jpegBuf[tile], jpegSize[tile], yuvBuf, - width, yuvPad, height, flags) == -1) + width, yuvAlign, height, flags) == -1) THROW_TJ("executing tjDecompressToYUV2()"); startDecode = getTime(); - if (tjDecodeYUV(handle, yuvBuf, yuvPad, subsamp, dstPtr2, width, + if (tjDecodeYUV(handle, yuvBuf, yuvAlign, subsamp, dstPtr2, width, pitch, height, pf, flags) == -1) THROW_TJ("executing tjDecodeYUV()"); if (iter >= 0) elapsedDecode += getTime() - startDecode; @@ -256,23 +262,23 @@ static int decomp(unsigned char *srcBuf, unsigned char **jpegBuf, if (!doWrite) goto bailout; if (sf.num != 1 || sf.denom != 1) - snprintf(sizeStr, 24, "%d_%d", sf.num, sf.denom); + SNPRINTF(sizeStr, 24, "%d_%d", sf.num, sf.denom); else if (tilew != w || tileh != h) - snprintf(sizeStr, 24, "%dx%d", tilew, tileh); - else snprintf(sizeStr, 24, "full"); + SNPRINTF(sizeStr, 24, "%dx%d", tilew, tileh); + else SNPRINTF(sizeStr, 24, "full"); if (decompOnly) - snprintf(tempStr, 1024, "%s_%s.%s", fileName, sizeStr, ext); + SNPRINTF(tempStr, 1024, "%s_%s.%s", fileName, sizeStr, ext); else - snprintf(tempStr, 1024, "%s_%s%s_%s.%s", fileName, subName[subsamp], + SNPRINTF(tempStr, 1024, "%s_%s%s_%s.%s", fileName, subName[subsamp], qualStr, sizeStr, ext); if (tjSaveImage(tempStr, dstBuf, scaledw, 0, scaledh, pf, flags) == -1) - THROW_TJG("saving bitmap"); + THROW_TJG("saving output image"); ptr = strrchr(tempStr, '.'); - snprintf(ptr, 1024 - (ptr - tempStr), "-err.%s", ext); + SNPRINTF(ptr, 1024 - (ptr - tempStr), "-err.%s", ext); if (srcBuf && sf.num == 1 && sf.denom == 1) { if (!quiet) printf("Compression error written to %s.\n", tempStr); - if (subsamp == TJ_GRAYSCALE) { + if (subsamp == TJSAMP_GRAY) { unsigned long index, index2; for (row = 0, index = 0; row < h; row++, index += pitch) { @@ -286,19 +292,20 @@ static int decomp(unsigned char *srcBuf, unsigned char **jpegBuf, if (y > 255) y = 255; if (y < 0) y = 0; - dstBuf[rindex] = abs(dstBuf[rindex] - y); - dstBuf[gindex] = abs(dstBuf[gindex] - y); - dstBuf[bindex] = abs(dstBuf[bindex] - y); + dstBuf[rindex] = (unsigned char)abs(dstBuf[rindex] - y); + dstBuf[gindex] = (unsigned char)abs(dstBuf[gindex] - y); + dstBuf[bindex] = (unsigned char)abs(dstBuf[bindex] - y); } } } else { for (row = 0; row < h; row++) for (col = 0; col < w * ps; col++) dstBuf[pitch * row + col] = - abs(dstBuf[pitch * row + col] - srcBuf[pitch * row + col]); + (unsigned char)abs(dstBuf[pitch * row + col] - + srcBuf[pitch * row + col]); } if (tjSaveImage(tempStr, dstBuf, w, 0, h, pf, flags) == -1) - THROW_TJG("saving bitmap"); + THROW_TJG("saving output image"); } bailout: @@ -373,7 +380,7 @@ static int fullTest(unsigned char *srcBuf, int w, int h, int subsamp, THROW_TJ("executing tjInitCompress()"); if (doYUV) { - yuvSize = tjBufSizeYUV2(tilew, yuvPad, tileh, subsamp); + yuvSize = tjBufSizeYUV2(tilew, yuvAlign, tileh, subsamp); if (yuvSize == (unsigned long)-1) THROW_TJ("allocating YUV buffer"); if ((yuvBuf = (unsigned char *)malloc(yuvSize)) == NULL) @@ -400,10 +407,10 @@ static int fullTest(unsigned char *srcBuf, int w, int h, int subsamp, double startEncode = getTime(); if (tjEncodeYUV3(handle, srcPtr2, width, pitch, height, pf, yuvBuf, - yuvPad, subsamp, flags) == -1) + yuvAlign, subsamp, flags) == -1) THROW_TJ("executing tjEncodeYUV3()"); if (iter >= 0) elapsedEncode += getTime() - startEncode; - if (tjCompressFromYUV(handle, yuvBuf, width, yuvPad, height, + if (tjCompressFromYUV(handle, yuvBuf, width, yuvAlign, height, subsamp, &jpegBuf[tile], &jpegSize[tile], jpegQual, flags) == -1) THROW_TJ("executing tjCompressFromYUV()"); @@ -471,7 +478,7 @@ static int fullTest(unsigned char *srcBuf, int w, int h, int subsamp, (double)totalJpegSize * 8. / 1000000. * (double)iter / elapsed); } if (tilew == w && tileh == h && doWrite) { - snprintf(tempStr, 1024, "%s_%s_Q%d.jpg", fileName, subName[subsamp], + SNPRINTF(tempStr, 1024, "%s_%s_Q%d.jpg", fileName, subName[subsamp], jpegQual); if ((file = fopen(tempStr, "wb")) == NULL) THROW_UNIX("opening reference image"); @@ -527,8 +534,8 @@ static int decompTest(char *fileName) int ps = tjPixelSize[pf], tile, row, col, i, iter, retval = 0, decompsrc = 0; char *temp = NULL, tempStr[80], tempStr2[80]; /* Original image */ - int w = 0, h = 0, tilew, tileh, ntilesw = 1, ntilesh = 1, subsamp = -1, - cs = -1; + int w = 0, h = 0, minTile, tilew, tileh, ntilesw = 1, ntilesh = 1, + subsamp = -1, cs = -1; /* Transformed image */ int tw, th, ttilew, ttileh, tntilesw, tntilesh, tsubsamp; @@ -561,7 +568,7 @@ static int decompTest(char *fileName) if (quiet == 1) { printf("All performance values in Mpixels/sec\n\n"); - printf("Bitmap JPEG JPEG %s %s Xform Comp Decomp ", + printf("Pixel JPEG JPEG %s %s Xform Comp Decomp ", doTile ? "Tile " : "Image", doTile ? "Tile " : "Image"); if (doYUV) printf("Decode"); printf("\n"); @@ -573,7 +580,8 @@ static int decompTest(char *fileName) formatName(subsamp, cs, tempStr), pixFormatStr[pf], (flags & TJFLAG_BOTTOMUP) ? "Bottom-up" : "Top-down"); - for (tilew = doTile ? 16 : w, tileh = doTile ? 16 : h; ; + minTile = max(tjMCUWidth[subsamp], tjMCUHeight[subsamp]); + for (tilew = doTile ? minTile : w, tileh = doTile ? minTile : h; ; tilew *= 2, tileh *= 2) { if (tilew > w) tilew = w; if (tileh > h) tileh = h; @@ -592,10 +600,16 @@ static int decompTest(char *fileName) if ((flags & TJFLAG_NOREALLOC) != 0 && (doTile || xformOp != TJXOP_NONE || xformOpt != 0 || customFilter)) for (i = 0; i < ntilesw * ntilesh; i++) { - if (tjBufSize(tilew, tileh, subsamp) > (unsigned long)INT_MAX) + unsigned long jpegBufSize; + + if (xformOp == TJXOP_TRANSPOSE || xformOp == TJXOP_TRANSVERSE || + xformOp == TJXOP_ROT90 || xformOp == TJXOP_ROT270) + jpegBufSize = tjBufSize(tileh, tilew, subsamp); + else + jpegBufSize = tjBufSize(tilew, tileh, subsamp); + if (jpegBufSize > (unsigned long)INT_MAX) THROW("getting buffer size", "Image is too large"); - if ((jpegBuf[i] = (unsigned char *) - tjAlloc(tjBufSize(tilew, tileh, subsamp))) == NULL) + if ((jpegBuf[i] = (unsigned char *)tjAlloc(jpegBufSize)) == NULL) THROW_UNIX("allocating JPEG tiles"); } @@ -623,7 +637,7 @@ static int decompTest(char *fileName) tw = h; th = w; ttilew = tileh; ttileh = tilew; } - if (xformOpt & TJXOPT_GRAY) tsubsamp = TJ_GRAYSCALE; + if (xformOpt & TJXOPT_GRAY) tsubsamp = TJSAMP_GRAY; if (xformOp == TJXOP_HFLIP || xformOp == TJXOP_ROT180) tw = tw - (tw % tjMCUWidth[tsubsamp]); if (xformOp == TJXOP_VFLIP || xformOp == TJXOP_ROT180) @@ -685,7 +699,7 @@ static int decompTest(char *fileName) sigfig((double)(w * h * ps) / (double)totalJpegSize, 4, tempStr2, 80), quiet == 2 ? "\n" : " "); - } else if (!quiet) { + } else { printf("Transform --> Frame rate: %f fps\n", 1.0 / elapsed); printf(" Output image size: %lu bytes\n", @@ -743,38 +757,34 @@ static void usage(char *progName) int i; printf("USAGE: %s\n", progName); - printf(" [options]\n\n"); + printf(" [options]\n\n"); printf(" %s\n", progName); - printf(" [options]\n\n"); + printf(" [options]\n\n"); printf("Options:\n\n"); - printf("-alloc = Dynamically allocate JPEG image buffers\n"); - printf("-bmp = Generate output images in Windows Bitmap format (default = PPM)\n"); - printf("-bottomup = Test bottom-up compression/decompression\n"); - printf("-tile = Test performance of the codec when the image is encoded as separate\n"); - printf(" tiles of varying sizes.\n"); + printf("-alloc = Dynamically allocate JPEG buffers\n"); + printf("-bmp = Use Windows Bitmap format for output images [default = PPM]\n"); + printf("-bottomup = Use bottom-up row order for packed-pixel source/destination buffers\n"); + printf("-tile = Compress/transform the input image into separate JPEG tiles of varying\n"); + printf(" sizes (useful for measuring JPEG overhead)\n"); printf("-rgb, -bgr, -rgbx, -bgrx, -xbgr, -xrgb =\n"); - printf(" Test the specified color conversion path in the codec (default = BGR)\n"); - printf("-cmyk = Indirectly test YCCK JPEG compression/decompression (the source\n"); - printf(" and destination bitmaps are still RGB. The conversion is done\n"); - printf(" internally prior to compression or after decompression.)\n"); - printf("-fastupsample = Use the fastest chrominance upsampling algorithm available in\n"); - printf(" the underlying codec\n"); - printf("-fastdct = Use the fastest DCT/IDCT algorithms available in the underlying\n"); - printf(" codec\n"); - printf("-accuratedct = Use the most accurate DCT/IDCT algorithms available in the\n"); - printf(" underlying codec\n"); + printf(" Use the specified pixel format for packed-pixel source/destination buffers\n"); + printf(" [default = BGR]\n"); + printf("-cmyk = Indirectly test YCCK JPEG compression/decompression\n"); + printf(" (use the CMYK pixel format for packed-pixel source/destination buffers)\n"); + printf("-fastupsample = Use the fastest chrominance upsampling algorithm available\n"); + printf("-fastdct = Use the fastest DCT/IDCT algorithm available\n"); + printf("-accuratedct = Use the most accurate DCT/IDCT algorithm available\n"); printf("-progressive = Use progressive entropy coding in JPEG images generated by\n"); - printf(" compression and transform operations.\n"); - printf("-subsamp = When testing JPEG compression, this option specifies the level\n"); - printf(" of chrominance subsampling to use ( = 444, 422, 440, 420, 411, or\n"); - printf(" GRAY). The default is to test Grayscale, 4:2:0, 4:2:2, and 4:4:4 in\n"); - printf(" sequence.\n"); + printf(" compression and transform operations\n"); + printf("-subsamp = When compressing, use the specified level of chrominance\n"); + printf(" subsampling ( = 444, 422, 440, 420, 411, or GRAY) [default = test\n"); + printf(" Grayscale, 4:2:0, 4:2:2, and 4:4:4 in sequence]\n"); printf("-quiet = Output results in tabular rather than verbose format\n"); - printf("-yuv = Test YUV encoding/decoding functions\n"); - printf("-yuvpad

    = If testing YUV encoding/decoding, this specifies the number of\n"); - printf(" bytes to which each row of each plane in the intermediate YUV image is\n"); - printf(" padded (default = 1)\n"); - printf("-scale M/N = Scale down the width/height of the decompressed JPEG image by a\n"); + printf("-yuv = Compress from/decompress to intermediate planar YUV images\n"); + printf("-yuvpad

    = The number of bytes by which each row in each plane of an\n"); + printf(" intermediate YUV image is evenly divisible (must be a power of 2)\n"); + printf(" [default = 1]\n"); + printf("-scale M/N = When decompressing, scale the width/height of the JPEG image by a\n"); printf(" factor of M/N (M/N = "); for (i = 0; i < nsf; i++) { printf("%d/%d", scalingFactors[i].num, scalingFactors[i].denom); @@ -787,22 +797,24 @@ static void usage(char *progName) } printf(")\n"); printf("-hflip, -vflip, -transpose, -transverse, -rot90, -rot180, -rot270 =\n"); - printf(" Perform the corresponding lossless transform prior to\n"); - printf(" decompression (these options are mutually exclusive)\n"); - printf("-grayscale = Perform lossless grayscale conversion prior to decompression\n"); - printf(" test (can be combined with the other transforms above)\n"); + printf(" Perform the specified lossless transform operation on the input image\n"); + printf(" prior to decompression (these operations are mutually exclusive)\n"); + printf("-grayscale = Transform the input image into a grayscale JPEG image prior to\n"); + printf(" decompression (can be combined with the other transform operations above)\n"); printf("-copynone = Do not copy any extra markers (including EXIF and ICC profile data)\n"); - printf(" when transforming the image.\n"); - printf("-benchtime = Run each benchmark for at least seconds (default = 5.0)\n"); - printf("-warmup = Run each benchmark for seconds (default = 1.0) prior to\n"); + printf(" when transforming the input image\n"); + printf("-benchtime = Run each benchmark for at least seconds [default = 5.0]\n"); + printf("-warmup = Run each benchmark for seconds [default = 1.0] prior to\n"); printf(" starting the timer, in order to prime the caches and thus improve the\n"); - printf(" consistency of the results.\n"); + printf(" consistency of the benchmark results\n"); printf("-componly = Stop after running compression tests. Do not test decompression.\n"); printf("-nowrite = Do not write reference or output images (improves consistency of\n"); - printf(" performance measurements.)\n"); + printf(" benchmark results)\n"); + printf("-limitscans = Refuse to decompress or transform progressive JPEG images that\n"); + printf(" have an unreasonably large number of scans\n"); printf("-stoponwarning = Immediately discontinue the current\n"); - printf(" compression/decompression/transform operation if the underlying codec\n"); - printf(" throws a warning (non-fatal error)\n\n"); + printf(" compression/decompression/transform operation if a warning (non-fatal\n"); + printf(" error) occurs\n\n"); printf("NOTE: If the quality is specified as a range (e.g. 90-100), a separate\n"); printf("test will be performed for all quality values in the range.\n\n"); exit(1); @@ -848,7 +860,7 @@ int main(int argc, char *argv[]) if (!strcasecmp(argv[i], "-tile")) { doTile = 1; xformOpt |= TJXOPT_CROP; } else if (!strcasecmp(argv[i], "-fastupsample")) { - printf("Using fast upsampling code\n\n"); + printf("Using fastest upsampling algorithm\n\n"); flags |= TJFLAG_FASTUPSAMPLE; } else if (!strcasecmp(argv[i], "-fastdct")) { printf("Using fastest DCT/IDCT algorithm\n\n"); @@ -859,6 +871,7 @@ int main(int argc, char *argv[]) } else if (!strcasecmp(argv[i], "-progressive")) { printf("Using progressive entropy coding\n\n"); flags |= TJFLAG_PROGRESSIVE; + xformOpt |= TJXOPT_PROGRESSIVE; } else if (!strcasecmp(argv[i], "-rgb")) pf = TJPF_RGB; else if (!strcasecmp(argv[i], "-rgbx")) @@ -931,12 +944,13 @@ int main(int argc, char *argv[]) else if (!strcasecmp(argv[i], "-bmp")) ext = "bmp"; else if (!strcasecmp(argv[i], "-yuv")) { - printf("Testing YUV planar encoding/decoding\n\n"); + printf("Testing planar YUV encoding/decoding\n\n"); doYUV = 1; } else if (!strcasecmp(argv[i], "-yuvpad") && i < argc - 1) { int tempi = atoi(argv[++i]); - if (tempi >= 1) yuvPad = tempi; + if (tempi >= 1 && (tempi & (tempi - 1)) == 0) yuvAlign = tempi; + else usage(argv[0]); } else if (!strcasecmp(argv[i], "-subsamp") && i < argc - 1) { i++; if (toupper(argv[i][0]) == 'G') subsamp = TJSAMP_GRAY; @@ -949,12 +963,15 @@ int main(int argc, char *argv[]) case 440: subsamp = TJSAMP_440; break; case 420: subsamp = TJSAMP_420; break; case 411: subsamp = TJSAMP_411; break; + default: usage(argv[0]); } } } else if (!strcasecmp(argv[i], "-componly")) compOnly = 1; else if (!strcasecmp(argv[i], "-nowrite")) doWrite = 0; + else if (!strcasecmp(argv[i], "-limitscans")) + flags |= TJFLAG_LIMITSCANS; else if (!strcasecmp(argv[i], "-stoponwarning")) flags |= TJFLAG_STOPONWARNING; else usage(argv[0]); @@ -963,26 +980,26 @@ int main(int argc, char *argv[]) if ((sf.num != 1 || sf.denom != 1) && doTile) { printf("Disabling tiled compression/decompression tests, because those tests do not\n"); - printf("work when scaled decompression is enabled.\n"); - doTile = 0; + printf("work when scaled decompression is enabled.\n\n"); + doTile = 0; xformOpt &= (~TJXOPT_CROP); } if ((flags & TJFLAG_NOREALLOC) == 0 && doTile) { printf("Disabling tiled compression/decompression tests, because those tests do not\n"); printf("work when dynamic JPEG buffer allocation is enabled.\n\n"); - doTile = 0; + doTile = 0; xformOpt &= (~TJXOPT_CROP); } if (!decompOnly) { if ((srcBuf = tjLoadImage(argv[1], &w, 1, &h, &pf, flags)) == NULL) - THROW_TJG("loading bitmap"); + THROW_TJG("loading input image"); temp = strrchr(argv[1], '.'); if (temp != NULL) *temp = '\0'; } if (quiet == 1 && !decompOnly) { printf("All performance values in Mpixels/sec\n\n"); - printf("Bitmap JPEG JPEG %s %s ", + printf("Pixel JPEG JPEG %s %s ", doTile ? "Tile " : "Image", doTile ? "Tile " : "Image"); if (doYUV) printf("Encode "); printf("Comp Comp Decomp "); diff --git a/third-party/mozjpeg/mozjpeg/tjexample.c b/third-party/mozjpeg/mozjpeg/tjexample.c index ef32c939a91..4b0e6d669f8 100644 --- a/third-party/mozjpeg/mozjpeg/tjexample.c +++ b/third-party/mozjpeg/mozjpeg/tjexample.c @@ -1,6 +1,6 @@ /* - * Copyright (C)2011-2012, 2014-2015, 2017, 2019 D. R. Commander. - * All Rights Reserved. + * Copyright (C)2011-2012, 2014-2015, 2017, 2019, 2021-2023 + * D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -32,6 +32,11 @@ * images using the TurboJPEG C API */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#include #include #include #include @@ -145,14 +150,11 @@ static void usage(char *programName) printf("General Options\n"); printf("---------------\n\n"); - printf("-fastupsample = Use the fastest chrominance upsampling algorithm available in\n"); - printf(" the underlying codec.\n\n"); + printf("-fastupsample = Use the fastest chrominance upsampling algorithm available\n\n"); - printf("-fastdct = Use the fastest DCT/IDCT algorithms available in the underlying\n"); - printf(" codec.\n\n"); + printf("-fastdct = Use the fastest DCT/IDCT algorithm available\n\n"); - printf("-accuratedct = Use the most accurate DCT/IDCT algorithms available in the\n"); - printf(" underlying codec.\n\n"); + printf("-accuratedct = Use the most accurate DCT/IDCT algorithm available\n\n"); exit(1); } @@ -273,6 +275,8 @@ int main(int argc, char **argv) if (size == 0) THROW("determining input file size", "Input file contains no data"); jpegSize = (unsigned long)size; + if (jpegSize > (unsigned long)INT_MAX) + THROW("allocating JPEG buffer", "Input file is too large"); if ((jpegBuf = (unsigned char *)tjAlloc(jpegSize)) == NULL) THROW_UNIX("allocating JPEG buffer"); if (fread(jpegBuf, jpegSize, 1, jpegFile) < 1) @@ -288,8 +292,10 @@ int main(int argc, char **argv) THROW_TJ("initializing transformer"); xform.options |= TJXOPT_TRIM; if (tjTransform(tjInstance, jpegBuf, jpegSize, 1, &dstBuf, &dstSize, - &xform, flags) < 0) + &xform, flags) < 0) { + tjFree(dstBuf); THROW_TJ("transforming input image"); + } tjFree(jpegBuf); jpegBuf = dstBuf; jpegSize = dstSize; @@ -328,8 +334,12 @@ int main(int argc, char **argv) outSubsamp = inSubsamp; pixelFormat = TJPF_BGRX; - if ((imgBuf = (unsigned char *)tjAlloc(width * height * - tjPixelSize[pixelFormat])) == NULL) + if ((unsigned long long)width * height * tjPixelSize[pixelFormat] > + (unsigned long long)((size_t)-1)) + THROW("allocating uncompressed image buffer", "Image is too large"); + if ((imgBuf = + (unsigned char *)malloc(sizeof(unsigned char) * width * height * + tjPixelSize[pixelFormat])) == NULL) THROW_UNIX("allocating uncompressed image buffer"); if (tjDecompress2(tjInstance, jpegBuf, jpegSize, imgBuf, width, 0, height, diff --git a/third-party/mozjpeg/mozjpeg/tjunittest.c b/third-party/mozjpeg/mozjpeg/tjunittest.c index f59939fdb04..165dff13c26 100644 --- a/third-party/mozjpeg/mozjpeg/tjunittest.c +++ b/third-party/mozjpeg/mozjpeg/tjunittest.c @@ -1,5 +1,6 @@ /* - * Copyright (C)2009-2014, 2017-2019 D. R. Commander. All Rights Reserved. + * Copyright (C)2009-2014, 2017-2019, 2022-2023 D. R. Commander. + * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -30,9 +31,14 @@ * This program tests the various code paths in the TurboJPEG C Wrapper */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #include #include #include +#include #include #include "tjutil.h" #include "turbojpeg.h" @@ -50,11 +56,11 @@ static void usage(char *progName) { printf("\nUSAGE: %s [options]\n\n", progName); printf("Options:\n"); - printf("-yuv = test YUV encoding/decoding support\n"); - printf("-noyuvpad = do not pad each line of each Y, U, and V plane to the nearest\n"); - printf(" 4-byte boundary\n"); - printf("-alloc = test automatic buffer allocation\n"); - printf("-bmp = tjLoadImage()/tjSaveImage() unit test\n\n"); + printf("-yuv = test YUV encoding/compression/decompression/decoding\n"); + printf("-noyuvpad = do not pad each row in each Y, U, and V plane to the nearest\n"); + printf(" multiple of 4 bytes\n"); + printf("-alloc = test automatic JPEG buffer allocation\n"); + printf("-bmp = test packed-pixel image I/O\n"); exit(1); } @@ -90,7 +96,7 @@ const int _4byteFormats[] = { const int _onlyGray[] = { TJPF_GRAY }; const int _onlyRGB[] = { TJPF_RGB }; -int doYUV = 0, alloc = 0, pad = 4; +int doYUV = 0, alloc = 0, yuvAlign = 4; int exitStatus = 0; #define BAILOUT() { exitStatus = -1; goto bailout; } @@ -277,7 +283,7 @@ static int checkBufYUV(unsigned char *buf, int w, int h, int subsamp, int hsf = tjMCUWidth[subsamp] / 8, vsf = tjMCUHeight[subsamp] / 8; int pw = PAD(w, hsf), ph = PAD(h, vsf); int cw = pw / hsf, ch = ph / vsf; - int ypitch = PAD(pw, pad), uvpitch = PAD(cw, pad); + int ypitch = PAD(pw, yuvAlign), uvpitch = PAD(cw, yuvAlign); int retval = 1; int halfway = 16 * sf.num / sf.denom; int blocksize = 8 * sf.num / sf.denom; @@ -376,7 +382,7 @@ static void compTest(tjhandle handle, unsigned char **dstBuf, if (!alloc) flags |= TJFLAG_NOREALLOC; if (doYUV) { - unsigned long yuvSize = tjBufSizeYUV2(w, pad, h, subsamp); + unsigned long yuvSize = tjBufSizeYUV2(w, yuvAlign, h, subsamp); tjscalingfactor sf = { 1, 1 }; tjhandle handle2 = tjInitCompress(); @@ -387,15 +393,15 @@ static void compTest(tjhandle handle, unsigned char **dstBuf, memset(yuvBuf, 0, yuvSize); printf("%s %s -> YUV %s ... ", pfStr, buStrLong, subNameLong[subsamp]); - TRY_TJ(tjEncodeYUV3(handle2, srcBuf, w, 0, h, pf, yuvBuf, pad, subsamp, - flags)); + TRY_TJ(tjEncodeYUV3(handle2, srcBuf, w, 0, h, pf, yuvBuf, yuvAlign, + subsamp, flags)); tjDestroy(handle2); if (checkBufYUV(yuvBuf, w, h, subsamp, sf)) printf("Passed.\n"); else printf("FAILED!\n"); printf("YUV %s %s -> JPEG Q%d ... ", subNameLong[subsamp], buStrLong, jpegQual); - TRY_TJ(tjCompressFromYUV(handle, yuvBuf, w, pad, h, subsamp, dstBuf, + TRY_TJ(tjCompressFromYUV(handle, yuvBuf, w, yuvAlign, h, subsamp, dstBuf, dstSize, jpegQual, flags)); } else { printf("%s %s -> %s Q%d ... ", pfStr, buStrLong, subNameLong[subsamp], @@ -404,7 +410,7 @@ static void compTest(tjhandle handle, unsigned char **dstBuf, jpegQual, flags)); } - snprintf(tempStr, 1024, "%s_enc_%s_%s_%s_Q%d.jpg", basename, pfStr, buStr, + SNPRINTF(tempStr, 1024, "%s_enc_%s_%s_%s_Q%d.jpg", basename, pfStr, buStr, subName[subsamp], jpegQual); writeJPEG(*dstBuf, *dstSize, tempStr); printf("Done.\n Result in %s\n", tempStr); @@ -437,7 +443,7 @@ static void _decompTest(tjhandle handle, unsigned char *jpegBuf, memset(dstBuf, 0, dstSize); if (doYUV) { - unsigned long yuvSize = tjBufSizeYUV2(scaledWidth, pad, scaledHeight, + unsigned long yuvSize = tjBufSizeYUV2(scaledWidth, yuvAlign, scaledHeight, subsamp); tjhandle handle2 = tjInitDecompress(); @@ -451,16 +457,20 @@ static void _decompTest(tjhandle handle, unsigned char *jpegBuf, if (sf.num != 1 || sf.denom != 1) printf("%d/%d ... ", sf.num, sf.denom); else printf("... "); - TRY_TJ(tjDecompressToYUV2(handle, jpegBuf, jpegSize, yuvBuf, scaledWidth, - pad, scaledHeight, flags)); + /* We pass scaledWidth + 1 and scaledHeight + 1 to validate that + tjDecompressToYUV2() generates the largest possible scaled image that + fits within the desired dimensions, as documented. */ + TRY_TJ(tjDecompressToYUV2(handle, jpegBuf, jpegSize, yuvBuf, + scaledWidth + 1, yuvAlign, scaledHeight + 1, + flags)); if (checkBufYUV(yuvBuf, scaledWidth, scaledHeight, subsamp, sf)) printf("Passed.\n"); else printf("FAILED!\n"); printf("YUV %s -> %s %s ... ", subNameLong[subsamp], pixFormatStr[pf], (flags & TJFLAG_BOTTOMUP) ? "Bottom-Up" : "Top-Down "); - TRY_TJ(tjDecodeYUV(handle2, yuvBuf, pad, subsamp, dstBuf, scaledWidth, 0, - scaledHeight, pf, flags)); + TRY_TJ(tjDecodeYUV(handle2, yuvBuf, yuvAlign, subsamp, dstBuf, scaledWidth, + 0, scaledHeight, pf, flags)); tjDestroy(handle2); } else { printf("JPEG -> %s %s ", pixFormatStr[pf], @@ -468,8 +478,11 @@ static void _decompTest(tjhandle handle, unsigned char *jpegBuf, if (sf.num != 1 || sf.denom != 1) printf("%d/%d ... ", sf.num, sf.denom); else printf("... "); - TRY_TJ(tjDecompress2(handle, jpegBuf, jpegSize, dstBuf, scaledWidth, 0, - scaledHeight, pf, flags)); + /* We pass scaledWidth + 1 and scaledHeight + 1 to validate that + tjDecompress2() generates the largest possible scaled image that fits + within the desired dimensions, as documented. */ + TRY_TJ(tjDecompress2(handle, jpegBuf, jpegSize, dstBuf, scaledWidth + 1, 0, + scaledHeight + 1, pf, flags)); } if (checkBuf(dstBuf, scaledWidth, scaledHeight, pf, subsamp, sf, flags)) @@ -566,11 +579,16 @@ static void doTest(int w, int h, const int *formats, int nformats, int subsamp, THROW(#function " overflow"); \ } #endif +#define CHECKSIZEINT(function) { \ + if (intsize != -1 || !strcmp(tjGetErrorStr2(NULL), "No error")) \ + THROW(#function " overflow"); \ +} static void overflowTest(void) { /* Ensure that the various buffer size functions don't overflow */ unsigned long size; + int intsize; size = tjBufSize(26755, 26755, TJSAMP_444); CHECKSIZE(tjBufSize()); @@ -578,12 +596,20 @@ static void overflowTest(void) CHECKSIZE(TJBUFSIZE()); size = tjBufSizeYUV2(37838, 1, 37838, TJSAMP_444); CHECKSIZE(tjBufSizeYUV2()); + size = tjBufSizeYUV2(37837, 3, 37837, TJSAMP_444); + CHECKSIZE(tjBufSizeYUV2()); + size = tjBufSizeYUV2(37837, -1, 37837, TJSAMP_444); + CHECKSIZE(tjBufSizeYUV2()); size = TJBUFSIZEYUV(37838, 37838, TJSAMP_444); CHECKSIZE(TJBUFSIZEYUV()); size = tjBufSizeYUV(37838, 37838, TJSAMP_444); CHECKSIZE(tjBufSizeYUV()); size = tjPlaneSizeYUV(0, 65536, 0, 65536, TJSAMP_444); CHECKSIZE(tjPlaneSizeYUV()); + intsize = tjPlaneWidth(0, INT_MAX, TJSAMP_420); + CHECKSIZEINT(tjPlaneWidth()); + intsize = tjPlaneHeight(0, INT_MAX, TJSAMP_420); + CHECKSIZEINT(tjPlaneHeight()); bailout: return; @@ -609,7 +635,7 @@ static void bufSizeTest(void) if ((srcBuf = (unsigned char *)malloc(w * h * 4)) == NULL) THROW("Memory allocation failure"); if (!alloc || doYUV) { - if (doYUV) dstSize = tjBufSizeYUV2(w, pad, h, subsamp); + if (doYUV) dstSize = tjBufSizeYUV2(w, yuvAlign, h, subsamp); else dstSize = tjBufSize(w, h, subsamp); if ((dstBuf = (unsigned char *)tjAlloc(dstSize)) == NULL) THROW("Memory allocation failure"); @@ -621,8 +647,8 @@ static void bufSizeTest(void) } if (doYUV) { - TRY_TJ(tjEncodeYUV3(handle, srcBuf, w, 0, h, TJPF_BGRX, dstBuf, pad, - subsamp, 0)); + TRY_TJ(tjEncodeYUV3(handle, srcBuf, w, 0, h, TJPF_BGRX, dstBuf, + yuvAlign, subsamp, 0)); } else { TRY_TJ(tjCompress2(handle, srcBuf, w, 0, h, TJPF_BGRX, &dstBuf, &dstSize, subsamp, 100, @@ -636,7 +662,7 @@ static void bufSizeTest(void) if ((srcBuf = (unsigned char *)malloc(h * w * 4)) == NULL) THROW("Memory allocation failure"); if (!alloc || doYUV) { - if (doYUV) dstSize = tjBufSizeYUV2(h, pad, w, subsamp); + if (doYUV) dstSize = tjBufSizeYUV2(h, yuvAlign, w, subsamp); else dstSize = tjBufSize(h, w, subsamp); if ((dstBuf = (unsigned char *)tjAlloc(dstSize)) == NULL) THROW("Memory allocation failure"); @@ -648,8 +674,8 @@ static void bufSizeTest(void) } if (doYUV) { - TRY_TJ(tjEncodeYUV3(handle, srcBuf, h, 0, w, TJPF_BGRX, dstBuf, pad, - subsamp, 0)); + TRY_TJ(tjEncodeYUV3(handle, srcBuf, h, 0, w, TJPF_BGRX, dstBuf, + yuvAlign, subsamp, 0)); } else { TRY_TJ(tjCompress2(handle, srcBuf, h, 0, w, TJPF_BGRX, &dstBuf, &dstSize, subsamp, 100, @@ -777,7 +803,7 @@ static int doBmpTest(const char *ext, int width, int align, int height, int pf, THROW("Could not allocate memory"); initBitmap(buf, width, pitch, height, pf, flags); - snprintf(filename, 80, "test_bmp_%s_%d_%s.%s", pixFormatStr[pf], align, + SNPRINTF(filename, 80, "test_bmp_%s_%d_%s.%s", pixFormatStr[pf], align, (flags & TJFLAG_BOTTOMUP) ? "bu" : "td", ext); TRY_TJ(tjSaveImage(filename, buf, width, pitch, height, pf, flags)); md5sum = MD5File(filename, md5buf); @@ -882,9 +908,13 @@ static int bmpTest(void) return 0; } +#ifdef _WIN32 +#define setenv(envvar, value, dummy) _putenv_s(envvar, value) +#endif int main(int argc, char *argv[]) { + setenv("TJ_REVERT", "1", 1); int i, num4bf = 5; #ifdef _WIN32 @@ -893,7 +923,7 @@ int main(int argc, char *argv[]) if (argc > 1) { for (i = 1; i < argc; i++) { if (!strcasecmp(argv[i], "-yuv")) doYUV = 1; - else if (!strcasecmp(argv[i], "-noyuvpad")) pad = 1; + else if (!strcasecmp(argv[i], "-noyuvpad")) yuvAlign = 1; else if (!strcasecmp(argv[i], "-alloc")) alloc = 1; else if (!strcasecmp(argv[i], "-bmp")) return bmpTest(); else usage(argv[0]); diff --git a/third-party/mozjpeg/mozjpeg/tjutil.h b/third-party/mozjpeg/mozjpeg/tjutil.h index f72840ceeba..10272e98867 100644 --- a/third-party/mozjpeg/mozjpeg/tjutil.h +++ b/third-party/mozjpeg/mozjpeg/tjutil.h @@ -1,5 +1,5 @@ /* - * Copyright (C)2011 D. R. Commander. All Rights Reserved. + * Copyright (C)2011, 2022 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -27,14 +27,20 @@ */ #ifdef _WIN32 -#ifndef __MINGW32__ -#include -#define snprintf(str, n, format, ...) \ - _snprintf_s(str, n, _TRUNCATE, format, __VA_ARGS__) -#endif +#ifndef strcasecmp #define strcasecmp stricmp +#endif +#ifndef strncasecmp #define strncasecmp strnicmp #endif +#endif + +#ifdef _MSC_VER +#define SNPRINTF(str, n, format, ...) \ + _snprintf_s(str, n, _TRUNCATE, format, ##__VA_ARGS__) +#else +#define SNPRINTF snprintf +#endif #ifndef min #define min(a, b) ((a) < (b) ? (a) : (b)) diff --git a/third-party/mozjpeg/mozjpeg/transupp.c b/third-party/mozjpeg/mozjpeg/transupp.c index f3370ace59d..78dc91b4af9 100644 --- a/third-party/mozjpeg/mozjpeg/transupp.c +++ b/third-party/mozjpeg/mozjpeg/transupp.c @@ -2,9 +2,9 @@ * transupp.c * * This file was part of the Independent JPEG Group's software: - * Copyright (C) 1997-2011, Thomas G. Lane, Guido Vollbeding. + * Copyright (C) 1997-2019, Thomas G. Lane, Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2010, 2017, D. R. Commander. + * Copyright (C) 2010, 2017, 2021-2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -88,6 +88,189 @@ */ +LOCAL(void) +dequant_comp(j_decompress_ptr cinfo, jpeg_component_info *compptr, + jvirt_barray_ptr coef_array, JQUANT_TBL *qtblptr1) +{ + JDIMENSION blk_x, blk_y; + int offset_y, k; + JQUANT_TBL *qtblptr; + JBLOCKARRAY buffer; + JBLOCKROW block; + JCOEFPTR ptr; + + qtblptr = compptr->quant_table; + for (blk_y = 0; blk_y < compptr->height_in_blocks; + blk_y += compptr->v_samp_factor) { + buffer = (*cinfo->mem->access_virt_barray) + ((j_common_ptr)cinfo, coef_array, blk_y, + (JDIMENSION)compptr->v_samp_factor, TRUE); + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + block = buffer[offset_y]; + for (blk_x = 0; blk_x < compptr->width_in_blocks; blk_x++) { + ptr = block[blk_x]; + for (k = 0; k < DCTSIZE2; k++) + if (qtblptr->quantval[k] != qtblptr1->quantval[k]) + ptr[k] *= qtblptr->quantval[k] / qtblptr1->quantval[k]; + } + } + } +} + + +LOCAL(void) +requant_comp(j_decompress_ptr cinfo, jpeg_component_info *compptr, + jvirt_barray_ptr coef_array, JQUANT_TBL *qtblptr1) +{ + JDIMENSION blk_x, blk_y; + int offset_y, k; + JQUANT_TBL *qtblptr; + JBLOCKARRAY buffer; + JBLOCKROW block; + JCOEFPTR ptr; + JCOEF temp, qval; + + qtblptr = compptr->quant_table; + for (blk_y = 0; blk_y < compptr->height_in_blocks; + blk_y += compptr->v_samp_factor) { + buffer = (*cinfo->mem->access_virt_barray) + ((j_common_ptr)cinfo, coef_array, blk_y, + (JDIMENSION)compptr->v_samp_factor, TRUE); + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + block = buffer[offset_y]; + for (blk_x = 0; blk_x < compptr->width_in_blocks; blk_x++) { + ptr = block[blk_x]; + for (k = 0; k < DCTSIZE2; k++) { + temp = qtblptr->quantval[k]; + qval = qtblptr1->quantval[k]; + if (temp != qval && qval != 0) { + temp *= ptr[k]; + /* The following quantization code is copied from jcdctmgr.c */ +#ifdef FAST_DIVIDE +#define DIVIDE_BY(a, b) a /= b +#else +#define DIVIDE_BY(a, b) if (a >= b) a /= b; else a = 0 +#endif + if (temp < 0) { + temp = -temp; + temp += qval >> 1; /* for rounding */ + DIVIDE_BY(temp, qval); + temp = -temp; + } else { + temp += qval >> 1; /* for rounding */ + DIVIDE_BY(temp, qval); + } + ptr[k] = temp; + } + } + } + } + } +} + + +/* + * Calculate largest common denominator using Euclid's algorithm. + */ +LOCAL(JCOEF) +largest_common_denominator(JCOEF a, JCOEF b) +{ + JCOEF c; + + do { + c = a % b; + a = b; + b = c; + } while (c); + + return a; +} + + +LOCAL(void) +adjust_quant(j_decompress_ptr srcinfo, jvirt_barray_ptr *src_coef_arrays, + j_decompress_ptr dropinfo, jvirt_barray_ptr *drop_coef_arrays, + boolean trim, j_compress_ptr dstinfo) +{ + jpeg_component_info *compptr1, *compptr2; + JQUANT_TBL *qtblptr1, *qtblptr2, *qtblptr3; + int ci, k; + + for (ci = 0; ci < dstinfo->num_components && ci < dropinfo->num_components; + ci++) { + compptr1 = srcinfo->comp_info + ci; + compptr2 = dropinfo->comp_info + ci; + qtblptr1 = compptr1->quant_table; + qtblptr2 = compptr2->quant_table; + for (k = 0; k < DCTSIZE2; k++) { + if (qtblptr1->quantval[k] != qtblptr2->quantval[k]) { + if (trim) + requant_comp(dropinfo, compptr2, drop_coef_arrays[ci], qtblptr1); + else { + qtblptr3 = dstinfo->quant_tbl_ptrs[compptr1->quant_tbl_no]; + for (k = 0; k < DCTSIZE2; k++) + if (qtblptr1->quantval[k] != qtblptr2->quantval[k]) + qtblptr3->quantval[k] = + largest_common_denominator(qtblptr1->quantval[k], + qtblptr2->quantval[k]); + dequant_comp(srcinfo, compptr1, src_coef_arrays[ci], qtblptr3); + dequant_comp(dropinfo, compptr2, drop_coef_arrays[ci], qtblptr3); + } + break; + } + } + } +} + + +LOCAL(void) +do_drop(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, + JDIMENSION x_crop_offset, JDIMENSION y_crop_offset, + jvirt_barray_ptr *src_coef_arrays, + j_decompress_ptr dropinfo, jvirt_barray_ptr *drop_coef_arrays, + JDIMENSION drop_width, JDIMENSION drop_height) +/* Drop (insert) the contents of another image into the source image. If the + * number of components in the drop image is smaller than the number of + * components in the destination image, then we fill in the remaining + * components with zero. This allows for dropping the contents of grayscale + * images into (arbitrarily sampled) color images. + */ +{ + JDIMENSION comp_width, comp_height; + JDIMENSION blk_y, x_drop_blocks, y_drop_blocks; + int ci, offset_y; + JBLOCKARRAY src_buffer, dst_buffer; + jpeg_component_info *compptr; + + for (ci = 0; ci < dstinfo->num_components; ci++) { + compptr = dstinfo->comp_info + ci; + comp_width = drop_width * compptr->h_samp_factor; + comp_height = drop_height * compptr->v_samp_factor; + x_drop_blocks = x_crop_offset * compptr->h_samp_factor; + y_drop_blocks = y_crop_offset * compptr->v_samp_factor; + for (blk_y = 0; blk_y < comp_height; blk_y += compptr->v_samp_factor) { + dst_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], blk_y + y_drop_blocks, + (JDIMENSION)compptr->v_samp_factor, TRUE); + if (ci < dropinfo->num_components) { + src_buffer = (*dropinfo->mem->access_virt_barray) + ((j_common_ptr)dropinfo, drop_coef_arrays[ci], blk_y, + (JDIMENSION)compptr->v_samp_factor, FALSE); + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + jcopy_block_row(src_buffer[offset_y], + dst_buffer[offset_y] + x_drop_blocks, comp_width); + } + } else { + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + memset(dst_buffer[offset_y] + x_drop_blocks, 0, + comp_width * sizeof(JBLOCK)); + } + } + } + } +} + + LOCAL(void) do_crop(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, JDIMENSION x_crop_offset, JDIMENSION y_crop_offset, @@ -113,13 +296,424 @@ do_crop(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, ((j_common_ptr)srcinfo, dst_coef_arrays[ci], dst_blk_y, (JDIMENSION)compptr->v_samp_factor, TRUE); src_buffer = (*srcinfo->mem->access_virt_barray) - ((j_common_ptr)srcinfo, src_coef_arrays[ci], - dst_blk_y + y_crop_blocks, + ((j_common_ptr)srcinfo, src_coef_arrays[ci], dst_blk_y + y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, FALSE); for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { jcopy_block_row(src_buffer[offset_y] + x_crop_blocks, - dst_buffer[offset_y], - compptr->width_in_blocks); + dst_buffer[offset_y], compptr->width_in_blocks); + } + } + } +} + + +LOCAL(void) +do_crop_ext_zero(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, + JDIMENSION x_crop_offset, JDIMENSION y_crop_offset, + jvirt_barray_ptr *src_coef_arrays, + jvirt_barray_ptr *dst_coef_arrays) +/* Crop. This is only used when no rotate/flip is requested with the crop. + * Extension: If the destination size is larger than the source, we fill in the + * expanded region with zero (neutral gray). Note that we also have to zero + * partial iMCUs at the right and bottom edge of the source image area in this + * case. + */ +{ + JDIMENSION MCU_cols, MCU_rows, comp_width, comp_height; + JDIMENSION dst_blk_y, x_crop_blocks, y_crop_blocks; + int ci, offset_y; + JBLOCKARRAY src_buffer, dst_buffer; + jpeg_component_info *compptr; + + MCU_cols = srcinfo->output_width / + (dstinfo->max_h_samp_factor * dstinfo_min_DCT_h_scaled_size); + MCU_rows = srcinfo->output_height / + (dstinfo->max_v_samp_factor * dstinfo_min_DCT_v_scaled_size); + + for (ci = 0; ci < dstinfo->num_components; ci++) { + compptr = dstinfo->comp_info + ci; + comp_width = MCU_cols * compptr->h_samp_factor; + comp_height = MCU_rows * compptr->v_samp_factor; + x_crop_blocks = x_crop_offset * compptr->h_samp_factor; + y_crop_blocks = y_crop_offset * compptr->v_samp_factor; + for (dst_blk_y = 0; dst_blk_y < compptr->height_in_blocks; + dst_blk_y += compptr->v_samp_factor) { + dst_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, dst_coef_arrays[ci], dst_blk_y, + (JDIMENSION)compptr->v_samp_factor, TRUE); + if (dstinfo->_jpeg_height > srcinfo->output_height) { + if (dst_blk_y < y_crop_blocks || + dst_blk_y >= y_crop_blocks + comp_height) { + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + memset(dst_buffer[offset_y], 0, + compptr->width_in_blocks * sizeof(JBLOCK)); + } + continue; + } + src_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], + dst_blk_y - y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, + FALSE); + } else { + src_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], + dst_blk_y + y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, + FALSE); + } + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + if (dstinfo->_jpeg_width > srcinfo->output_width) { + if (x_crop_blocks > 0) { + memset(dst_buffer[offset_y], 0, x_crop_blocks * sizeof(JBLOCK)); + } + jcopy_block_row(src_buffer[offset_y], + dst_buffer[offset_y] + x_crop_blocks, comp_width); + if (compptr->width_in_blocks > x_crop_blocks + comp_width) { + memset(dst_buffer[offset_y] + x_crop_blocks + comp_width, 0, + (compptr->width_in_blocks - x_crop_blocks - comp_width) * + sizeof(JBLOCK)); + } + } else { + jcopy_block_row(src_buffer[offset_y] + x_crop_blocks, + dst_buffer[offset_y], compptr->width_in_blocks); + } + } + } + } +} + + +LOCAL(void) +do_crop_ext_flat(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, + JDIMENSION x_crop_offset, JDIMENSION y_crop_offset, + jvirt_barray_ptr *src_coef_arrays, + jvirt_barray_ptr *dst_coef_arrays) +/* Crop. This is only used when no rotate/flip is requested with the crop. + * Extension: The destination width is larger than the source, and we fill in + * the expanded region with the DC coefficient of the adjacent block. Note + * that we also have to fill partial iMCUs at the right and bottom edge of the + * source image area in this case. + */ +{ + JDIMENSION MCU_cols, MCU_rows, comp_width, comp_height; + JDIMENSION dst_blk_x, dst_blk_y, x_crop_blocks, y_crop_blocks; + int ci, offset_y; + JCOEF dc; + JBLOCKARRAY src_buffer, dst_buffer; + jpeg_component_info *compptr; + + MCU_cols = srcinfo->output_width / + (dstinfo->max_h_samp_factor * dstinfo_min_DCT_h_scaled_size); + MCU_rows = srcinfo->output_height / + (dstinfo->max_v_samp_factor * dstinfo_min_DCT_v_scaled_size); + + for (ci = 0; ci < dstinfo->num_components; ci++) { + compptr = dstinfo->comp_info + ci; + comp_width = MCU_cols * compptr->h_samp_factor; + comp_height = MCU_rows * compptr->v_samp_factor; + x_crop_blocks = x_crop_offset * compptr->h_samp_factor; + y_crop_blocks = y_crop_offset * compptr->v_samp_factor; + for (dst_blk_y = 0; dst_blk_y < compptr->height_in_blocks; + dst_blk_y += compptr->v_samp_factor) { + dst_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, dst_coef_arrays[ci], dst_blk_y, + (JDIMENSION)compptr->v_samp_factor, TRUE); + if (dstinfo->_jpeg_height > srcinfo->output_height) { + if (dst_blk_y < y_crop_blocks || + dst_blk_y >= y_crop_blocks + comp_height) { + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + memset(dst_buffer[offset_y], 0, + compptr->width_in_blocks * sizeof(JBLOCK)); + } + continue; + } + src_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], + dst_blk_y - y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, + FALSE); + } else { + src_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], + dst_blk_y + y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, + FALSE); + } + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + if (x_crop_blocks > 0) { + memset(dst_buffer[offset_y], 0, x_crop_blocks * sizeof(JBLOCK)); + dc = src_buffer[offset_y][0][0]; + for (dst_blk_x = 0; dst_blk_x < x_crop_blocks; dst_blk_x++) { + dst_buffer[offset_y][dst_blk_x][0] = dc; + } + } + jcopy_block_row(src_buffer[offset_y], + dst_buffer[offset_y] + x_crop_blocks, comp_width); + if (compptr->width_in_blocks > x_crop_blocks + comp_width) { + memset(dst_buffer[offset_y] + x_crop_blocks + comp_width, 0, + (compptr->width_in_blocks - x_crop_blocks - comp_width) * + sizeof(JBLOCK)); + dc = src_buffer[offset_y][comp_width - 1][0]; + for (dst_blk_x = x_crop_blocks + comp_width; + dst_blk_x < compptr->width_in_blocks; dst_blk_x++) { + dst_buffer[offset_y][dst_blk_x][0] = dc; + } + } + } + } + } +} + + +LOCAL(void) +do_crop_ext_reflect(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, + JDIMENSION x_crop_offset, JDIMENSION y_crop_offset, + jvirt_barray_ptr *src_coef_arrays, + jvirt_barray_ptr *dst_coef_arrays) +/* Crop. This is only used when no rotate/flip is requested with the crop. + * Extension: The destination width is larger than the source, and we fill in + * the expanded region with repeated reflections of the source image. Note + * that we also have to fill partial iMCUs at the right and bottom edge of the + * source image area in this case. + */ +{ + JDIMENSION MCU_cols, MCU_rows, comp_width, comp_height, src_blk_x; + JDIMENSION dst_blk_x, dst_blk_y, x_crop_blocks, y_crop_blocks; + int ci, k, offset_y; + JBLOCKARRAY src_buffer, dst_buffer; + JBLOCKROW src_row_ptr, dst_row_ptr; + JCOEFPTR src_ptr, dst_ptr; + jpeg_component_info *compptr; + + MCU_cols = srcinfo->output_width / + (dstinfo->max_h_samp_factor * dstinfo_min_DCT_h_scaled_size); + MCU_rows = srcinfo->output_height / + (dstinfo->max_v_samp_factor * dstinfo_min_DCT_v_scaled_size); + + for (ci = 0; ci < dstinfo->num_components; ci++) { + compptr = dstinfo->comp_info + ci; + comp_width = MCU_cols * compptr->h_samp_factor; + comp_height = MCU_rows * compptr->v_samp_factor; + x_crop_blocks = x_crop_offset * compptr->h_samp_factor; + y_crop_blocks = y_crop_offset * compptr->v_samp_factor; + for (dst_blk_y = 0; dst_blk_y < compptr->height_in_blocks; + dst_blk_y += compptr->v_samp_factor) { + dst_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, dst_coef_arrays[ci], dst_blk_y, + (JDIMENSION)compptr->v_samp_factor, TRUE); + if (dstinfo->_jpeg_height > srcinfo->output_height) { + if (dst_blk_y < y_crop_blocks || + dst_blk_y >= y_crop_blocks + comp_height) { + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + memset(dst_buffer[offset_y], 0, + compptr->width_in_blocks * sizeof(JBLOCK)); + } + continue; + } + src_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], + dst_blk_y - y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, + FALSE); + } else { + src_buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], + dst_blk_y + y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, + FALSE); + } + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + /* Copy source region */ + jcopy_block_row(src_buffer[offset_y], + dst_buffer[offset_y] + x_crop_blocks, comp_width); + if (x_crop_blocks > 0) { + /* Reflect to left */ + dst_row_ptr = dst_buffer[offset_y] + x_crop_blocks; + for (dst_blk_x = x_crop_blocks; dst_blk_x > 0;) { + src_row_ptr = dst_row_ptr; /* (re)set axis of reflection */ + for (src_blk_x = comp_width; src_blk_x > 0 && dst_blk_x > 0; + src_blk_x--, dst_blk_x--) { + dst_ptr = *(--dst_row_ptr); /* destination goes left */ + src_ptr = *src_row_ptr++; /* source goes right */ + /* This unrolled loop doesn't need to know which row it's on. */ + for (k = 0; k < DCTSIZE2; k += 2) { + *dst_ptr++ = *src_ptr++; /* copy even column */ + *dst_ptr++ = -(*src_ptr++); /* copy odd column with sign + change */ + } + } + } + } + if (compptr->width_in_blocks > x_crop_blocks + comp_width) { + /* Reflect to right */ + dst_row_ptr = dst_buffer[offset_y] + x_crop_blocks + comp_width; + for (dst_blk_x = compptr->width_in_blocks - x_crop_blocks - comp_width; + dst_blk_x > 0;) { + src_row_ptr = dst_row_ptr; /* (re)set axis of reflection */ + for (src_blk_x = comp_width; src_blk_x > 0 && dst_blk_x > 0; + src_blk_x--, dst_blk_x--) { + dst_ptr = *dst_row_ptr++; /* destination goes right */ + src_ptr = *(--src_row_ptr); /* source goes left */ + /* This unrolled loop doesn't need to know which row it's on. */ + for (k = 0; k < DCTSIZE2; k += 2) { + *dst_ptr++ = *src_ptr++; /* copy even column */ + *dst_ptr++ = -(*src_ptr++); /* copy odd column with sign + change */ + } + } + } + } + } + } + } +} + + +LOCAL(void) +do_wipe(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, + JDIMENSION x_crop_offset, JDIMENSION y_crop_offset, + jvirt_barray_ptr *src_coef_arrays, + JDIMENSION drop_width, JDIMENSION drop_height) +/* Wipe - discard image contents of specified region and fill with zero + * (neutral gray) + */ +{ + JDIMENSION x_wipe_blocks, wipe_width; + JDIMENSION y_wipe_blocks, wipe_bottom; + int ci, offset_y; + JBLOCKARRAY buffer; + jpeg_component_info *compptr; + + for (ci = 0; ci < dstinfo->num_components; ci++) { + compptr = dstinfo->comp_info + ci; + x_wipe_blocks = x_crop_offset * compptr->h_samp_factor; + wipe_width = drop_width * compptr->h_samp_factor; + y_wipe_blocks = y_crop_offset * compptr->v_samp_factor; + wipe_bottom = drop_height * compptr->v_samp_factor + y_wipe_blocks; + for (; y_wipe_blocks < wipe_bottom; + y_wipe_blocks += compptr->v_samp_factor) { + buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], y_wipe_blocks, + (JDIMENSION)compptr->v_samp_factor, TRUE); + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + memset(buffer[offset_y] + x_wipe_blocks, 0, + wipe_width * sizeof(JBLOCK)); + } + } + } +} + + +LOCAL(void) +do_flatten(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, + JDIMENSION x_crop_offset, JDIMENSION y_crop_offset, + jvirt_barray_ptr *src_coef_arrays, + JDIMENSION drop_width, JDIMENSION drop_height) +/* Flatten - discard image contents of specified region, similarly to wipe, + * but fill with the average of adjacent blocks instead of zero. + */ +{ + JDIMENSION x_wipe_blocks, wipe_width, wipe_right; + JDIMENSION y_wipe_blocks, wipe_bottom, blk_x; + int ci, offset_y, dc_left_value, dc_right_value, average; + JBLOCKARRAY buffer; + jpeg_component_info *compptr; + + for (ci = 0; ci < dstinfo->num_components; ci++) { + compptr = dstinfo->comp_info + ci; + x_wipe_blocks = x_crop_offset * compptr->h_samp_factor; + wipe_width = drop_width * compptr->h_samp_factor; + wipe_right = wipe_width + x_wipe_blocks; + y_wipe_blocks = y_crop_offset * compptr->v_samp_factor; + wipe_bottom = drop_height * compptr->v_samp_factor + y_wipe_blocks; + for (; y_wipe_blocks < wipe_bottom; + y_wipe_blocks += compptr->v_samp_factor) { + buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], y_wipe_blocks, + (JDIMENSION)compptr->v_samp_factor, TRUE); + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + memset(buffer[offset_y] + x_wipe_blocks, 0, + wipe_width * sizeof(JBLOCK)); + if (x_wipe_blocks > 0) { + dc_left_value = buffer[offset_y][x_wipe_blocks - 1][0]; + if (wipe_right < compptr->width_in_blocks) { + dc_right_value = buffer[offset_y][wipe_right][0]; + average = (dc_left_value + dc_right_value) >> 1; + } else { + average = dc_left_value; + } + } else if (wipe_right < compptr->width_in_blocks) { + average = buffer[offset_y][wipe_right][0]; + } else continue; + for (blk_x = x_wipe_blocks; blk_x < wipe_right; blk_x++) { + buffer[offset_y][blk_x][0] = (JCOEF)average; + } + } + } + } +} + + +LOCAL(void) +do_reflect(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, + JDIMENSION x_crop_offset, jvirt_barray_ptr *src_coef_arrays, + JDIMENSION drop_width, JDIMENSION drop_height) +/* Reflect - discard image contents of specified region, similarly to wipe, + * but fill with repeated reflections of the outside region instead of zero. + * NB: y_crop_offset is assumed to be zero. + */ +{ + JDIMENSION x_wipe_blocks, wipe_width; + JDIMENSION y_wipe_blocks, wipe_bottom; + JDIMENSION src_blk_x, dst_blk_x; + int ci, k, offset_y; + JBLOCKARRAY buffer; + JBLOCKROW src_row_ptr, dst_row_ptr; + JCOEFPTR src_ptr, dst_ptr; + jpeg_component_info *compptr; + + for (ci = 0; ci < dstinfo->num_components; ci++) { + compptr = dstinfo->comp_info + ci; + x_wipe_blocks = x_crop_offset * compptr->h_samp_factor; + wipe_width = drop_width * compptr->h_samp_factor; + wipe_bottom = drop_height * compptr->v_samp_factor; + for (y_wipe_blocks = 0; y_wipe_blocks < wipe_bottom; + y_wipe_blocks += compptr->v_samp_factor) { + buffer = (*srcinfo->mem->access_virt_barray) + ((j_common_ptr)srcinfo, src_coef_arrays[ci], y_wipe_blocks, + (JDIMENSION)compptr->v_samp_factor, TRUE); + for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { + if (x_wipe_blocks > 0) { + /* Reflect from left */ + dst_row_ptr = buffer[offset_y] + x_wipe_blocks; + for (dst_blk_x = wipe_width; dst_blk_x > 0;) { + src_row_ptr = dst_row_ptr; /* (re)set axis of reflection */ + for (src_blk_x = x_wipe_blocks; + src_blk_x > 0 && dst_blk_x > 0; src_blk_x--, dst_blk_x--) { + dst_ptr = *dst_row_ptr++; /* destination goes right */ + src_ptr = *(--src_row_ptr); /* source goes left */ + /* this unrolled loop doesn't need to know which row it's on... */ + for (k = 0; k < DCTSIZE2; k += 2) { + *dst_ptr++ = *src_ptr++; /* copy even column */ + *dst_ptr++ = -(*src_ptr++); /* copy odd column with sign change */ + } + } + } + } else if (compptr->width_in_blocks > x_wipe_blocks + wipe_width) { + /* Reflect from right */ + dst_row_ptr = buffer[offset_y] + x_wipe_blocks + wipe_width; + for (dst_blk_x = wipe_width; dst_blk_x > 0;) { + src_row_ptr = dst_row_ptr; /* (re)set axis of reflection */ + src_blk_x = compptr->width_in_blocks - x_wipe_blocks - wipe_width; + for (; src_blk_x > 0 && dst_blk_x > 0; src_blk_x--, dst_blk_x--) { + dst_ptr = *(--dst_row_ptr); /* destination goes left */ + src_ptr = *src_row_ptr++; /* source goes right */ + /* this unrolled loop doesn't need to know which row it's on... */ + for (k = 0; k < DCTSIZE2; k += 2) { + *dst_ptr++ = *src_ptr++; /* copy even column */ + *dst_ptr++ = -(*src_ptr++); /* copy odd column with sign change */ + } + } + } + } else { + memset(buffer[offset_y] + x_wipe_blocks, 0, + wipe_width * sizeof(JBLOCK)); + } } } } @@ -224,8 +818,7 @@ do_flip_h(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, ((j_common_ptr)srcinfo, dst_coef_arrays[ci], dst_blk_y, (JDIMENSION)compptr->v_samp_factor, TRUE); src_buffer = (*srcinfo->mem->access_virt_barray) - ((j_common_ptr)srcinfo, src_coef_arrays[ci], - dst_blk_y + y_crop_blocks, + ((j_common_ptr)srcinfo, src_coef_arrays[ci], dst_blk_y + y_crop_blocks, (JDIMENSION)compptr->v_samp_factor, FALSE); for (offset_y = 0; offset_y < compptr->v_samp_factor; offset_y++) { dst_row_ptr = dst_buffer[offset_y]; @@ -238,8 +831,9 @@ do_flip_h(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, src_ptr = src_row_ptr[comp_width - x_crop_blocks - dst_blk_x - 1]; /* this unrolled loop doesn't need to know which row it's on... */ for (k = 0; k < DCTSIZE2; k += 2) { - *dst_ptr++ = *src_ptr++; /* copy even column */ - *dst_ptr++ = - *src_ptr++; /* copy odd column with sign change */ + *dst_ptr++ = *src_ptr++; /* copy even column */ + *dst_ptr++ = -(*src_ptr++); /* copy odd column with sign + change */ } } else { /* Copy last partial block(s) verbatim */ @@ -318,14 +912,13 @@ do_flip_v(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, *dst_ptr++ = *src_ptr++; /* copy odd row with sign change */ for (j = 0; j < DCTSIZE; j++) - *dst_ptr++ = - *src_ptr++; + *dst_ptr++ = -(*src_ptr++); } } } else { /* Just copy row verbatim. */ jcopy_block_row(src_buffer[offset_y] + x_crop_blocks, - dst_buffer[offset_y], - compptr->width_in_blocks); + dst_buffer[offset_y], compptr->width_in_blocks); } } } @@ -599,11 +1192,11 @@ do_rot_180(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, /* For even row, negate every odd column. */ for (j = 0; j < DCTSIZE; j += 2) { *dst_ptr++ = *src_ptr++; - *dst_ptr++ = - *src_ptr++; + *dst_ptr++ = -(*src_ptr++); } /* For odd row, negate every even column. */ for (j = 0; j < DCTSIZE; j += 2) { - *dst_ptr++ = - *src_ptr++; + *dst_ptr++ = -(*src_ptr++); *dst_ptr++ = *src_ptr++; } } @@ -614,7 +1207,7 @@ do_rot_180(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, for (j = 0; j < DCTSIZE; j++) *dst_ptr++ = *src_ptr++; for (j = 0; j < DCTSIZE; j++) - *dst_ptr++ = - *src_ptr++; + *dst_ptr++ = -(*src_ptr++); } } } @@ -630,7 +1223,7 @@ do_rot_180(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, src_row_ptr[comp_width - x_crop_blocks - dst_blk_x - 1]; for (i = 0; i < DCTSIZE2; i += 2) { *dst_ptr++ = *src_ptr++; - *dst_ptr++ = - *src_ptr++; + *dst_ptr++ = -(*src_ptr++); } } else { /* Any remaining right-edge blocks are only copied. */ @@ -786,7 +1379,7 @@ jt_read_integer(const char **strptr, JDIMENSION *result) * The routine returns TRUE if the spec string is valid, FALSE if not. * * The crop spec string should have the format - * [f]x[f]{+-}{+-} + * [{fr}]x[{fr}]{+-}{+-} * where width, height, xoffset, and yoffset are unsigned integers. * Each of the elements can be omitted to indicate a default value. * (A weakness of this style is that it is not possible to omit xoffset @@ -811,6 +1404,9 @@ jtransform_parse_crop_spec(jpeg_transform_info *info, const char *spec) if (*spec == 'f' || *spec == 'F') { spec++; info->crop_width_set = JCROP_FORCE; + } else if (*spec == 'r' || *spec == 'R') { + spec++; + info->crop_width_set = JCROP_REFLECT; } else info->crop_width_set = JCROP_POS; } @@ -822,6 +1418,9 @@ jtransform_parse_crop_spec(jpeg_transform_info *info, const char *spec) if (*spec == 'f' || *spec == 'F') { spec++; info->crop_height_set = JCROP_FORCE; + } else if (*spec == 'r' || *spec == 'R') { + spec++; + info->crop_height_set = JCROP_REFLECT; } else info->crop_height_set = JCROP_POS; } @@ -896,10 +1495,10 @@ jtransform_request_workspace(j_decompress_ptr srcinfo, jvirt_barray_ptr *coef_arrays; boolean need_workspace, transpose_it; jpeg_component_info *compptr; - JDIMENSION xoffset, yoffset; + JDIMENSION xoffset, yoffset, dtemp; JDIMENSION width_in_iMCUs, height_in_iMCUs; JDIMENSION width_in_blocks, height_in_blocks; - int ci, h_samp_factor, v_samp_factor; + int itemp, ci, h_samp_factor, v_samp_factor; /* Determine number of components in output image */ if (info->force_grayscale && @@ -985,39 +1584,129 @@ jtransform_request_workspace(j_decompress_ptr srcinfo, info->crop_xoffset = 0; /* default to +0 */ if (info->crop_yoffset_set == JCROP_UNSET) info->crop_yoffset = 0; /* default to +0 */ - if (info->crop_xoffset >= info->output_width || - info->crop_yoffset >= info->output_height) - ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); - if (info->crop_width_set == JCROP_UNSET) + if (info->crop_width_set == JCROP_UNSET) { + if (info->crop_xoffset >= info->output_width) + ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); info->crop_width = info->output_width - info->crop_xoffset; - if (info->crop_height_set == JCROP_UNSET) + } else { + /* Check for crop extension */ + if (info->crop_width > info->output_width) { + /* Crop extension does not work when transforming! */ + if (info->transform != JXFORM_NONE || + info->crop_xoffset >= info->crop_width || + info->crop_xoffset > info->crop_width - info->output_width) + ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); + } else { + if (info->crop_xoffset >= info->output_width || + info->crop_width <= 0 || + info->crop_xoffset > info->output_width - info->crop_width) + ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); + } + } + if (info->crop_height_set == JCROP_UNSET) { + if (info->crop_yoffset >= info->output_height) + ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); info->crop_height = info->output_height - info->crop_yoffset; - /* Ensure parameters are valid */ - if (info->crop_width <= 0 || info->crop_width > info->output_width || - info->crop_height <= 0 || info->crop_height > info->output_height || - info->crop_xoffset > info->output_width - info->crop_width || - info->crop_yoffset > info->output_height - info->crop_height) - ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); + } else { + /* Check for crop extension */ + if (info->crop_height > info->output_height) { + /* Crop extension does not work when transforming! */ + if (info->transform != JXFORM_NONE || + info->crop_yoffset >= info->crop_height || + info->crop_yoffset > info->crop_height - info->output_height) + ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); + } else { + if (info->crop_yoffset >= info->output_height || + info->crop_height <= 0 || + info->crop_yoffset > info->output_height - info->crop_height) + ERREXIT(srcinfo, JERR_BAD_CROP_SPEC); + } + } /* Convert negative crop offsets into regular offsets */ - if (info->crop_xoffset_set == JCROP_NEG) - xoffset = info->output_width - info->crop_width - info->crop_xoffset; - else + if (info->crop_xoffset_set != JCROP_NEG) xoffset = info->crop_xoffset; - if (info->crop_yoffset_set == JCROP_NEG) - yoffset = info->output_height - info->crop_height - info->crop_yoffset; + else if (info->crop_width > info->output_width) /* crop extension */ + xoffset = info->crop_width - info->output_width - info->crop_xoffset; else + xoffset = info->output_width - info->crop_width - info->crop_xoffset; + if (info->crop_yoffset_set != JCROP_NEG) yoffset = info->crop_yoffset; - /* Now adjust so that upper left corner falls at an iMCU boundary */ - if (info->crop_width_set == JCROP_FORCE) - info->output_width = info->crop_width; - else - info->output_width = - info->crop_width + (xoffset % info->iMCU_sample_width); - if (info->crop_height_set == JCROP_FORCE) - info->output_height = info->crop_height; + else if (info->crop_height > info->output_height) /* crop extension */ + yoffset = info->crop_height - info->output_height - info->crop_yoffset; else - info->output_height = - info->crop_height + (yoffset % info->iMCU_sample_height); + yoffset = info->output_height - info->crop_height - info->crop_yoffset; + /* Now adjust so that upper left corner falls at an iMCU boundary */ + switch (info->transform) { + case JXFORM_DROP: + /* Ensure the effective drop region will not exceed the requested */ + itemp = info->iMCU_sample_width; + dtemp = itemp - 1 - ((xoffset + itemp - 1) % itemp); + xoffset += dtemp; + if (info->crop_width <= dtemp) + info->drop_width = 0; + else if (xoffset + info->crop_width - dtemp == info->output_width) + /* Matching right edge: include partial iMCU */ + info->drop_width = (info->crop_width - dtemp + itemp - 1) / itemp; + else + info->drop_width = (info->crop_width - dtemp) / itemp; + itemp = info->iMCU_sample_height; + dtemp = itemp - 1 - ((yoffset + itemp - 1) % itemp); + yoffset += dtemp; + if (info->crop_height <= dtemp) + info->drop_height = 0; + else if (yoffset + info->crop_height - dtemp == info->output_height) + /* Matching bottom edge: include partial iMCU */ + info->drop_height = (info->crop_height - dtemp + itemp - 1) / itemp; + else + info->drop_height = (info->crop_height - dtemp) / itemp; + /* Check if sampling factors match for dropping */ + if (info->drop_width != 0 && info->drop_height != 0) + for (ci = 0; ci < info->num_components && + ci < info->drop_ptr->num_components; ci++) { + if (info->drop_ptr->comp_info[ci].h_samp_factor * + srcinfo->max_h_samp_factor != + srcinfo->comp_info[ci].h_samp_factor * + info->drop_ptr->max_h_samp_factor) + ERREXIT6(srcinfo, JERR_BAD_DROP_SAMPLING, ci, + info->drop_ptr->comp_info[ci].h_samp_factor, + info->drop_ptr->max_h_samp_factor, + srcinfo->comp_info[ci].h_samp_factor, + srcinfo->max_h_samp_factor, 'h'); + if (info->drop_ptr->comp_info[ci].v_samp_factor * + srcinfo->max_v_samp_factor != + srcinfo->comp_info[ci].v_samp_factor * + info->drop_ptr->max_v_samp_factor) + ERREXIT6(srcinfo, JERR_BAD_DROP_SAMPLING, ci, + info->drop_ptr->comp_info[ci].v_samp_factor, + info->drop_ptr->max_v_samp_factor, + srcinfo->comp_info[ci].v_samp_factor, + srcinfo->max_v_samp_factor, 'v'); + } + break; + case JXFORM_WIPE: + /* Ensure the effective wipe region will cover the requested */ + info->drop_width = (JDIMENSION)jdiv_round_up + ((long)(info->crop_width + (xoffset % info->iMCU_sample_width)), + (long)info->iMCU_sample_width); + info->drop_height = (JDIMENSION)jdiv_round_up + ((long)(info->crop_height + (yoffset % info->iMCU_sample_height)), + (long)info->iMCU_sample_height); + break; + default: + /* Ensure the effective crop region will cover the requested */ + if (info->crop_width_set == JCROP_FORCE || + info->crop_width > info->output_width) + info->output_width = info->crop_width; + else + info->output_width = + info->crop_width + (xoffset % info->iMCU_sample_width); + if (info->crop_height_set == JCROP_FORCE || + info->crop_height > info->output_height) + info->output_height = info->crop_height; + else + info->output_height = + info->crop_height + (yoffset % info->iMCU_sample_height); + } /* Save x/y offsets measured in iMCUs */ info->x_crop_offset = xoffset / info->iMCU_sample_width; info->y_crop_offset = yoffset / info->iMCU_sample_height; @@ -1033,7 +1722,9 @@ jtransform_request_workspace(j_decompress_ptr srcinfo, transpose_it = FALSE; switch (info->transform) { case JXFORM_NONE: - if (info->x_crop_offset != 0 || info->y_crop_offset != 0) + if (info->x_crop_offset != 0 || info->y_crop_offset != 0 || + info->output_width > srcinfo->output_width || + info->output_height > srcinfo->output_height) need_workspace = TRUE; /* No workspace needed if neither cropping nor transforming */ break; @@ -1087,6 +1778,10 @@ jtransform_request_workspace(j_decompress_ptr srcinfo, need_workspace = TRUE; transpose_it = TRUE; break; + case JXFORM_WIPE: + break; + case JXFORM_DROP: + break; } /* Allocate workspace if needed. @@ -1190,47 +1885,47 @@ adjust_exif_parameters(JOCTET *data, unsigned int length, JDIMENSION new_width, if (length < 12) return; /* Length of an IFD entry */ /* Discover byte order */ - if (GETJOCTET(data[0]) == 0x49 && GETJOCTET(data[1]) == 0x49) + if (data[0] == 0x49 && data[1] == 0x49) is_motorola = FALSE; - else if (GETJOCTET(data[0]) == 0x4D && GETJOCTET(data[1]) == 0x4D) + else if (data[0] == 0x4D && data[1] == 0x4D) is_motorola = TRUE; else return; /* Check Tag Mark */ if (is_motorola) { - if (GETJOCTET(data[2]) != 0) return; - if (GETJOCTET(data[3]) != 0x2A) return; + if (data[2] != 0) return; + if (data[3] != 0x2A) return; } else { - if (GETJOCTET(data[3]) != 0) return; - if (GETJOCTET(data[2]) != 0x2A) return; + if (data[3] != 0) return; + if (data[2] != 0x2A) return; } /* Get first IFD offset (offset to IFD0) */ if (is_motorola) { - if (GETJOCTET(data[4]) != 0) return; - if (GETJOCTET(data[5]) != 0) return; - firstoffset = GETJOCTET(data[6]); + if (data[4] != 0) return; + if (data[5] != 0) return; + firstoffset = data[6]; firstoffset <<= 8; - firstoffset += GETJOCTET(data[7]); + firstoffset += data[7]; } else { - if (GETJOCTET(data[7]) != 0) return; - if (GETJOCTET(data[6]) != 0) return; - firstoffset = GETJOCTET(data[5]); + if (data[7] != 0) return; + if (data[6] != 0) return; + firstoffset = data[5]; firstoffset <<= 8; - firstoffset += GETJOCTET(data[4]); + firstoffset += data[4]; } if (firstoffset > length - 2) return; /* check end of data segment */ /* Get the number of directory entries contained in this IFD */ if (is_motorola) { - number_of_tags = GETJOCTET(data[firstoffset]); + number_of_tags = data[firstoffset]; number_of_tags <<= 8; - number_of_tags += GETJOCTET(data[firstoffset + 1]); + number_of_tags += data[firstoffset + 1]; } else { - number_of_tags = GETJOCTET(data[firstoffset + 1]); + number_of_tags = data[firstoffset + 1]; number_of_tags <<= 8; - number_of_tags += GETJOCTET(data[firstoffset]); + number_of_tags += data[firstoffset]; } if (number_of_tags == 0) return; firstoffset += 2; @@ -1240,13 +1935,13 @@ adjust_exif_parameters(JOCTET *data, unsigned int length, JDIMENSION new_width, if (firstoffset > length - 12) return; /* check end of data segment */ /* Get Tag number */ if (is_motorola) { - tagnum = GETJOCTET(data[firstoffset]); + tagnum = data[firstoffset]; tagnum <<= 8; - tagnum += GETJOCTET(data[firstoffset + 1]); + tagnum += data[firstoffset + 1]; } else { - tagnum = GETJOCTET(data[firstoffset + 1]); + tagnum = data[firstoffset + 1]; tagnum <<= 8; - tagnum += GETJOCTET(data[firstoffset]); + tagnum += data[firstoffset]; } if (tagnum == 0x8769) break; /* found ExifSubIFD offset Tag */ if (--number_of_tags == 0) return; @@ -1255,29 +1950,29 @@ adjust_exif_parameters(JOCTET *data, unsigned int length, JDIMENSION new_width, /* Get the ExifSubIFD offset */ if (is_motorola) { - if (GETJOCTET(data[firstoffset + 8]) != 0) return; - if (GETJOCTET(data[firstoffset + 9]) != 0) return; - offset = GETJOCTET(data[firstoffset + 10]); + if (data[firstoffset + 8] != 0) return; + if (data[firstoffset + 9] != 0) return; + offset = data[firstoffset + 10]; offset <<= 8; - offset += GETJOCTET(data[firstoffset + 11]); + offset += data[firstoffset + 11]; } else { - if (GETJOCTET(data[firstoffset + 11]) != 0) return; - if (GETJOCTET(data[firstoffset + 10]) != 0) return; - offset = GETJOCTET(data[firstoffset + 9]); + if (data[firstoffset + 11] != 0) return; + if (data[firstoffset + 10] != 0) return; + offset = data[firstoffset + 9]; offset <<= 8; - offset += GETJOCTET(data[firstoffset + 8]); + offset += data[firstoffset + 8]; } if (offset > length - 2) return; /* check end of data segment */ /* Get the number of directory entries contained in this SubIFD */ if (is_motorola) { - number_of_tags = GETJOCTET(data[offset]); + number_of_tags = data[offset]; number_of_tags <<= 8; - number_of_tags += GETJOCTET(data[offset + 1]); + number_of_tags += data[offset + 1]; } else { - number_of_tags = GETJOCTET(data[offset + 1]); + number_of_tags = data[offset + 1]; number_of_tags <<= 8; - number_of_tags += GETJOCTET(data[offset]); + number_of_tags += data[offset]; } if (number_of_tags < 2) return; offset += 2; @@ -1287,13 +1982,13 @@ adjust_exif_parameters(JOCTET *data, unsigned int length, JDIMENSION new_width, if (offset > length - 12) return; /* check end of data segment */ /* Get Tag number */ if (is_motorola) { - tagnum = GETJOCTET(data[offset]); + tagnum = data[offset]; tagnum <<= 8; - tagnum += GETJOCTET(data[offset + 1]); + tagnum += data[offset + 1]; } else { - tagnum = GETJOCTET(data[offset + 1]); + tagnum = data[offset + 1]; tagnum <<= 8; - tagnum += GETJOCTET(data[offset]); + tagnum += data[offset]; } if (tagnum == 0xA002 || tagnum == 0xA003) { if (tagnum == 0xA002) @@ -1387,7 +2082,7 @@ jtransform_adjust_parameters(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, dstinfo->jpeg_height = info->output_height; #endif - /* Transpose destination image parameters */ + /* Transpose destination image parameters, adjust quantization */ switch (info->transform) { case JXFORM_TRANSPOSE: case JXFORM_TRANSVERSE: @@ -1399,6 +2094,12 @@ jtransform_adjust_parameters(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, #endif transpose_critical_parameters(dstinfo); break; + case JXFORM_DROP: + if (info->drop_width != 0 && info->drop_height != 0) + adjust_quant(srcinfo, src_coef_arrays, + info->drop_ptr, info->drop_coef_arrays, + info->trim, dstinfo); + break; default: #if JPEG_LIB_VERSION < 80 dstinfo->image_width = info->output_width; @@ -1411,12 +2112,12 @@ jtransform_adjust_parameters(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, if (srcinfo->marker_list != NULL && srcinfo->marker_list->marker == JPEG_APP0 + 1 && srcinfo->marker_list->data_length >= 6 && - GETJOCTET(srcinfo->marker_list->data[0]) == 0x45 && - GETJOCTET(srcinfo->marker_list->data[1]) == 0x78 && - GETJOCTET(srcinfo->marker_list->data[2]) == 0x69 && - GETJOCTET(srcinfo->marker_list->data[3]) == 0x66 && - GETJOCTET(srcinfo->marker_list->data[4]) == 0 && - GETJOCTET(srcinfo->marker_list->data[5]) == 0) { + srcinfo->marker_list->data[0] == 0x45 && + srcinfo->marker_list->data[1] == 0x78 && + srcinfo->marker_list->data[2] == 0x69 && + srcinfo->marker_list->data[3] == 0x66 && + srcinfo->marker_list->data[4] == 0 && + srcinfo->marker_list->data[5] == 0) { /* Suppress output of JFIF marker */ dstinfo->write_JFIF_header = FALSE; /* Adjust Exif image parameters */ @@ -1465,7 +2166,23 @@ jtransform_execute_transform(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, */ switch (info->transform) { case JXFORM_NONE: - if (info->x_crop_offset != 0 || info->y_crop_offset != 0) + if (info->output_width > srcinfo->output_width || + info->output_height > srcinfo->output_height) { + if (info->output_width > srcinfo->output_width && + info->crop_width_set == JCROP_REFLECT) + do_crop_ext_reflect(srcinfo, dstinfo, + info->x_crop_offset, info->y_crop_offset, + src_coef_arrays, dst_coef_arrays); + else if (info->output_width > srcinfo->output_width && + info->crop_width_set == JCROP_FORCE) + do_crop_ext_flat(srcinfo, dstinfo, + info->x_crop_offset, info->y_crop_offset, + src_coef_arrays, dst_coef_arrays); + else + do_crop_ext_zero(srcinfo, dstinfo, + info->x_crop_offset, info->y_crop_offset, + src_coef_arrays, dst_coef_arrays); + } else if (info->x_crop_offset != 0 || info->y_crop_offset != 0) do_crop(srcinfo, dstinfo, info->x_crop_offset, info->y_crop_offset, src_coef_arrays, dst_coef_arrays); break; @@ -1501,6 +2218,30 @@ jtransform_execute_transform(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, do_rot_270(srcinfo, dstinfo, info->x_crop_offset, info->y_crop_offset, src_coef_arrays, dst_coef_arrays); break; + case JXFORM_WIPE: + if (info->crop_width_set == JCROP_REFLECT && + info->y_crop_offset == 0 && info->drop_height == + (JDIMENSION)jdiv_round_up + ((long)info->output_height, (long)info->iMCU_sample_height) && + (info->x_crop_offset == 0 || + info->x_crop_offset + info->drop_width == + (JDIMENSION)jdiv_round_up + ((long)info->output_width, (long)info->iMCU_sample_width))) + do_reflect(srcinfo, dstinfo, info->x_crop_offset, + src_coef_arrays, info->drop_width, info->drop_height); + else if (info->crop_width_set == JCROP_FORCE) + do_flatten(srcinfo, dstinfo, info->x_crop_offset, info->y_crop_offset, + src_coef_arrays, info->drop_width, info->drop_height); + else + do_wipe(srcinfo, dstinfo, info->x_crop_offset, info->y_crop_offset, + src_coef_arrays, info->drop_width, info->drop_height); + break; + case JXFORM_DROP: + if (info->drop_width != 0 && info->drop_height != 0) + do_drop(srcinfo, dstinfo, info->x_crop_offset, info->y_crop_offset, + src_coef_arrays, info->drop_ptr, info->drop_coef_arrays, + info->drop_width, info->drop_height); + break; } } @@ -1571,7 +2312,7 @@ jcopy_markers_setup(j_decompress_ptr srcinfo, JCOPY_OPTION option) int m; /* Save comments except under NONE option */ - if (option != JCOPYOPT_NONE) { + if (option != JCOPYOPT_NONE && option != JCOPYOPT_ICC) { jpeg_save_markers(srcinfo, JPEG_COM, 0xFFFF); } /* Save all types of APPn markers iff ALL option */ @@ -1582,6 +2323,10 @@ jcopy_markers_setup(j_decompress_ptr srcinfo, JCOPY_OPTION option) jpeg_save_markers(srcinfo, JPEG_APP0 + m, 0xFFFF); } } + /* Save only APP2 markers if ICC option selected */ + if (option == JCOPYOPT_ICC) { + jpeg_save_markers(srcinfo, JPEG_APP0 + 2, 0xFFFF); + } #endif /* SAVE_MARKERS_SUPPORTED */ } @@ -1607,20 +2352,20 @@ jcopy_markers_execute(j_decompress_ptr srcinfo, j_compress_ptr dstinfo, if (dstinfo->write_JFIF_header && marker->marker == JPEG_APP0 && marker->data_length >= 5 && - GETJOCTET(marker->data[0]) == 0x4A && - GETJOCTET(marker->data[1]) == 0x46 && - GETJOCTET(marker->data[2]) == 0x49 && - GETJOCTET(marker->data[3]) == 0x46 && - GETJOCTET(marker->data[4]) == 0) + marker->data[0] == 0x4A && + marker->data[1] == 0x46 && + marker->data[2] == 0x49 && + marker->data[3] == 0x46 && + marker->data[4] == 0) continue; /* reject duplicate JFIF */ if (dstinfo->write_Adobe_marker && marker->marker == JPEG_APP0 + 14 && marker->data_length >= 5 && - GETJOCTET(marker->data[0]) == 0x41 && - GETJOCTET(marker->data[1]) == 0x64 && - GETJOCTET(marker->data[2]) == 0x6F && - GETJOCTET(marker->data[3]) == 0x62 && - GETJOCTET(marker->data[4]) == 0x65) + marker->data[0] == 0x41 && + marker->data[1] == 0x64 && + marker->data[2] == 0x6F && + marker->data[3] == 0x62 && + marker->data[4] == 0x65) continue; /* reject duplicate Adobe */ jpeg_write_marker(dstinfo, marker->marker, marker->data, marker->data_length); diff --git a/third-party/mozjpeg/mozjpeg/transupp.h b/third-party/mozjpeg/mozjpeg/transupp.h index 0a57d8848bd..f4559de53b6 100644 --- a/third-party/mozjpeg/mozjpeg/transupp.h +++ b/third-party/mozjpeg/mozjpeg/transupp.h @@ -2,9 +2,9 @@ * transupp.h * * This file was part of the Independent JPEG Group's software: - * Copyright (C) 1997-2011, Thomas G. Lane, Guido Vollbeding. + * Copyright (C) 1997-2019, Thomas G. Lane, Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2017, D. R. Commander. + * Copyright (C) 2017, 2021, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -68,6 +68,17 @@ extern "C" { * output image covers at least the requested region, but may cover more.) * The adjustment of the region dimensions may be optionally disabled. * + * A complementary lossless wipe option is provided to discard (gray out) data + * inside a given image region while losslessly preserving what is outside. + * A lossless drop option is also provided, which allows another JPEG image to + * be inserted ("dropped") into the source image data at a given position, + * replacing the existing image data at that position. Both the source image + * and the drop image must have the same subsampling level. It is best if they + * also have the same quantization (quality.) Otherwise, the quantization of + * the output image will be adapted to accommodate the higher of the source + * image quality and the drop image quality. The trim option can be used with + * the drop option to requantize the drop image to match the source image. + * * We also provide a lossless-resize option, which is kind of a lossless-crop * operation in the DCT coefficient block domain - it discards higher-order * coefficients and losslessly preserves lower-order coefficients of a @@ -98,20 +109,23 @@ typedef enum { JXFORM_TRANSVERSE, /* transpose across UR-to-LL axis */ JXFORM_ROT_90, /* 90-degree clockwise rotation */ JXFORM_ROT_180, /* 180-degree rotation */ - JXFORM_ROT_270 /* 270-degree clockwise (or 90 ccw) */ + JXFORM_ROT_270, /* 270-degree clockwise (or 90 ccw) */ + JXFORM_WIPE, /* wipe */ + JXFORM_DROP /* drop */ } JXFORM_CODE; /* * Codes for crop parameters, which can individually be unspecified, * positive or negative for xoffset or yoffset, - * positive or forced for width or height. + * positive or force or reflect for width or height. */ typedef enum { JCROP_UNSET, JCROP_POS, JCROP_NEG, - JCROP_FORCE + JCROP_FORCE, + JCROP_REFLECT } JCROP_CODE; /* @@ -126,7 +140,7 @@ typedef struct { boolean perfect; /* if TRUE, fail if partial MCUs are requested */ boolean trim; /* if TRUE, trim partial MCUs as needed */ boolean force_grayscale; /* if TRUE, convert color image to grayscale */ - boolean crop; /* if TRUE, crop source image */ + boolean crop; /* if TRUE, crop or wipe source image, or drop */ boolean slow_hflip; /* For best performance, the JXFORM_FLIP_H transform normally modifies the source coefficients in place. Setting this to TRUE will instead use a slower, @@ -139,14 +153,18 @@ typedef struct { * These can be filled in by jtransform_parse_crop_spec(). */ JDIMENSION crop_width; /* Width of selected region */ - JCROP_CODE crop_width_set; /* (forced disables adjustment) */ + JCROP_CODE crop_width_set; /* (force-disables adjustment) */ JDIMENSION crop_height; /* Height of selected region */ - JCROP_CODE crop_height_set; /* (forced disables adjustment) */ + JCROP_CODE crop_height_set; /* (force-disables adjustment) */ JDIMENSION crop_xoffset; /* X offset of selected region */ JCROP_CODE crop_xoffset_set; /* (negative measures from right edge) */ JDIMENSION crop_yoffset; /* Y offset of selected region */ JCROP_CODE crop_yoffset_set; /* (negative measures from bottom edge) */ + /* Drop parameters: set by caller for drop request */ + j_decompress_ptr drop_ptr; + jvirt_barray_ptr *drop_coef_arrays; + /* Internal workspace: caller should not touch these */ int num_components; /* # of components in workspace */ jvirt_barray_ptr *workspace_coef_arrays; /* workspace for transformations */ @@ -154,6 +172,8 @@ typedef struct { JDIMENSION output_height; JDIMENSION x_crop_offset; /* destination crop offsets measured in iMCUs */ JDIMENSION y_crop_offset; + JDIMENSION drop_width; /* drop/wipe dimensions measured in iMCUs */ + JDIMENSION drop_height; int iMCU_sample_width; /* destination iMCU size */ int iMCU_sample_height; } jpeg_transform_info; @@ -199,10 +219,11 @@ EXTERN(boolean) jtransform_perfect_transform(JDIMENSION image_width, */ typedef enum { - JCOPYOPT_NONE, /* copy no optional markers */ - JCOPYOPT_COMMENTS, /* copy only comment (COM) markers */ - JCOPYOPT_ALL, /* copy all optional markers */ - JCOPYOPT_ALL_EXCEPT_ICC /* copy all optional markers except APP2 */ + JCOPYOPT_NONE, /* copy no optional markers */ + JCOPYOPT_COMMENTS, /* copy only comment (COM) markers */ + JCOPYOPT_ALL, /* copy all optional markers */ + JCOPYOPT_ALL_EXCEPT_ICC, /* copy all optional markers except APP2 */ + JCOPYOPT_ICC /* copy only ICC profile (APP2) markers */ } JCOPY_OPTION; #define JCOPYOPT_DEFAULT JCOPYOPT_COMMENTS /* recommended default */ diff --git a/third-party/mozjpeg/mozjpeg/turbojpeg-jni.c b/third-party/mozjpeg/mozjpeg/turbojpeg-jni.c index 936345016f6..786af5c8b0a 100644 --- a/third-party/mozjpeg/mozjpeg/turbojpeg-jni.c +++ b/third-party/mozjpeg/mozjpeg/turbojpeg-jni.c @@ -1,5 +1,5 @@ /* - * Copyright (C)2011-2019 D. R. Commander. All Rights Reserved. + * Copyright (C)2011-2023 D. R. Commander. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -26,12 +26,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ -#include -#include +#include #include "turbojpeg.h" -#ifdef WIN32 -#include "tjutil.h" -#endif +#include "jinclude.h" #include #include "java/org_libjpegturbo_turbojpeg_TJCompressor.h" #include "java/org_libjpegturbo_turbojpeg_TJDecompressor.h" @@ -44,6 +41,12 @@ } \ } +#define BAILIF0NOEC(f) { \ + if (!(f)) { \ + goto bailout; \ + } \ +} + #define THROW(msg, exceptionClass) { \ jclass _exccls = (*env)->FindClass(env, exceptionClass); \ \ @@ -82,20 +85,20 @@ BAILIF0(_fid = (*env)->GetFieldID(env, _cls, "handle", "J")); \ handle = (tjhandle)(size_t)(*env)->GetLongField(env, obj, _fid); -#ifdef _WIN32 -#define setenv(envvar, value, dummy) _putenv_s(envvar, value) -#endif - +#ifndef NO_PUTENV #define PROP2ENV(property, envvar) { \ - if ((jName = (*env)->NewStringUTF(env, property)) != NULL && \ - (jValue = (*env)->CallStaticObjectMethod(env, cls, mid, \ - jName)) != NULL) { \ - if ((value = (*env)->GetStringUTFChars(env, jValue, 0)) != NULL) { \ - setenv(envvar, value, 1); \ + if ((jName = (*env)->NewStringUTF(env, property)) != NULL) { \ + jboolean exception; \ + jValue = (*env)->CallStaticObjectMethod(env, cls, mid, jName); \ + exception = (*env)->ExceptionCheck(env); \ + if (jValue && !exception && \ + (value = (*env)->GetStringUTFChars(env, jValue, 0)) != NULL) { \ + PUTENV_S(envvar, value); \ (*env)->ReleaseStringUTFChars(env, jValue, value); \ } \ } \ } +#endif #define SAFE_RELEASE(javaArray, cArray) { \ if (javaArray && cArray) \ @@ -114,10 +117,12 @@ static int ProcessSystemProperties(JNIEnv *env) BAILIF0(mid = (*env)->GetStaticMethodID(env, cls, "getProperty", "(Ljava/lang/String;)Ljava/lang/String;")); +#ifndef NO_PUTENV PROP2ENV("turbojpeg.optimize", "TJ_OPTIMIZE"); PROP2ENV("turbojpeg.arithmetic", "TJ_ARITHMETIC"); PROP2ENV("turbojpeg.restart", "TJ_RESTART"); PROP2ENV("turbojpeg.progressive", "TJ_PROGRESSIVE"); +#endif return 0; bailout: @@ -128,24 +133,28 @@ static int ProcessSystemProperties(JNIEnv *env) JNIEXPORT jint JNICALL Java_org_libjpegturbo_turbojpeg_TJ_bufSize (JNIEnv *env, jclass cls, jint width, jint height, jint jpegSubsamp) { - jint retval = (jint)tjBufSize(width, height, jpegSubsamp); + unsigned long retval = tjBufSize(width, height, jpegSubsamp); - if (retval == -1) THROW_ARG(tjGetErrorStr()); + if (retval == (unsigned long)-1) THROW_ARG(tjGetErrorStr()); + if (retval > (unsigned long)INT_MAX) + THROW_ARG("Image is too large"); bailout: - return retval; + return (jint)retval; } /* TurboJPEG 1.4.x: TJ::bufSizeYUV() */ JNIEXPORT jint JNICALL Java_org_libjpegturbo_turbojpeg_TJ_bufSizeYUV__IIII - (JNIEnv *env, jclass cls, jint width, jint pad, jint height, jint subsamp) + (JNIEnv *env, jclass cls, jint width, jint align, jint height, jint subsamp) { - jint retval = (jint)tjBufSizeYUV2(width, pad, height, subsamp); + unsigned long retval = tjBufSizeYUV2(width, align, height, subsamp); - if (retval == -1) THROW_ARG(tjGetErrorStr()); + if (retval == (unsigned long)-1) THROW_ARG(tjGetErrorStr()); + if (retval > (unsigned long)INT_MAX) + THROW_ARG("Image is too large"); bailout: - return retval; + return (jint)retval; } /* TurboJPEG 1.2.x: TJ::bufSizeYUV() */ @@ -162,13 +171,15 @@ JNIEXPORT jint JNICALL Java_org_libjpegturbo_turbojpeg_TJ_planeSizeYUV__IIIII (JNIEnv *env, jclass cls, jint componentID, jint width, jint stride, jint height, jint subsamp) { - jint retval = (jint)tjPlaneSizeYUV(componentID, width, stride, height, - subsamp); + unsigned long retval = tjPlaneSizeYUV(componentID, width, stride, height, + subsamp); - if (retval == -1) THROW_ARG(tjGetErrorStr()); + if (retval == (unsigned long)-1) THROW_ARG(tjGetErrorStr()); + if (retval > (unsigned long)INT_MAX) + THROW_ARG("Image is too large"); bailout: - return retval; + return (jint)retval; } /* TurboJPEG 1.4.x: TJ::planeWidth() */ @@ -242,8 +253,8 @@ static jint TJCompressor_compress if (ProcessSystemProperties(env) < 0) goto bailout; - BAILIF0(srcBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); - BAILIF0(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); + BAILIF0NOEC(srcBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); + BAILIF0NOEC(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); if (tjCompress2(handle, &srcBuf[y * actualPitch + x * tjPixelSize[pf]], width, pitch, height, pf, &jpegBuf, &jpegSize, jpegSubsamp, @@ -326,9 +337,11 @@ JNIEXPORT jint JNICALL Java_org_libjpegturbo_turbojpeg_TJCompressor_compressFrom tjhandle handle = 0; unsigned long jpegSize = 0; jbyteArray jSrcPlanes[3] = { NULL, NULL, NULL }; - const unsigned char *srcPlanes[3]; + const unsigned char *srcPlanesTmp[3] = { NULL, NULL, NULL }; + const unsigned char *srcPlanes[3] = { NULL, NULL, NULL }; + jint srcOffsetsTmp[3] = { 0, 0, 0 }, srcStridesTmp[3] = { 0, 0, 0 }; + int srcOffsets[3] = { 0, 0, 0 }, srcStrides[3] = { 0, 0, 0 }; unsigned char *jpegBuf = NULL; - int *srcOffsets = NULL, *srcStrides = NULL; int nc = (subsamp == org_libjpegturbo_turbojpeg_TJ_SAMP_GRAY ? 1 : 3), i; GET_HANDLE(); @@ -351,56 +364,53 @@ JNIEXPORT jint JNICALL Java_org_libjpegturbo_turbojpeg_TJCompressor_compressFrom if (ProcessSystemProperties(env) < 0) goto bailout; -#define RELEASE_ARRAYS_COMPRESSFROMYUV() { \ - SAFE_RELEASE(dst, jpegBuf); \ - for (i = 0; i < nc; i++) \ - SAFE_RELEASE(jSrcPlanes[i], srcPlanes[i]); \ - SAFE_RELEASE(jSrcStrides, srcStrides); \ - SAFE_RELEASE(jSrcOffsets, srcOffsets); \ -} + (*env)->GetIntArrayRegion(env, jSrcOffsets, 0, nc, srcOffsetsTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + srcOffsets[i] = srcOffsetsTmp[i]; + + (*env)->GetIntArrayRegion(env, jSrcStrides, 0, nc, srcStridesTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + srcStrides[i] = srcStridesTmp[i]; - BAILIF0(srcOffsets = (*env)->GetPrimitiveArrayCritical(env, jSrcOffsets, 0)); - BAILIF0(srcStrides = (*env)->GetPrimitiveArrayCritical(env, jSrcStrides, 0)); for (i = 0; i < nc; i++) { int planeSize = tjPlaneSizeYUV(i, width, srcStrides[i], height, subsamp); int pw = tjPlaneWidth(i, width, subsamp); - if (planeSize < 0 || pw < 0) { - RELEASE_ARRAYS_COMPRESSFROMYUV(); + if (planeSize < 0 || pw < 0) THROW_ARG(tjGetErrorStr()); - } - if (srcOffsets[i] < 0) { - RELEASE_ARRAYS_COMPRESSFROMYUV(); + if (srcOffsets[i] < 0) THROW_ARG("Invalid argument in compressFromYUV()"); - } - if (srcStrides[i] < 0 && srcOffsets[i] - planeSize + pw < 0) { - RELEASE_ARRAYS_COMPRESSFROMYUV(); + if (srcStrides[i] < 0 && srcOffsets[i] - planeSize + pw < 0) THROW_ARG("Negative plane stride would cause memory to be accessed below plane boundary"); - } BAILIF0(jSrcPlanes[i] = (*env)->GetObjectArrayElement(env, srcobjs, i)); if ((*env)->GetArrayLength(env, jSrcPlanes[i]) < - srcOffsets[i] + planeSize) { - RELEASE_ARRAYS_COMPRESSFROMYUV(); + srcOffsets[i] + planeSize) THROW_ARG("Source plane is not large enough"); - } - - BAILIF0(srcPlanes[i] = - (*env)->GetPrimitiveArrayCritical(env, jSrcPlanes[i], 0)); - srcPlanes[i] = &srcPlanes[i][srcOffsets[i]]; } - BAILIF0(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); + for (i = 0; i < nc; i++) { + BAILIF0NOEC(srcPlanesTmp[i] = + (*env)->GetPrimitiveArrayCritical(env, jSrcPlanes[i], 0)); + srcPlanes[i] = &srcPlanesTmp[i][srcOffsets[i]]; + } + BAILIF0NOEC(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); if (tjCompressFromYUVPlanes(handle, srcPlanes, width, srcStrides, height, subsamp, &jpegBuf, &jpegSize, jpegQual, flags | TJFLAG_NOREALLOC) == -1) { - RELEASE_ARRAYS_COMPRESSFROMYUV(); + SAFE_RELEASE(dst, jpegBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jSrcPlanes[i], srcPlanesTmp[i]); THROW_TJ(); } bailout: - RELEASE_ARRAYS_COMPRESSFROMYUV(); + SAFE_RELEASE(dst, jpegBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jSrcPlanes[i], srcPlanesTmp[i]); return (jint)jpegSize; } @@ -411,9 +421,12 @@ static void TJCompressor_encodeYUV { tjhandle handle = 0; jsize arraySize = 0, actualPitch; + unsigned char *srcBuf = NULL; jbyteArray jDstPlanes[3] = { NULL, NULL, NULL }; - unsigned char *srcBuf = NULL, *dstPlanes[3]; - int *dstOffsets = NULL, *dstStrides = NULL; + unsigned char *dstPlanesTmp[3] = { NULL, NULL, NULL }; + unsigned char *dstPlanes[3] = { NULL, NULL, NULL }; + jint dstOffsetsTmp[3] = { 0, 0, 0 }, dstStridesTmp[3] = { 0, 0, 0 }; + int dstOffsets[3] = { 0, 0, 0 }, dstStrides[3] = { 0, 0, 0 }; int nc = (subsamp == org_libjpegturbo_turbojpeg_TJ_SAMP_GRAY ? 1 : 3), i; GET_HANDLE(); @@ -438,56 +451,53 @@ static void TJCompressor_encodeYUV if ((*env)->GetArrayLength(env, src) * srcElementSize < arraySize) THROW_ARG("Source buffer is not large enough"); -#define RELEASE_ARRAYS_ENCODEYUV() { \ - SAFE_RELEASE(src, srcBuf); \ - for (i = 0; i < nc; i++) \ - SAFE_RELEASE(jDstPlanes[i], dstPlanes[i]); \ - SAFE_RELEASE(jDstStrides, dstStrides); \ - SAFE_RELEASE(jDstOffsets, dstOffsets); \ -} + (*env)->GetIntArrayRegion(env, jDstOffsets, 0, nc, dstOffsetsTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + dstOffsets[i] = dstOffsetsTmp[i]; + + (*env)->GetIntArrayRegion(env, jDstStrides, 0, nc, dstStridesTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + dstStrides[i] = dstStridesTmp[i]; - BAILIF0(dstOffsets = (*env)->GetPrimitiveArrayCritical(env, jDstOffsets, 0)); - BAILIF0(dstStrides = (*env)->GetPrimitiveArrayCritical(env, jDstStrides, 0)); for (i = 0; i < nc; i++) { int planeSize = tjPlaneSizeYUV(i, width, dstStrides[i], height, subsamp); int pw = tjPlaneWidth(i, width, subsamp); - if (planeSize < 0 || pw < 0) { - RELEASE_ARRAYS_ENCODEYUV(); + if (planeSize < 0 || pw < 0) THROW_ARG(tjGetErrorStr()); - } - if (dstOffsets[i] < 0) { - RELEASE_ARRAYS_ENCODEYUV(); + if (dstOffsets[i] < 0) THROW_ARG("Invalid argument in encodeYUV()"); - } - if (dstStrides[i] < 0 && dstOffsets[i] - planeSize + pw < 0) { - RELEASE_ARRAYS_ENCODEYUV(); + if (dstStrides[i] < 0 && dstOffsets[i] - planeSize + pw < 0) THROW_ARG("Negative plane stride would cause memory to be accessed below plane boundary"); - } BAILIF0(jDstPlanes[i] = (*env)->GetObjectArrayElement(env, dstobjs, i)); if ((*env)->GetArrayLength(env, jDstPlanes[i]) < - dstOffsets[i] + planeSize) { - RELEASE_ARRAYS_ENCODEYUV(); + dstOffsets[i] + planeSize) THROW_ARG("Destination plane is not large enough"); - } - - BAILIF0(dstPlanes[i] = - (*env)->GetPrimitiveArrayCritical(env, jDstPlanes[i], 0)); - dstPlanes[i] = &dstPlanes[i][dstOffsets[i]]; } - BAILIF0(srcBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); + for (i = 0; i < nc; i++) { + BAILIF0NOEC(dstPlanesTmp[i] = + (*env)->GetPrimitiveArrayCritical(env, jDstPlanes[i], 0)); + dstPlanes[i] = &dstPlanesTmp[i][dstOffsets[i]]; + } + BAILIF0NOEC(srcBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); if (tjEncodeYUVPlanes(handle, &srcBuf[y * actualPitch + x * tjPixelSize[pf]], width, pitch, height, pf, dstPlanes, dstStrides, subsamp, flags) == -1) { - RELEASE_ARRAYS_ENCODEYUV(); + SAFE_RELEASE(src, srcBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jDstPlanes[i], dstPlanesTmp[i]); THROW_TJ(); } bailout: - RELEASE_ARRAYS_ENCODEYUV(); + SAFE_RELEASE(src, srcBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jDstPlanes[i], dstPlanesTmp[i]); } /* TurboJPEG 1.4.x: TJCompressor::encodeYUV() byte source */ @@ -542,8 +552,8 @@ static void JNICALL TJCompressor_encodeYUV_12 (jsize)tjBufSizeYUV(width, height, subsamp)) THROW_ARG("Destination buffer is not large enough"); - BAILIF0(srcBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); - BAILIF0(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); + BAILIF0NOEC(srcBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); + BAILIF0NOEC(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); if (tjEncodeYUV2(handle, srcBuf, width, pitch, height, pf, dstBuf, subsamp, flags) == -1) { @@ -662,7 +672,7 @@ JNIEXPORT void JNICALL Java_org_libjpegturbo_turbojpeg_TJDecompressor_decompress if ((*env)->GetArrayLength(env, src) < jpegSize) THROW_ARG("Source buffer is not large enough"); - BAILIF0(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); + BAILIF0NOEC(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); if (tjDecompressHeader3(handle, jpegBuf, (unsigned long)jpegSize, &width, &height, &jpegSubsamp, &jpegColorspace) == -1) { @@ -710,8 +720,8 @@ static void TJDecompressor_decompress if ((*env)->GetArrayLength(env, dst) * dstElementSize < arraySize) THROW_ARG("Destination buffer is not large enough"); - BAILIF0(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); - BAILIF0(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); + BAILIF0NOEC(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); + BAILIF0NOEC(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); if (tjDecompress2(handle, jpegBuf, (unsigned long)jpegSize, &dstBuf[y * actualPitch + x * tjPixelSize[pf]], width, @@ -785,9 +795,12 @@ JNIEXPORT void JNICALL Java_org_libjpegturbo_turbojpeg_TJDecompressor_decompress jintArray jDstStrides, jint desiredHeight, jint flags) { tjhandle handle = 0; + unsigned char *jpegBuf = NULL; jbyteArray jDstPlanes[3] = { NULL, NULL, NULL }; - unsigned char *jpegBuf = NULL, *dstPlanes[3]; - int *dstOffsets = NULL, *dstStrides = NULL; + unsigned char *dstPlanesTmp[3] = { NULL, NULL, NULL }; + unsigned char *dstPlanes[3] = { NULL, NULL, NULL }; + jint dstOffsetsTmp[3] = { 0, 0, 0 }, dstStridesTmp[3] = { 0, 0, 0 }; + int dstOffsets[3] = { 0, 0, 0 }, dstStrides[3] = { 0, 0, 0 }; int jpegSubsamp = -1, jpegWidth = 0, jpegHeight = 0; int nc = 0, i, width, height, scaledWidth, scaledHeight, nsf = 0; tjscalingfactor *sf; @@ -821,57 +834,54 @@ JNIEXPORT void JNICALL Java_org_libjpegturbo_turbojpeg_TJDecompressor_decompress if (i >= nsf) THROW_ARG("Could not scale down to desired image dimensions"); -#define RELEASE_ARRAYS_DECOMPRESSTOYUV() { \ - SAFE_RELEASE(src, jpegBuf); \ - for (i = 0; i < nc; i++) \ - SAFE_RELEASE(jDstPlanes[i], dstPlanes[i]); \ - SAFE_RELEASE(jDstStrides, dstStrides); \ - SAFE_RELEASE(jDstOffsets, dstOffsets); \ -} + (*env)->GetIntArrayRegion(env, jDstOffsets, 0, nc, dstOffsetsTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + dstOffsets[i] = dstOffsetsTmp[i]; + + (*env)->GetIntArrayRegion(env, jDstStrides, 0, nc, dstStridesTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + dstStrides[i] = dstStridesTmp[i]; - BAILIF0(dstOffsets = (*env)->GetPrimitiveArrayCritical(env, jDstOffsets, 0)); - BAILIF0(dstStrides = (*env)->GetPrimitiveArrayCritical(env, jDstStrides, 0)); for (i = 0; i < nc; i++) { int planeSize = tjPlaneSizeYUV(i, scaledWidth, dstStrides[i], scaledHeight, jpegSubsamp); int pw = tjPlaneWidth(i, scaledWidth, jpegSubsamp); - if (planeSize < 0 || pw < 0) { - RELEASE_ARRAYS_DECOMPRESSTOYUV(); + if (planeSize < 0 || pw < 0) THROW_ARG(tjGetErrorStr()); - } - if (dstOffsets[i] < 0) { - RELEASE_ARRAYS_DECOMPRESSTOYUV(); + if (dstOffsets[i] < 0) THROW_ARG("Invalid argument in decompressToYUV()"); - } - if (dstStrides[i] < 0 && dstOffsets[i] - planeSize + pw < 0) { - RELEASE_ARRAYS_DECOMPRESSTOYUV(); + if (dstStrides[i] < 0 && dstOffsets[i] - planeSize + pw < 0) THROW_ARG("Negative plane stride would cause memory to be accessed below plane boundary"); - } BAILIF0(jDstPlanes[i] = (*env)->GetObjectArrayElement(env, dstobjs, i)); if ((*env)->GetArrayLength(env, jDstPlanes[i]) < - dstOffsets[i] + planeSize) { - RELEASE_ARRAYS_DECOMPRESSTOYUV(); + dstOffsets[i] + planeSize) THROW_ARG("Destination plane is not large enough"); - } - - BAILIF0(dstPlanes[i] = - (*env)->GetPrimitiveArrayCritical(env, jDstPlanes[i], 0)); - dstPlanes[i] = &dstPlanes[i][dstOffsets[i]]; } - BAILIF0(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); + for (i = 0; i < nc; i++) { + BAILIF0NOEC(dstPlanesTmp[i] = + (*env)->GetPrimitiveArrayCritical(env, jDstPlanes[i], 0)); + dstPlanes[i] = &dstPlanesTmp[i][dstOffsets[i]]; + } + BAILIF0NOEC(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); if (tjDecompressToYUVPlanes(handle, jpegBuf, (unsigned long)jpegSize, dstPlanes, desiredWidth, dstStrides, desiredHeight, flags) == -1) { - RELEASE_ARRAYS_DECOMPRESSTOYUV(); + SAFE_RELEASE(src, jpegBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jDstPlanes[i], dstPlanesTmp[i]); THROW_TJ(); } bailout: - RELEASE_ARRAYS_DECOMPRESSTOYUV(); + SAFE_RELEASE(src, jpegBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jDstPlanes[i], dstPlanesTmp[i]); } /* TurboJPEG 1.2.x: TJDecompressor::decompressToYUV() */ @@ -897,8 +907,8 @@ JNIEXPORT void JNICALL Java_org_libjpegturbo_turbojpeg_TJDecompressor_decompress (jsize)tjBufSizeYUV(jpegWidth, jpegHeight, jpegSubsamp)) THROW_ARG("Destination buffer is not large enough"); - BAILIF0(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); - BAILIF0(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); + BAILIF0NOEC(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, src, 0)); + BAILIF0NOEC(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); if (tjDecompressToYUV(handle, jpegBuf, (unsigned long)jpegSize, dstBuf, flags) == -1) { @@ -920,9 +930,11 @@ static void TJDecompressor_decodeYUV tjhandle handle = 0; jsize arraySize = 0, actualPitch; jbyteArray jSrcPlanes[3] = { NULL, NULL, NULL }; - const unsigned char *srcPlanes[3]; + const unsigned char *srcPlanesTmp[3] = { NULL, NULL, NULL }; + const unsigned char *srcPlanes[3] = { NULL, NULL, NULL }; + jint srcOffsetsTmp[3] = { 0, 0, 0 }, srcStridesTmp[3] = { 0, 0, 0 }; + int srcOffsets[3] = { 0, 0, 0 }, srcStrides[3] = { 0, 0, 0 }; unsigned char *dstBuf = NULL; - int *srcOffsets = NULL, *srcStrides = NULL; int nc = (subsamp == org_libjpegturbo_turbojpeg_TJ_SAMP_GRAY ? 1 : 3), i; GET_HANDLE(); @@ -946,56 +958,53 @@ static void TJDecompressor_decodeYUV if ((*env)->GetArrayLength(env, dst) * dstElementSize < arraySize) THROW_ARG("Destination buffer is not large enough"); -#define RELEASE_ARRAYS_DECODEYUV() { \ - SAFE_RELEASE(dst, dstBuf); \ - for (i = 0; i < nc; i++) \ - SAFE_RELEASE(jSrcPlanes[i], srcPlanes[i]); \ - SAFE_RELEASE(jSrcStrides, srcStrides); \ - SAFE_RELEASE(jSrcOffsets, srcOffsets); \ -} + (*env)->GetIntArrayRegion(env, jSrcOffsets, 0, nc, srcOffsetsTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + srcOffsets[i] = srcOffsetsTmp[i]; + + (*env)->GetIntArrayRegion(env, jSrcStrides, 0, nc, srcStridesTmp); + if ((*env)->ExceptionCheck(env)) goto bailout; + for (i = 0; i < 3; i++) + srcStrides[i] = srcStridesTmp[i]; - BAILIF0(srcOffsets = (*env)->GetPrimitiveArrayCritical(env, jSrcOffsets, 0)); - BAILIF0(srcStrides = (*env)->GetPrimitiveArrayCritical(env, jSrcStrides, 0)); for (i = 0; i < nc; i++) { int planeSize = tjPlaneSizeYUV(i, width, srcStrides[i], height, subsamp); int pw = tjPlaneWidth(i, width, subsamp); - if (planeSize < 0 || pw < 0) { - RELEASE_ARRAYS_DECODEYUV(); + if (planeSize < 0 || pw < 0) THROW_ARG(tjGetErrorStr()); - } - if (srcOffsets[i] < 0) { - RELEASE_ARRAYS_DECODEYUV(); + if (srcOffsets[i] < 0) THROW_ARG("Invalid argument in decodeYUV()"); - } - if (srcStrides[i] < 0 && srcOffsets[i] - planeSize + pw < 0) { - RELEASE_ARRAYS_DECODEYUV(); + if (srcStrides[i] < 0 && srcOffsets[i] - planeSize + pw < 0) THROW_ARG("Negative plane stride would cause memory to be accessed below plane boundary"); - } BAILIF0(jSrcPlanes[i] = (*env)->GetObjectArrayElement(env, srcobjs, i)); if ((*env)->GetArrayLength(env, jSrcPlanes[i]) < - srcOffsets[i] + planeSize) { - RELEASE_ARRAYS_DECODEYUV(); + srcOffsets[i] + planeSize) THROW_ARG("Source plane is not large enough"); - } - - BAILIF0(srcPlanes[i] = - (*env)->GetPrimitiveArrayCritical(env, jSrcPlanes[i], 0)); - srcPlanes[i] = &srcPlanes[i][srcOffsets[i]]; } - BAILIF0(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); + for (i = 0; i < nc; i++) { + BAILIF0NOEC(srcPlanesTmp[i] = + (*env)->GetPrimitiveArrayCritical(env, jSrcPlanes[i], 0)); + srcPlanes[i] = &srcPlanesTmp[i][srcOffsets[i]]; + } + BAILIF0NOEC(dstBuf = (*env)->GetPrimitiveArrayCritical(env, dst, 0)); if (tjDecodeYUVPlanes(handle, srcPlanes, srcStrides, subsamp, &dstBuf[y * actualPitch + x * tjPixelSize[pf]], width, pitch, height, pf, flags) == -1) { - RELEASE_ARRAYS_DECODEYUV(); + SAFE_RELEASE(dst, dstBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jSrcPlanes[i], srcPlanesTmp[i]); THROW_TJ(); } bailout: - RELEASE_ARRAYS_DECODEYUV(); + SAFE_RELEASE(dst, dstBuf); + for (i = 0; i < nc; i++) + SAFE_RELEASE(jSrcPlanes[i], srcPlanesTmp[i]); } /* TurboJPEG 1.4.x: TJDecompressor::decodeYUV() byte destination */ @@ -1194,6 +1203,10 @@ JNIEXPORT jintArray JNICALL Java_org_libjpegturbo_turbojpeg_TJTransformer_transf for (i = 0; i < n; i++) { int w = jpegWidth, h = jpegHeight; + if (t[i].op == TJXOP_TRANSPOSE || t[i].op == TJXOP_TRANSVERSE || + t[i].op == TJXOP_ROT90 || t[i].op == TJXOP_ROT270) { + w = jpegHeight; h = jpegWidth; + } if (t[i].r.w != 0) w = t[i].r.w; if (t[i].r.h != 0) h = t[i].r.h; BAILIF0(jdstBufs[i] = (*env)->GetObjectArrayElement(env, dstobjs, i)); @@ -1201,10 +1214,10 @@ JNIEXPORT jintArray JNICALL Java_org_libjpegturbo_turbojpeg_TJTransformer_transf tjBufSize(w, h, jpegSubsamp)) THROW_ARG("Destination buffer is not large enough"); } - BAILIF0(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, jsrcBuf, 0)); + BAILIF0NOEC(jpegBuf = (*env)->GetPrimitiveArrayCritical(env, jsrcBuf, 0)); for (i = 0; i < n; i++) - BAILIF0(dstBufs[i] = - (*env)->GetPrimitiveArrayCritical(env, jdstBufs[i], 0)); + BAILIF0NOEC(dstBufs[i] = + (*env)->GetPrimitiveArrayCritical(env, jdstBufs[i], 0)); if (tjTransform(handle, jpegBuf, jpegSize, n, dstBufs, dstSizes, t, flags | TJFLAG_NOREALLOC) == -1) { diff --git a/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile b/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile old mode 100755 new mode 100644 index 5477fed2c36..07a429be91e --- a/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile +++ b/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile @@ -1,14 +1,14 @@ TURBOJPEG_1.0 { global: - tjInitCompress; - tjCompress; TJBUFSIZE; - tjInitDecompress; - tjDecompressHeader; + tjCompress; tjDecompress; + tjDecompressHeader; tjDestroy; tjGetErrorStr; + tjInitCompress; + tjInitDecompress; local: *; }; diff --git a/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile.jni b/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile.jni old mode 100755 new mode 100644 index 44327912b32..4ae25aad9f9 --- a/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile.jni +++ b/third-party/mozjpeg/mozjpeg/turbojpeg-mapfile.jni @@ -1,14 +1,14 @@ TURBOJPEG_1.0 { global: - tjInitCompress; - tjCompress; TJBUFSIZE; - tjInitDecompress; - tjDecompressHeader; + tjCompress; tjDecompress; + tjDecompressHeader; tjDestroy; tjGetErrorStr; + tjInitCompress; + tjInitDecompress; local: *; }; diff --git a/third-party/mozjpeg/mozjpeg/turbojpeg.c b/third-party/mozjpeg/mozjpeg/turbojpeg.c index f8fd36bdd7a..a5e45f7959d 100644 --- a/third-party/mozjpeg/mozjpeg/turbojpeg.c +++ b/third-party/mozjpeg/mozjpeg/turbojpeg.c @@ -1,5 +1,8 @@ /* - * Copyright (C)2009-2019 D. R. Commander. All Rights Reserved. + * Copyright (C)2009-2023 D. R. Commander. All Rights Reserved. + * Copyright (C)2021 Alex Richardson. All Rights Reserved. + * mozjpeg Modifications: + * Copyright (C) 2014, Mozilla Corporation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,9 +32,8 @@ /* TurboJPEG/LJT: this implements the TurboJPEG API using libjpeg or libjpeg-turbo */ -#include -#include #include +#include #include #define JPEG_INTERNALS #include @@ -55,7 +57,7 @@ extern void jpeg_mem_src_tj(j_decompress_ptr, const unsigned char *, /* Error handling (based on example in example.txt) */ -static char errStr[JMSG_LENGTH_MAX] = "No error"; +static THREAD_LOCAL char errStr[JMSG_LENGTH_MAX] = "No error"; struct my_error_mgr { struct jpeg_error_mgr pub; @@ -98,7 +100,7 @@ static void my_emit_message(j_common_ptr cinfo, int msg_level) } -/* Global structures, macros, etc. */ +/********************** Global structures, macros, etc. **********************/ enum { COMPRESS = 1, DECOMPRESS = 2 }; @@ -111,6 +113,32 @@ typedef struct _tjinstance { boolean isInstanceError; } tjinstance; +struct my_progress_mgr { + struct jpeg_progress_mgr pub; + tjinstance *this; +}; +typedef struct my_progress_mgr *my_progress_ptr; + +static void my_progress_monitor(j_common_ptr dinfo) +{ + my_error_ptr myerr = (my_error_ptr)dinfo->err; + my_progress_ptr myprog = (my_progress_ptr)dinfo->progress; + + if (dinfo->is_decompressor) { + int scan_no = ((j_decompress_ptr)dinfo)->input_scan_number; + + if (scan_no > 500) { + SNPRINTF(myprog->this->errStr, JMSG_LENGTH_MAX, + "Progressive JPEG image has more than 500 scans"); + SNPRINTF(errStr, JMSG_LENGTH_MAX, + "Progressive JPEG image has more than 500 scans"); + myprog->this->isInstanceError = TRUE; + myerr->warning = FALSE; + longjmp(myerr->setjmp_buffer, 1); + } + } +} + static const int pixelsize[TJ_NUMSAMP] = { 3, 3, 3, 1, 3, 3 }; static const JXFORM_CODE xformtypes[TJ_NUMXOP] = { @@ -165,25 +193,39 @@ static int cs2pf[JPEG_NUMCS] = { }; #define THROWG(m) { \ - snprintf(errStr, JMSG_LENGTH_MAX, "%s", m); \ + SNPRINTF(errStr, JMSG_LENGTH_MAX, "%s", m); \ retval = -1; goto bailout; \ } +#ifdef _MSC_VER #define THROW_UNIX(m) { \ - snprintf(errStr, JMSG_LENGTH_MAX, "%s\n%s", m, strerror(errno)); \ + char strerrorBuf[80] = { 0 }; \ + strerror_s(strerrorBuf, 80, errno); \ + SNPRINTF(errStr, JMSG_LENGTH_MAX, "%s\n%s", m, strerrorBuf); \ retval = -1; goto bailout; \ } +#else +#define THROW_UNIX(m) { \ + SNPRINTF(errStr, JMSG_LENGTH_MAX, "%s\n%s", m, strerror(errno)); \ + retval = -1; goto bailout; \ +} +#endif #define THROW(m) { \ - snprintf(this->errStr, JMSG_LENGTH_MAX, "%s", m); \ + SNPRINTF(this->errStr, JMSG_LENGTH_MAX, "%s", m); \ this->isInstanceError = TRUE; THROWG(m) \ } +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +/* Private flag that triggers different TurboJPEG API behavior when fuzzing */ +#define TJFLAG_FUZZING (1 << 30) +#endif + #define GET_INSTANCE(handle) \ tjinstance *this = (tjinstance *)handle; \ j_compress_ptr cinfo = NULL; \ j_decompress_ptr dinfo = NULL; \ \ if (!this) { \ - snprintf(errStr, JMSG_LENGTH_MAX, "Invalid handle"); \ + SNPRINTF(errStr, JMSG_LENGTH_MAX, "Invalid handle"); \ return -1; \ } \ cinfo = &this->cinfo; dinfo = &this->dinfo; \ @@ -195,7 +237,7 @@ static int cs2pf[JPEG_NUMCS] = { j_compress_ptr cinfo = NULL; \ \ if (!this) { \ - snprintf(errStr, JMSG_LENGTH_MAX, "Invalid handle"); \ + SNPRINTF(errStr, JMSG_LENGTH_MAX, "Invalid handle"); \ return -1; \ } \ cinfo = &this->cinfo; \ @@ -207,7 +249,7 @@ static int cs2pf[JPEG_NUMCS] = { j_decompress_ptr dinfo = NULL; \ \ if (!this) { \ - snprintf(errStr, JMSG_LENGTH_MAX, "Invalid handle"); \ + SNPRINTF(errStr, JMSG_LENGTH_MAX, "Invalid handle"); \ return -1; \ } \ dinfo = &this->dinfo; \ @@ -233,31 +275,38 @@ static int getPixelFormat(int pixelSize, int flags) return -1; } -static int setCompDefaults(struct jpeg_compress_struct *cinfo, int pixelFormat, - int subsamp, int jpegQual, int flags) +static void setCompDefaults(struct jpeg_compress_struct *cinfo, + int pixelFormat, int subsamp, int jpegQual, + int flags) { - int retval = 0; #ifndef NO_GETENV - char *env = NULL; + char env[7] = { 0 }; #endif cinfo->in_color_space = pf2cs[pixelFormat]; cinfo->input_components = tjPixelSize[pixelFormat]; +#ifndef NO_GETENV + if (!GETENV_S(env, 7, "TJ_REVERT") && !strcmp(env, "1")) + cinfo->master->compress_profile=JCP_FASTEST; +#endif jpeg_set_defaults(cinfo); #ifndef NO_GETENV - if ((env = getenv("TJ_OPTIMIZE")) != NULL && strlen(env) > 0 && - !strcmp(env, "1")) + if (!GETENV_S(env, 7, "TJ_OPTIMIZE") && !strcmp(env, "1")) cinfo->optimize_coding = TRUE; - if ((env = getenv("TJ_ARITHMETIC")) != NULL && strlen(env) > 0 && - !strcmp(env, "1")) + if (!GETENV_S(env, 7, "TJ_ARITHMETIC") && !strcmp(env, "1")) cinfo->arith_code = TRUE; - if ((env = getenv("TJ_RESTART")) != NULL && strlen(env) > 0) { + if (!GETENV_S(env, 7, "TJ_RESTART") && strlen(env) > 0) { int temp = -1; char tempc = 0; +#ifdef _MSC_VER + if (sscanf_s(env, "%d%c", &temp, &tempc, 1) >= 1 && temp >= 0 && + temp <= 65535) { +#else if (sscanf(env, "%d%c", &temp, &tempc) >= 1 && temp >= 0 && temp <= 65535) { +#endif if (toupper(tempc) == 'B') { cinfo->restart_interval = temp; cinfo->restart_in_rows = 0; @@ -281,14 +330,19 @@ static int setCompDefaults(struct jpeg_compress_struct *cinfo, int pixelFormat, else jpeg_set_colorspace(cinfo, JCS_YCbCr); +#ifdef C_PROGRESSIVE_SUPPORTED if (flags & TJFLAG_PROGRESSIVE) jpeg_simple_progression(cinfo); #ifndef NO_GETENV - else if ((env = getenv("TJ_PROGRESSIVE")) != NULL && strlen(env) > 0 && - !strcmp(env, "1")) + else if (!GETENV_S(env, 7, "TJ_PROGRESSIVE") && !strcmp(env, "1")) jpeg_simple_progression(cinfo); +#endif #endif + /* Set scan pattern again as colorspace might have changed */ + if(cinfo->master->compress_profile == JCP_MAX_COMPRESSION) + jpeg_simple_progression(cinfo); + cinfo->comp_info[0].h_samp_factor = tjMCUWidth[subsamp] / 8; cinfo->comp_info[1].h_samp_factor = 1; cinfo->comp_info[2].h_samp_factor = 1; @@ -299,8 +353,6 @@ static int setCompDefaults(struct jpeg_compress_struct *cinfo, int pixelFormat, cinfo->comp_info[2].v_samp_factor = 1; if (cinfo->num_components > 3) cinfo->comp_info[3].v_samp_factor = tjMCUHeight[subsamp] / 8; - - return retval; } @@ -315,7 +367,7 @@ static int getSubsamp(j_decompress_ptr dinfo) if (dinfo->num_components == 1 && dinfo->jpeg_color_space == JCS_GRAYSCALE) return TJSAMP_GRAY; - for (i = 0; i < NUMSUBOPT; i++) { + for (i = 0; i < TJ_NUMSAMP; i++) { if (dinfo->num_components == pixelsize[i] || ((dinfo->jpeg_color_space == JCS_YCCK || dinfo->jpeg_color_space == JCS_CMYK) && @@ -377,15 +429,16 @@ static int getSubsamp(j_decompress_ptr dinfo) retval = i; break; } } + } } } - } return retval; } -/* General API functions */ +/*************************** General API functions ***************************/ +/* TurboJPEG 2.0+ */ DLLEXPORT char *tjGetErrorStr2(tjhandle handle) { tjinstance *this = (tjinstance *)handle; @@ -398,12 +451,14 @@ DLLEXPORT char *tjGetErrorStr2(tjhandle handle) } +/* TurboJPEG 1.0+ */ DLLEXPORT char *tjGetErrorStr(void) { return errStr; } +/* TurboJPEG 2.0+ */ DLLEXPORT int tjGetErrorCode(tjhandle handle) { tjinstance *this = (tjinstance *)handle; @@ -413,6 +468,7 @@ DLLEXPORT int tjGetErrorCode(tjhandle handle) } +/* TurboJPEG 1.0+ */ DLLEXPORT int tjDestroy(tjhandle handle) { GET_INSTANCE(handle); @@ -430,19 +486,21 @@ DLLEXPORT int tjDestroy(tjhandle handle) with turbojpeg.dll for compatibility reasons. However, these functions can potentially be used for other purposes by different implementations. */ +/* TurboJPEG 1.2+ */ DLLEXPORT void tjFree(unsigned char *buf) { free(buf); } +/* TurboJPEG 1.2+ */ DLLEXPORT unsigned char *tjAlloc(int bytes) { return (unsigned char *)malloc(bytes); } -/* Compressor */ +/******************************** Compressor *********************************/ static tjhandle _tjInitCompress(tjinstance *this) { @@ -467,6 +525,14 @@ static tjhandle _tjInitCompress(tjinstance *this) } jpeg_create_compress(&this->cinfo); + +#ifndef NO_GETENV + /* This is used in unit tests */ + char *env = NULL; + if((env=getenv("TJ_REVERT"))!=NULL && strlen(env)>0 && !strcmp(env, "1")) + jpeg_c_set_int_param(&this->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST); +#endif + /* Make an initial call so it will create the destination manager */ jpeg_mem_dest_tj(&this->cinfo, &buf, &size, 0); @@ -474,27 +540,29 @@ static tjhandle _tjInitCompress(tjinstance *this) return (tjhandle)this; } +/* TurboJPEG 1.0+ */ DLLEXPORT tjhandle tjInitCompress(void) { tjinstance *this = NULL; if ((this = (tjinstance *)malloc(sizeof(tjinstance))) == NULL) { - snprintf(errStr, JMSG_LENGTH_MAX, + SNPRINTF(errStr, JMSG_LENGTH_MAX, "tjInitCompress(): Memory allocation failure"); return NULL; } - MEMZERO(this, sizeof(tjinstance)); - snprintf(this->errStr, JMSG_LENGTH_MAX, "No error"); + memset(this, 0, sizeof(tjinstance)); + SNPRINTF(this->errStr, JMSG_LENGTH_MAX, "No error"); return _tjInitCompress(this); } +/* TurboJPEG 1.2+ */ DLLEXPORT unsigned long tjBufSize(int width, int height, int jpegSubsamp) { unsigned long long retval = 0; int mcuw, mcuh, chromasf; - if (width < 1 || height < 1 || jpegSubsamp < 0 || jpegSubsamp >= NUMSUBOPT) + if (width < 1 || height < 1 || jpegSubsamp < 0 || jpegSubsamp >= TJ_NUMSAMP) THROWG("tjBufSize(): Invalid argument"); /* This allows for rare corner cases in which a JPEG image can actually be @@ -511,6 +579,7 @@ DLLEXPORT unsigned long tjBufSize(int width, int height, int jpegSubsamp) return (unsigned long)retval; } +/* TurboJPEG 1.0+ */ DLLEXPORT unsigned long TJBUFSIZE(int width, int height) { unsigned long long retval = 0; @@ -530,19 +599,20 @@ DLLEXPORT unsigned long TJBUFSIZE(int width, int height) } -DLLEXPORT unsigned long tjBufSizeYUV2(int width, int pad, int height, +/* TurboJPEG 1.4+ */ +DLLEXPORT unsigned long tjBufSizeYUV2(int width, int align, int height, int subsamp) { unsigned long long retval = 0; int nc, i; - if (subsamp < 0 || subsamp >= NUMSUBOPT) + if (align < 1 || !IS_POW2(align) || subsamp < 0 || subsamp >= TJ_NUMSAMP) THROWG("tjBufSizeYUV2(): Invalid argument"); nc = (subsamp == TJSAMP_GRAY ? 1 : 3); for (i = 0; i < nc; i++) { int pw = tjPlaneWidth(i, width, subsamp); - int stride = PAD(pw, pad); + int stride = PAD(pw, align); int ph = tjPlaneHeight(i, height, subsamp); if (pw < 0 || ph < 0) return -1; @@ -555,20 +625,24 @@ DLLEXPORT unsigned long tjBufSizeYUV2(int width, int pad, int height, return (unsigned long)retval; } +/* TurboJPEG 1.2+ */ DLLEXPORT unsigned long tjBufSizeYUV(int width, int height, int subsamp) { return tjBufSizeYUV2(width, 4, height, subsamp); } +/* TurboJPEG 1.1+ */ DLLEXPORT unsigned long TJBUFSIZEYUV(int width, int height, int subsamp) { return tjBufSizeYUV(width, height, subsamp); } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjPlaneWidth(int componentID, int width, int subsamp) { - int pw, nc, retval = 0; + unsigned long long pw, retval = 0; + int nc; if (width < 1 || subsamp < 0 || subsamp >= TJ_NUMSAMP) THROWG("tjPlaneWidth(): Invalid argument"); @@ -576,20 +650,25 @@ DLLEXPORT int tjPlaneWidth(int componentID, int width, int subsamp) if (componentID < 0 || componentID >= nc) THROWG("tjPlaneWidth(): Invalid argument"); - pw = PAD(width, tjMCUWidth[subsamp] / 8); + pw = PAD((unsigned long long)width, tjMCUWidth[subsamp] / 8); if (componentID == 0) retval = pw; else retval = pw * 8 / tjMCUWidth[subsamp]; + if (retval > (unsigned long long)INT_MAX) + THROWG("tjPlaneWidth(): Width is too large"); + bailout: - return retval; + return (int)retval; } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjPlaneHeight(int componentID, int height, int subsamp) { - int ph, nc, retval = 0; + unsigned long long ph, retval = 0; + int nc; if (height < 1 || subsamp < 0 || subsamp >= TJ_NUMSAMP) THROWG("tjPlaneHeight(): Invalid argument"); @@ -597,24 +676,28 @@ DLLEXPORT int tjPlaneHeight(int componentID, int height, int subsamp) if (componentID < 0 || componentID >= nc) THROWG("tjPlaneHeight(): Invalid argument"); - ph = PAD(height, tjMCUHeight[subsamp] / 8); + ph = PAD((unsigned long long)height, tjMCUHeight[subsamp] / 8); if (componentID == 0) retval = ph; else retval = ph * 8 / tjMCUHeight[subsamp]; + if (retval > (unsigned long long)INT_MAX) + THROWG("tjPlaneHeight(): Height is too large"); + bailout: - return retval; + return (int)retval; } +/* TurboJPEG 1.4+ */ DLLEXPORT unsigned long tjPlaneSizeYUV(int componentID, int width, int stride, int height, int subsamp) { unsigned long long retval = 0; int pw, ph; - if (width < 1 || height < 1 || subsamp < 0 || subsamp >= NUMSUBOPT) + if (width < 1 || height < 1 || subsamp < 0 || subsamp >= TJ_NUMSAMP) THROWG("tjPlaneSizeYUV(): Invalid argument"); pw = tjPlaneWidth(componentID, width, subsamp); @@ -633,12 +716,14 @@ DLLEXPORT unsigned long tjPlaneSizeYUV(int componentID, int width, int stride, } +/* TurboJPEG 1.2+ */ DLLEXPORT int tjCompress2(tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char **jpegBuf, unsigned long *jpegSize, int jpegSubsamp, int jpegQual, int flags) { - int i, retval = 0, alloc = 1; + int i, retval = 0; + boolean alloc = TRUE; JSAMPROW *row_pointer = NULL; GET_CINSTANCE(handle) @@ -648,7 +733,7 @@ DLLEXPORT int tjCompress2(tjhandle handle, const unsigned char *srcBuf, if (srcBuf == NULL || width <= 0 || pitch < 0 || height <= 0 || pixelFormat < 0 || pixelFormat >= TJ_NUMPF || jpegBuf == NULL || - jpegSize == NULL || jpegSubsamp < 0 || jpegSubsamp >= NUMSUBOPT || + jpegSize == NULL || jpegSubsamp < 0 || jpegSubsamp >= TJ_NUMSAMP || jpegQual < 0 || jpegQual > 100) THROW("tjCompress2(): Invalid argument"); @@ -666,17 +751,16 @@ DLLEXPORT int tjCompress2(tjhandle handle, const unsigned char *srcBuf, cinfo->image_height = height; #ifndef NO_PUTENV - if (flags & TJFLAG_FORCEMMX) putenv("JSIMD_FORCEMMX=1"); - else if (flags & TJFLAG_FORCESSE) putenv("JSIMD_FORCESSE=1"); - else if (flags & TJFLAG_FORCESSE2) putenv("JSIMD_FORCESSE2=1"); + if (flags & TJFLAG_FORCEMMX) PUTENV_S("JSIMD_FORCEMMX", "1"); + else if (flags & TJFLAG_FORCESSE) PUTENV_S("JSIMD_FORCESSE", "1"); + else if (flags & TJFLAG_FORCESSE2) PUTENV_S("JSIMD_FORCESSE2", "1"); #endif if (flags & TJFLAG_NOREALLOC) { - alloc = 0; *jpegSize = tjBufSize(width, height, jpegSubsamp); + alloc = FALSE; *jpegSize = tjBufSize(width, height, jpegSubsamp); } jpeg_mem_dest_tj(cinfo, jpegBuf, jpegSize, alloc); - if (setCompDefaults(cinfo, pixelFormat, jpegSubsamp, jpegQual, flags) == -1) - return -1; + setCompDefaults(cinfo, pixelFormat, jpegSubsamp, jpegQual, flags); jpeg_start_compress(cinfo, TRUE); for (i = 0; i < height; i++) { @@ -691,13 +775,17 @@ DLLEXPORT int tjCompress2(tjhandle handle, const unsigned char *srcBuf, jpeg_finish_compress(cinfo); bailout: - if (cinfo->global_state > CSTATE_START) jpeg_abort_compress(cinfo); + if (cinfo->global_state > CSTATE_START) { + if (alloc) (*cinfo->dest->term_destination) (cinfo); + jpeg_abort_compress(cinfo); + } free(row_pointer); if (this->jerr.warning) retval = -1; this->jerr.stopOnWarning = FALSE; return retval; } +/* TurboJPEG 1.0+ */ DLLEXPORT int tjCompress(tjhandle handle, unsigned char *srcBuf, int width, int pitch, int height, int pixelSize, unsigned char *jpegBuf, unsigned long *jpegSize, @@ -721,6 +809,7 @@ DLLEXPORT int tjCompress(tjhandle handle, unsigned char *srcBuf, int width, } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char **dstPlanes, @@ -747,13 +836,13 @@ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, if (srcBuf == NULL || width <= 0 || pitch < 0 || height <= 0 || pixelFormat < 0 || pixelFormat >= TJ_NUMPF || !dstPlanes || - !dstPlanes[0] || subsamp < 0 || subsamp >= NUMSUBOPT) + !dstPlanes[0] || subsamp < 0 || subsamp >= TJ_NUMSAMP) THROW("tjEncodeYUVPlanes(): Invalid argument"); if (subsamp != TJSAMP_GRAY && (!dstPlanes[1] || !dstPlanes[2])) THROW("tjEncodeYUVPlanes(): Invalid argument"); if (pixelFormat == TJPF_CMYK) - THROW("tjEncodeYUVPlanes(): Cannot generate YUV images from CMYK pixels"); + THROW("tjEncodeYUVPlanes(): Cannot generate YUV images from packed-pixel CMYK images"); if (pitch == 0) pitch = width * tjPixelSize[pixelFormat]; @@ -766,12 +855,12 @@ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, cinfo->image_height = height; #ifndef NO_PUTENV - if (flags & TJFLAG_FORCEMMX) putenv("JSIMD_FORCEMMX=1"); - else if (flags & TJFLAG_FORCESSE) putenv("JSIMD_FORCESSE=1"); - else if (flags & TJFLAG_FORCESSE2) putenv("JSIMD_FORCESSE2=1"); + if (flags & TJFLAG_FORCEMMX) PUTENV_S("JSIMD_FORCEMMX", "1"); + else if (flags & TJFLAG_FORCESSE) PUTENV_S("JSIMD_FORCESSE", "1"); + else if (flags & TJFLAG_FORCESSE2) PUTENV_S("JSIMD_FORCESSE2", "1"); #endif - if (setCompDefaults(cinfo, pixelFormat, subsamp, -1, flags) == -1) return -1; + setCompDefaults(cinfo, pixelFormat, subsamp, -1, flags); /* Execute only the parts of jpeg_start_compress() that we need. If we were to call the whole jpeg_start_compress() function, then it would try @@ -813,7 +902,7 @@ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, THROW("tjEncodeYUVPlanes(): Memory allocation failure"); for (row = 0; row < cinfo->max_v_samp_factor; row++) { unsigned char *_tmpbuf_aligned = - (unsigned char *)PAD((size_t)_tmpbuf[i], 32); + (unsigned char *)PAD((JUINTPTR)_tmpbuf[i], 32); tmpbuf[i][row] = &_tmpbuf_aligned[ PAD((compptr->width_in_blocks * cinfo->max_h_samp_factor * DCTSIZE) / @@ -829,7 +918,7 @@ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, THROW("tjEncodeYUVPlanes(): Memory allocation failure"); for (row = 0; row < compptr->v_samp_factor; row++) { unsigned char *_tmpbuf2_aligned = - (unsigned char *)PAD((size_t)_tmpbuf2[i], 32); + (unsigned char *)PAD((JUINTPTR)_tmpbuf2[i], 32); tmpbuf2[i][row] = &_tmpbuf2_aligned[PAD(compptr->width_in_blocks * DCTSIZE, 32) * row]; @@ -879,9 +968,10 @@ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, return retval; } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjEncodeYUV3(tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, - unsigned char *dstBuf, int pad, int subsamp, + unsigned char *dstBuf, int align, int subsamp, int flags) { unsigned char *dstPlanes[3]; @@ -891,14 +981,14 @@ DLLEXPORT int tjEncodeYUV3(tjhandle handle, const unsigned char *srcBuf, if (!this) THROWG("tjEncodeYUV3(): Invalid handle"); this->isInstanceError = FALSE; - if (width <= 0 || height <= 0 || dstBuf == NULL || pad < 0 || - !IS_POW2(pad) || subsamp < 0 || subsamp >= NUMSUBOPT) + if (width <= 0 || height <= 0 || dstBuf == NULL || align < 1 || + !IS_POW2(align) || subsamp < 0 || subsamp >= TJ_NUMSAMP) THROW("tjEncodeYUV3(): Invalid argument"); pw0 = tjPlaneWidth(0, width, subsamp); ph0 = tjPlaneHeight(0, height, subsamp); dstPlanes[0] = dstBuf; - strides[0] = PAD(pw0, pad); + strides[0] = PAD(pw0, align); if (subsamp == TJSAMP_GRAY) { strides[1] = strides[2] = 0; dstPlanes[1] = dstPlanes[2] = NULL; @@ -906,7 +996,7 @@ DLLEXPORT int tjEncodeYUV3(tjhandle handle, const unsigned char *srcBuf, int pw1 = tjPlaneWidth(1, width, subsamp); int ph1 = tjPlaneHeight(1, height, subsamp); - strides[1] = strides[2] = PAD(pw1, pad); + strides[1] = strides[2] = PAD(pw1, align); dstPlanes[1] = dstPlanes[0] + strides[0] * ph0; dstPlanes[2] = dstPlanes[1] + strides[1] * ph1; } @@ -918,6 +1008,7 @@ DLLEXPORT int tjEncodeYUV3(tjhandle handle, const unsigned char *srcBuf, return retval; } +/* TurboJPEG 1.2+ */ DLLEXPORT int tjEncodeYUV2(tjhandle handle, unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char *dstBuf, int subsamp, int flags) @@ -926,6 +1017,7 @@ DLLEXPORT int tjEncodeYUV2(tjhandle handle, unsigned char *srcBuf, int width, dstBuf, 4, subsamp, flags); } +/* TurboJPEG 1.1+ */ DLLEXPORT int tjEncodeYUV(tjhandle handle, unsigned char *srcBuf, int width, int pitch, int height, int pixelSize, unsigned char *dstBuf, int subsamp, int flags) @@ -936,6 +1028,7 @@ DLLEXPORT int tjEncodeYUV(tjhandle handle, unsigned char *srcBuf, int width, } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, const unsigned char **srcPlanes, int width, const int *strides, @@ -944,7 +1037,8 @@ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, unsigned long *jpegSize, int jpegQual, int flags) { - int i, row, retval = 0, alloc = 1; + int i, row, retval = 0; + boolean alloc = TRUE; int pw[MAX_COMPONENTS], ph[MAX_COMPONENTS], iw[MAX_COMPONENTS], tmpbufsize = 0, usetmpbuf = 0, th[MAX_COMPONENTS]; JSAMPLE *_tmpbuf = NULL, *ptr; @@ -961,7 +1055,7 @@ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, THROW("tjCompressFromYUVPlanes(): Instance has not been initialized for compression"); if (!srcPlanes || !srcPlanes[0] || width <= 0 || height <= 0 || - subsamp < 0 || subsamp >= NUMSUBOPT || jpegBuf == NULL || + subsamp < 0 || subsamp >= TJ_NUMSAMP || jpegBuf == NULL || jpegSize == NULL || jpegQual < 0 || jpegQual > 100) THROW("tjCompressFromYUVPlanes(): Invalid argument"); if (subsamp != TJSAMP_GRAY && (!srcPlanes[1] || !srcPlanes[2])) @@ -976,17 +1070,16 @@ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, cinfo->image_height = height; #ifndef NO_PUTENV - if (flags & TJFLAG_FORCEMMX) putenv("JSIMD_FORCEMMX=1"); - else if (flags & TJFLAG_FORCESSE) putenv("JSIMD_FORCESSE=1"); - else if (flags & TJFLAG_FORCESSE2) putenv("JSIMD_FORCESSE2=1"); + if (flags & TJFLAG_FORCEMMX) PUTENV_S("JSIMD_FORCEMMX", "1"); + else if (flags & TJFLAG_FORCESSE) PUTENV_S("JSIMD_FORCESSE", "1"); + else if (flags & TJFLAG_FORCESSE2) PUTENV_S("JSIMD_FORCESSE2", "1"); #endif if (flags & TJFLAG_NOREALLOC) { - alloc = 0; *jpegSize = tjBufSize(width, height, subsamp); + alloc = FALSE; *jpegSize = tjBufSize(width, height, subsamp); } jpeg_mem_dest_tj(cinfo, jpegBuf, jpegSize, alloc); - if (setCompDefaults(cinfo, TJPF_RGB, subsamp, jpegQual, flags) == -1) - return -1; + setCompDefaults(cinfo, TJPF_RGB, subsamp, jpegQual, flags); cinfo->raw_data_in = TRUE; jpeg_start_compress(cinfo, TRUE); @@ -1060,7 +1153,10 @@ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, jpeg_finish_compress(cinfo); bailout: - if (cinfo->global_state > CSTATE_START) jpeg_abort_compress(cinfo); + if (cinfo->global_state > CSTATE_START) { + if (alloc) (*cinfo->dest->term_destination) (cinfo); + jpeg_abort_compress(cinfo); + } for (i = 0; i < MAX_COMPONENTS; i++) { free(tmpbuf[i]); free(inbuf[i]); @@ -1071,8 +1167,9 @@ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, return retval; } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjCompressFromYUV(tjhandle handle, const unsigned char *srcBuf, - int width, int pad, int height, int subsamp, + int width, int align, int height, int subsamp, unsigned char **jpegBuf, unsigned long *jpegSize, int jpegQual, int flags) @@ -1084,14 +1181,14 @@ DLLEXPORT int tjCompressFromYUV(tjhandle handle, const unsigned char *srcBuf, if (!this) THROWG("tjCompressFromYUV(): Invalid handle"); this->isInstanceError = FALSE; - if (srcBuf == NULL || width <= 0 || pad < 1 || height <= 0 || subsamp < 0 || - subsamp >= NUMSUBOPT) + if (srcBuf == NULL || width <= 0 || align < 1 || !IS_POW2(align) || + height <= 0 || subsamp < 0 || subsamp >= TJ_NUMSAMP) THROW("tjCompressFromYUV(): Invalid argument"); pw0 = tjPlaneWidth(0, width, subsamp); ph0 = tjPlaneHeight(0, height, subsamp); srcPlanes[0] = srcBuf; - strides[0] = PAD(pw0, pad); + strides[0] = PAD(pw0, align); if (subsamp == TJSAMP_GRAY) { strides[1] = strides[2] = 0; srcPlanes[1] = srcPlanes[2] = NULL; @@ -1099,7 +1196,7 @@ DLLEXPORT int tjCompressFromYUV(tjhandle handle, const unsigned char *srcBuf, int pw1 = tjPlaneWidth(1, width, subsamp); int ph1 = tjPlaneHeight(1, height, subsamp); - strides[1] = strides[2] = PAD(pw1, pad); + strides[1] = strides[2] = PAD(pw1, align); srcPlanes[1] = srcPlanes[0] + strides[0] * ph0; srcPlanes[2] = srcPlanes[1] + strides[1] * ph1; } @@ -1112,7 +1209,7 @@ DLLEXPORT int tjCompressFromYUV(tjhandle handle, const unsigned char *srcBuf, } -/* Decompressor */ +/******************************* Decompressor ********************************/ static tjhandle _tjInitDecompress(tjinstance *this) { @@ -1142,21 +1239,23 @@ static tjhandle _tjInitDecompress(tjinstance *this) return (tjhandle)this; } +/* TurboJPEG 1.0+ */ DLLEXPORT tjhandle tjInitDecompress(void) { tjinstance *this; if ((this = (tjinstance *)malloc(sizeof(tjinstance))) == NULL) { - snprintf(errStr, JMSG_LENGTH_MAX, + SNPRINTF(errStr, JMSG_LENGTH_MAX, "tjInitDecompress(): Memory allocation failure"); return NULL; } - MEMZERO(this, sizeof(tjinstance)); - snprintf(this->errStr, JMSG_LENGTH_MAX, "No error"); + memset(this, 0, sizeof(tjinstance)); + SNPRINTF(this->errStr, JMSG_LENGTH_MAX, "No error"); return _tjInitDecompress(this); } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjDecompressHeader3(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, int *width, @@ -1179,7 +1278,13 @@ DLLEXPORT int tjDecompressHeader3(tjhandle handle, } jpeg_mem_src_tj(dinfo, jpegBuf, jpegSize); - jpeg_read_header(dinfo, TRUE); + + /* jpeg_read_header() calls jpeg_abort() and returns JPEG_HEADER_TABLES_ONLY + if the datastream is a tables-only datastream. Since we aren't using a + suspending data source, the only other value it can return is + JPEG_HEADER_OK. */ + if (jpeg_read_header(dinfo, FALSE) == JPEG_HEADER_TABLES_ONLY) + return 0; *width = dinfo->image_width; *height = dinfo->image_height; @@ -1207,6 +1312,7 @@ DLLEXPORT int tjDecompressHeader3(tjhandle handle, return retval; } +/* TurboJPEG 1.1+ */ DLLEXPORT int tjDecompressHeader2(tjhandle handle, unsigned char *jpegBuf, unsigned long jpegSize, int *width, int *height, int *jpegSubsamp) @@ -1217,6 +1323,7 @@ DLLEXPORT int tjDecompressHeader2(tjhandle handle, unsigned char *jpegBuf, jpegSubsamp, &jpegColorspace); } +/* TurboJPEG 1.0+ */ DLLEXPORT int tjDecompressHeader(tjhandle handle, unsigned char *jpegBuf, unsigned long jpegSize, int *width, int *height) @@ -1228,19 +1335,21 @@ DLLEXPORT int tjDecompressHeader(tjhandle handle, unsigned char *jpegBuf, } -DLLEXPORT tjscalingfactor *tjGetScalingFactors(int *numscalingfactors) +/* TurboJPEG 1.2+ */ +DLLEXPORT tjscalingfactor *tjGetScalingFactors(int *numScalingFactors) { - if (numscalingfactors == NULL) { - snprintf(errStr, JMSG_LENGTH_MAX, + if (numScalingFactors == NULL) { + SNPRINTF(errStr, JMSG_LENGTH_MAX, "tjGetScalingFactors(): Invalid argument"); return NULL; } - *numscalingfactors = NUMSF; + *numScalingFactors = NUMSF; return (tjscalingfactor *)sf; } +/* TurboJPEG 1.2+ */ DLLEXPORT int tjDecompress2(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, int width, int pitch, int height, int pixelFormat, @@ -1248,6 +1357,7 @@ DLLEXPORT int tjDecompress2(tjhandle handle, const unsigned char *jpegBuf, { JSAMPROW *row_pointer = NULL; int i, retval = 0, jpegwidth, jpegheight, scaledw, scaledh; + struct my_progress_mgr progress; GET_DINSTANCE(handle); this->jerr.stopOnWarning = (flags & TJFLAG_STOPONWARNING) ? TRUE : FALSE; @@ -1259,11 +1369,19 @@ DLLEXPORT int tjDecompress2(tjhandle handle, const unsigned char *jpegBuf, THROW("tjDecompress2(): Invalid argument"); #ifndef NO_PUTENV - if (flags & TJFLAG_FORCEMMX) putenv("JSIMD_FORCEMMX=1"); - else if (flags & TJFLAG_FORCESSE) putenv("JSIMD_FORCESSE=1"); - else if (flags & TJFLAG_FORCESSE2) putenv("JSIMD_FORCESSE2=1"); + if (flags & TJFLAG_FORCEMMX) PUTENV_S("JSIMD_FORCEMMX", "1"); + else if (flags & TJFLAG_FORCESSE) PUTENV_S("JSIMD_FORCESSE", "1"); + else if (flags & TJFLAG_FORCESSE2) PUTENV_S("JSIMD_FORCESSE2", "1"); #endif + if (flags & TJFLAG_LIMITSCANS) { + memset(&progress, 0, sizeof(struct my_progress_mgr)); + progress.pub.progress_monitor = my_progress_monitor; + progress.this = this; + dinfo->progress = &progress.pub; + } else + dinfo->progress = NULL; + if (setjmp(this->jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. */ retval = -1; goto bailout; @@ -1319,6 +1437,7 @@ DLLEXPORT int tjDecompress2(tjhandle handle, const unsigned char *jpegBuf, return retval; } +/* TurboJPEG 1.0+ */ DLLEXPORT int tjDecompress(tjhandle handle, unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, int width, int pitch, int height, int pixelSize, @@ -1332,8 +1451,8 @@ DLLEXPORT int tjDecompress(tjhandle handle, unsigned char *jpegBuf, } -static int setDecodeDefaults(struct jpeg_decompress_struct *dinfo, - int pixelFormat, int subsamp, int flags) +static void setDecodeDefaults(struct jpeg_decompress_struct *dinfo, + int pixelFormat, int subsamp, int flags) { int i; @@ -1368,8 +1487,6 @@ static int setDecodeDefaults(struct jpeg_decompress_struct *dinfo, if (dinfo->quant_tbl_ptrs[i] == NULL) dinfo->quant_tbl_ptrs[i] = jpeg_alloc_quant_table((j_common_ptr)dinfo); } - - return 0; } @@ -1382,6 +1499,7 @@ static void my_reset_marker_reader(j_decompress_ptr dinfo) { } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjDecodeYUVPlanes(tjhandle handle, const unsigned char **srcPlanes, const int *strides, int subsamp, @@ -1407,7 +1525,7 @@ DLLEXPORT int tjDecodeYUVPlanes(tjhandle handle, if ((this->init & DECOMPRESS) == 0) THROW("tjDecodeYUVPlanes(): Instance has not been initialized for decompression"); - if (!srcPlanes || !srcPlanes[0] || subsamp < 0 || subsamp >= NUMSUBOPT || + if (!srcPlanes || !srcPlanes[0] || subsamp < 0 || subsamp >= TJ_NUMSAMP || dstBuf == NULL || width <= 0 || pitch < 0 || height <= 0 || pixelFormat < 0 || pixelFormat >= TJ_NUMPF) THROW("tjDecodeYUVPlanes(): Invalid argument"); @@ -1420,24 +1538,22 @@ DLLEXPORT int tjDecodeYUVPlanes(tjhandle handle, } if (pixelFormat == TJPF_CMYK) - THROW("tjDecodeYUVPlanes(): Cannot decode YUV images into CMYK pixels."); + THROW("tjDecodeYUVPlanes(): Cannot decode YUV images into packed-pixel CMYK images."); if (pitch == 0) pitch = width * tjPixelSize[pixelFormat]; dinfo->image_width = width; dinfo->image_height = height; #ifndef NO_PUTENV - if (flags & TJFLAG_FORCEMMX) putenv("JSIMD_FORCEMMX=1"); - else if (flags & TJFLAG_FORCESSE) putenv("JSIMD_FORCESSE=1"); - else if (flags & TJFLAG_FORCESSE2) putenv("JSIMD_FORCESSE2=1"); + if (flags & TJFLAG_FORCEMMX) PUTENV_S("JSIMD_FORCEMMX", "1"); + else if (flags & TJFLAG_FORCESSE) PUTENV_S("JSIMD_FORCESSE", "1"); + else if (flags & TJFLAG_FORCESSE2) PUTENV_S("JSIMD_FORCESSE2", "1"); #endif dinfo->progressive_mode = dinfo->inputctl->has_multiple_scans = FALSE; dinfo->Ss = dinfo->Ah = dinfo->Al = 0; dinfo->Se = DCTSIZE2 - 1; - if (setDecodeDefaults(dinfo, pixelFormat, subsamp, flags) == -1) { - retval = -1; goto bailout; - } + setDecodeDefaults(dinfo, pixelFormat, subsamp, flags); old_read_markers = dinfo->marker->read_markers; dinfo->marker->read_markers = my_read_markers; old_reset_marker_reader = dinfo->marker->reset_marker_reader; @@ -1481,7 +1597,7 @@ DLLEXPORT int tjDecodeYUVPlanes(tjhandle handle, THROW("tjDecodeYUVPlanes(): Memory allocation failure"); for (row = 0; row < compptr->v_samp_factor; row++) { unsigned char *_tmpbuf_aligned = - (unsigned char *)PAD((size_t)_tmpbuf[i], 32); + (unsigned char *)PAD((JUINTPTR)_tmpbuf[i], 32); tmpbuf[i][row] = &_tmpbuf_aligned[PAD(compptr->width_in_blocks * DCTSIZE, 32) * row]; @@ -1530,8 +1646,9 @@ DLLEXPORT int tjDecodeYUVPlanes(tjhandle handle, return retval; } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjDecodeYUV(tjhandle handle, const unsigned char *srcBuf, - int pad, int subsamp, unsigned char *dstBuf, + int align, int subsamp, unsigned char *dstBuf, int width, int pitch, int height, int pixelFormat, int flags) { @@ -1542,14 +1659,14 @@ DLLEXPORT int tjDecodeYUV(tjhandle handle, const unsigned char *srcBuf, if (!this) THROWG("tjDecodeYUV(): Invalid handle"); this->isInstanceError = FALSE; - if (srcBuf == NULL || pad < 0 || !IS_POW2(pad) || subsamp < 0 || - subsamp >= NUMSUBOPT || width <= 0 || height <= 0) + if (srcBuf == NULL || align < 1 || !IS_POW2(align) || subsamp < 0 || + subsamp >= TJ_NUMSAMP || width <= 0 || height <= 0) THROW("tjDecodeYUV(): Invalid argument"); pw0 = tjPlaneWidth(0, width, subsamp); ph0 = tjPlaneHeight(0, height, subsamp); srcPlanes[0] = srcBuf; - strides[0] = PAD(pw0, pad); + strides[0] = PAD(pw0, align); if (subsamp == TJSAMP_GRAY) { strides[1] = strides[2] = 0; srcPlanes[1] = srcPlanes[2] = NULL; @@ -1557,7 +1674,7 @@ DLLEXPORT int tjDecodeYUV(tjhandle handle, const unsigned char *srcBuf, int pw1 = tjPlaneWidth(1, width, subsamp); int ph1 = tjPlaneHeight(1, height, subsamp); - strides[1] = strides[2] = PAD(pw1, pad); + strides[1] = strides[2] = PAD(pw1, align); srcPlanes[1] = srcPlanes[0] + strides[0] * ph0; srcPlanes[2] = srcPlanes[1] + strides[1] * ph1; } @@ -1569,6 +1686,7 @@ DLLEXPORT int tjDecodeYUV(tjhandle handle, const unsigned char *srcBuf, return retval; } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjDecompressToYUVPlanes(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, @@ -1582,6 +1700,7 @@ DLLEXPORT int tjDecompressToYUVPlanes(tjhandle handle, JSAMPLE *_tmpbuf = NULL, *ptr; JSAMPROW *outbuf[MAX_COMPONENTS], *tmpbuf[MAX_COMPONENTS]; int dctsize; + struct my_progress_mgr progress; GET_DINSTANCE(handle); this->jerr.stopOnWarning = (flags & TJFLAG_STOPONWARNING) ? TRUE : FALSE; @@ -1598,11 +1717,19 @@ DLLEXPORT int tjDecompressToYUVPlanes(tjhandle handle, THROW("tjDecompressToYUVPlanes(): Invalid argument"); #ifndef NO_PUTENV - if (flags & TJFLAG_FORCEMMX) putenv("JSIMD_FORCEMMX=1"); - else if (flags & TJFLAG_FORCESSE) putenv("JSIMD_FORCESSE=1"); - else if (flags & TJFLAG_FORCESSE2) putenv("JSIMD_FORCESSE2=1"); + if (flags & TJFLAG_FORCEMMX) PUTENV_S("JSIMD_FORCEMMX", "1"); + else if (flags & TJFLAG_FORCESSE) PUTENV_S("JSIMD_FORCESSE", "1"); + else if (flags & TJFLAG_FORCESSE2) PUTENV_S("JSIMD_FORCESSE2", "1"); #endif + if (flags & TJFLAG_LIMITSCANS) { + memset(&progress, 0, sizeof(struct my_progress_mgr)); + progress.pub.progress_monitor = my_progress_monitor; + progress.this = this; + dinfo->progress = &progress.pub; + } else + dinfo->progress = NULL; + if (setjmp(this->jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. */ retval = -1; goto bailout; @@ -1693,7 +1820,7 @@ DLLEXPORT int tjDecompressToYUVPlanes(tjhandle handle, for (i = 0; i < dinfo->num_components; i++) { jpeg_component_info *compptr = &dinfo->comp_info[i]; - if (jpegSubsamp == TJ_420) { + if (jpegSubsamp == TJSAMP_420) { /* When 4:2:0 subsampling is used with IDCT scaling, libjpeg will try to be clever and use the IDCT to perform upsampling on the U and V planes. For instance, if the output image is to be scaled by 1/2 @@ -1740,9 +1867,10 @@ DLLEXPORT int tjDecompressToYUVPlanes(tjhandle handle, return retval; } +/* TurboJPEG 1.4+ */ DLLEXPORT int tjDecompressToYUV2(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, - int width, int pad, int height, int flags) + int width, int align, int height, int flags) { unsigned char *dstPlanes[3]; int pw0, ph0, strides[3], retval = -1, jpegSubsamp = -1; @@ -1752,7 +1880,7 @@ DLLEXPORT int tjDecompressToYUV2(tjhandle handle, const unsigned char *jpegBuf, this->jerr.stopOnWarning = (flags & TJFLAG_STOPONWARNING) ? TRUE : FALSE; if (jpegBuf == NULL || jpegSize <= 0 || dstBuf == NULL || width < 0 || - pad < 1 || !IS_POW2(pad) || height < 0) + align < 1 || !IS_POW2(align) || height < 0) THROW("tjDecompressToYUV2(): Invalid argument"); if (setjmp(this->jerr.setjmp_buffer)) { @@ -1769,7 +1897,6 @@ DLLEXPORT int tjDecompressToYUV2(tjhandle handle, const unsigned char *jpegBuf, jpegwidth = dinfo->image_width; jpegheight = dinfo->image_height; if (width == 0) width = jpegwidth; if (height == 0) height = jpegheight; - for (i = 0; i < NUMSF; i++) { scaledw = TJSCALED(jpegwidth, sf[i]); scaledh = TJSCALED(jpegheight, sf[i]); @@ -1779,10 +1906,12 @@ DLLEXPORT int tjDecompressToYUV2(tjhandle handle, const unsigned char *jpegBuf, if (i >= NUMSF) THROW("tjDecompressToYUV2(): Could not scale down to desired image dimensions"); + width = scaledw; height = scaledh; + pw0 = tjPlaneWidth(0, width, jpegSubsamp); ph0 = tjPlaneHeight(0, height, jpegSubsamp); dstPlanes[0] = dstBuf; - strides[0] = PAD(pw0, pad); + strides[0] = PAD(pw0, align); if (jpegSubsamp == TJSAMP_GRAY) { strides[1] = strides[2] = 0; dstPlanes[1] = dstPlanes[2] = NULL; @@ -1790,7 +1919,7 @@ DLLEXPORT int tjDecompressToYUV2(tjhandle handle, const unsigned char *jpegBuf, int pw1 = tjPlaneWidth(1, width, jpegSubsamp); int ph1 = tjPlaneHeight(1, height, jpegSubsamp); - strides[1] = strides[2] = PAD(pw1, pad); + strides[1] = strides[2] = PAD(pw1, align); dstPlanes[1] = dstPlanes[0] + strides[0] * ph0; dstPlanes[2] = dstPlanes[1] + strides[1] * ph1; } @@ -1804,6 +1933,7 @@ DLLEXPORT int tjDecompressToYUV2(tjhandle handle, const unsigned char *jpegBuf, return retval; } +/* TurboJPEG 1.1+ */ DLLEXPORT int tjDecompressToYUV(tjhandle handle, unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, int flags) @@ -1812,20 +1942,21 @@ DLLEXPORT int tjDecompressToYUV(tjhandle handle, unsigned char *jpegBuf, } -/* Transformer */ +/******************************** Transformer ********************************/ +/* TurboJPEG 1.2+ */ DLLEXPORT tjhandle tjInitTransform(void) { tjinstance *this = NULL; tjhandle handle = NULL; if ((this = (tjinstance *)malloc(sizeof(tjinstance))) == NULL) { - snprintf(errStr, JMSG_LENGTH_MAX, + SNPRINTF(errStr, JMSG_LENGTH_MAX, "tjInitTransform(): Memory allocation failure"); return NULL; } - MEMZERO(this, sizeof(tjinstance)); - snprintf(this->errStr, JMSG_LENGTH_MAX, "No error"); + memset(this, 0, sizeof(tjinstance)); + SNPRINTF(this->errStr, JMSG_LENGTH_MAX, "No error"); handle = _tjInitCompress(this); if (!handle) return NULL; handle = _tjInitDecompress(this); @@ -1833,6 +1964,7 @@ DLLEXPORT tjhandle tjInitTransform(void) } +/* TurboJPEG 1.2+ */ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, int n, unsigned char **dstBufs, unsigned long *dstSizes, @@ -1841,6 +1973,8 @@ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, jpeg_transform_info *xinfo = NULL; jvirt_barray_ptr *srccoefs, *dstcoefs; int retval = 0, i, jpegSubsamp, saveMarkers = 0; + boolean alloc = TRUE; + struct my_progress_mgr progress; GET_INSTANCE(handle); this->jerr.stopOnWarning = (flags & TJFLAG_STOPONWARNING) ? TRUE : FALSE; @@ -1852,15 +1986,23 @@ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, THROW("tjTransform(): Invalid argument"); #ifndef NO_PUTENV - if (flags & TJFLAG_FORCEMMX) putenv("JSIMD_FORCEMMX=1"); - else if (flags & TJFLAG_FORCESSE) putenv("JSIMD_FORCESSE=1"); - else if (flags & TJFLAG_FORCESSE2) putenv("JSIMD_FORCESSE2=1"); + if (flags & TJFLAG_FORCEMMX) PUTENV_S("JSIMD_FORCEMMX", "1"); + else if (flags & TJFLAG_FORCESSE) PUTENV_S("JSIMD_FORCESSE", "1"); + else if (flags & TJFLAG_FORCESSE2) PUTENV_S("JSIMD_FORCESSE2", "1"); #endif + if (flags & TJFLAG_LIMITSCANS) { + memset(&progress, 0, sizeof(struct my_progress_mgr)); + progress.pub.progress_monitor = my_progress_monitor; + progress.this = this; + dinfo->progress = &progress.pub; + } else + dinfo->progress = NULL; + if ((xinfo = (jpeg_transform_info *)malloc(sizeof(jpeg_transform_info) * n)) == NULL) THROW("tjTransform(): Memory allocation failure"); - MEMZERO(xinfo, sizeof(jpeg_transform_info) * n); + memset(xinfo, 0, sizeof(jpeg_transform_info) * n); if (setjmp(this->jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. */ @@ -1904,12 +2046,12 @@ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, THROW("tjTransform(): Transform is not perfect"); if (xinfo[i].crop) { - if ((t[i].r.x % xinfo[i].iMCU_sample_width) != 0 || - (t[i].r.y % xinfo[i].iMCU_sample_height) != 0) { - snprintf(this->errStr, JMSG_LENGTH_MAX, + if ((t[i].r.x % tjMCUWidth[jpegSubsamp]) != 0 || + (t[i].r.y % tjMCUHeight[jpegSubsamp]) != 0) { + SNPRINTF(this->errStr, JMSG_LENGTH_MAX, "To crop this JPEG image, x must be a multiple of %d\n" "and y must be a multiple of %d.\n", - xinfo[i].iMCU_sample_width, xinfo[i].iMCU_sample_height); + tjMCUWidth[jpegSubsamp], tjMCUHeight[jpegSubsamp]); this->isInstanceError = TRUE; retval = -1; goto bailout; } @@ -1919,22 +2061,28 @@ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, srccoefs = jpeg_read_coefficients(dinfo); for (i = 0; i < n; i++) { - int w, h, alloc = 1; + int w, h; if (!xinfo[i].crop) { w = dinfo->image_width; h = dinfo->image_height; + if (t[i].op == TJXOP_TRANSPOSE || t[i].op == TJXOP_TRANSVERSE || + t[i].op == TJXOP_ROT90 || t[i].op == TJXOP_ROT270) { + w = dinfo->image_height; h = dinfo->image_width; + } } else { w = xinfo[i].crop_width; h = xinfo[i].crop_height; } if (flags & TJFLAG_NOREALLOC) { - alloc = 0; dstSizes[i] = tjBufSize(w, h, jpegSubsamp); + alloc = FALSE; dstSizes[i] = tjBufSize(w, h, jpegSubsamp); } if (!(t[i].options & TJXOPT_NOOUTPUT)) jpeg_mem_dest_tj(cinfo, &dstBufs[i], &dstSizes[i], alloc); jpeg_copy_critical_parameters(dinfo, cinfo); dstcoefs = jtransform_adjust_parameters(dinfo, cinfo, srccoefs, &xinfo[i]); +#ifdef C_PROGRESSIVE_SUPPORTED if (flags & TJFLAG_PROGRESSIVE || t[i].options & TJXOPT_PROGRESSIVE) jpeg_simple_progression(cinfo); +#endif if (!(t[i].options & TJXOPT_NOOUTPUT)) { jpeg_write_coefficients(cinfo, dstcoefs); jcopy_markers_execute(dinfo, cinfo, t[i].options & TJXOPT_COPYNONE ? @@ -1948,13 +2096,13 @@ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, for (ci = 0; ci < cinfo->num_components; ci++) { jpeg_component_info *compptr = &cinfo->comp_info[ci]; - tjregion arrayRegion = { - 0, 0, compptr->width_in_blocks * DCTSIZE, DCTSIZE - }; - tjregion planeRegion = { - 0, 0, compptr->width_in_blocks * DCTSIZE, - compptr->height_in_blocks * DCTSIZE - }; + tjregion arrayRegion = { 0, 0, 0, 0 }; + tjregion planeRegion = { 0, 0, 0, 0 }; + + arrayRegion.w = compptr->width_in_blocks * DCTSIZE; + arrayRegion.h = DCTSIZE; + planeRegion.w = compptr->width_in_blocks * DCTSIZE; + planeRegion.h = compptr->height_in_blocks * DCTSIZE; for (by = 0; by < compptr->height_in_blocks; by += compptr->v_samp_factor) { @@ -1977,7 +2125,10 @@ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, jpeg_finish_decompress(dinfo); bailout: - if (cinfo->global_state > CSTATE_START) jpeg_abort_compress(cinfo); + if (cinfo->global_state > CSTATE_START) { + if (alloc) (*cinfo->dest->term_destination) (cinfo); + jpeg_abort_compress(cinfo); + } if (dinfo->global_state > DSTATE_START) jpeg_abort_decompress(dinfo); free(xinfo); if (this->jerr.warning) retval = -1; @@ -1986,6 +2137,9 @@ DLLEXPORT int tjTransform(tjhandle handle, const unsigned char *jpegBuf, } +/*************************** Packed-Pixel Image I/O **************************/ + +/* TurboJPEG 2.0+ */ DLLEXPORT unsigned char *tjLoadImage(const char *filename, int *width, int align, int *height, int *pixelFormat, int flags) @@ -2010,7 +2164,11 @@ DLLEXPORT unsigned char *tjLoadImage(const char *filename, int *width, this = (tjinstance *)handle; cinfo = &this->cinfo; +#ifdef _MSC_VER + if (fopen_s(&file, filename, "rb") || file == NULL) +#else if ((file = fopen(filename, "rb")) == NULL) +#endif THROW_UNIX("tjLoadImage(): Cannot open input file"); if ((tempc = getc(file)) < 0 || ungetc(tempc, file) == EOF) @@ -2031,12 +2189,17 @@ DLLEXPORT unsigned char *tjLoadImage(const char *filename, int *width, invert = (flags & TJFLAG_BOTTOMUP) == 0; } else if (tempc == 'P') { if ((src = jinit_read_ppm(cinfo)) == NULL) - THROWG("tjLoadImage(): Could not initialize bitmap loader"); + THROWG("tjLoadImage(): Could not initialize PPM loader"); invert = (flags & TJFLAG_BOTTOMUP) != 0; } else THROWG("tjLoadImage(): Unsupported file type"); src->input_file = file; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + /* Refuse to load images larger than 1 Megapixel when fuzzing. */ + if (flags & TJFLAG_FUZZING) + src->max_pixels = 1048576; +#endif (*src->start_input) (cinfo, src); (*cinfo->mem->realize_virt_arrays) ((j_common_ptr)cinfo); @@ -2079,6 +2242,7 @@ DLLEXPORT unsigned char *tjLoadImage(const char *filename, int *width, } +/* TurboJPEG 2.0+ */ DLLEXPORT int tjSaveImage(const char *filename, unsigned char *buffer, int width, int pitch, int height, int pixelFormat, int flags) @@ -2101,7 +2265,11 @@ DLLEXPORT int tjSaveImage(const char *filename, unsigned char *buffer, this = (tjinstance *)handle; dinfo = &this->dinfo; +#ifdef _MSC_VER + if (fopen_s(&file, filename, "wb") || file == NULL) +#else if ((file = fopen(filename, "wb")) == NULL) +#endif THROW_UNIX("tjSaveImage(): Cannot open output file"); if (setjmp(this->jerr.setjmp_buffer)) { diff --git a/third-party/mozjpeg/mozjpeg/turbojpeg.h b/third-party/mozjpeg/mozjpeg/turbojpeg.h index 074f015f4fc..8664ab6f5cf 100644 --- a/third-party/mozjpeg/mozjpeg/turbojpeg.h +++ b/third-party/mozjpeg/mozjpeg/turbojpeg.h @@ -1,5 +1,6 @@ /* - * Copyright (C)2009-2015, 2017, 2020 D. R. Commander. All Rights Reserved. + * Copyright (C)2009-2015, 2017, 2020-2021, 2023 D. R. Commander. + * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -53,23 +54,24 @@ * Each plane is simply a 2D array of bytes, each byte representing the value * of one of the components (Y, Cb, or Cr) at a particular location in the * image. The width and height of each plane are determined by the image - * width, height, and level of chrominance subsampling. The luminance plane + * width, height, and level of chrominance subsampling. The luminance plane * width is the image width padded to the nearest multiple of the horizontal - * subsampling factor (2 in the case of 4:2:0 and 4:2:2, 4 in the case of - * 4:1:1, 1 in the case of 4:4:4 or grayscale.) Similarly, the luminance plane - * height is the image height padded to the nearest multiple of the vertical - * subsampling factor (2 in the case of 4:2:0 or 4:4:0, 1 in the case of 4:4:4 - * or grayscale.) This is irrespective of any additional padding that may be - * specified as an argument to the various YUV functions. The chrominance - * plane width is equal to the luminance plane width divided by the horizontal - * subsampling factor, and the chrominance plane height is equal to the - * luminance plane height divided by the vertical subsampling factor. + * subsampling factor (1 in the case of 4:4:4, grayscale, or 4:4:0; 2 in the + * case of 4:2:2 or 4:2:0; 4 in the case of 4:1:1.) Similarly, the luminance + * plane height is the image height padded to the nearest multiple of the + * vertical subsampling factor (1 in the case of 4:4:4, 4:2:2, grayscale, or + * 4:1:1; 2 in the case of 4:2:0 or 4:4:0.) This is irrespective of any + * additional padding that may be specified as an argument to the various YUV + * functions. The chrominance plane width is equal to the luminance plane + * width divided by the horizontal subsampling factor, and the chrominance + * plane height is equal to the luminance plane height divided by the vertical + * subsampling factor. * * For example, if the source image is 35 x 35 pixels and 4:2:2 subsampling is * used, then the luminance plane would be 36 x 35 bytes, and each of the - * chrominance planes would be 18 x 35 bytes. If you specify a line padding of - * 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, and - * each of the chrominance planes would be 20 x 35 bytes. + * chrominance planes would be 18 x 35 bytes. If you specify a row alignment + * of 4 bytes on top of this, then the luminance plane would be 36 x 35 bytes, + * and each of the chrominance planes would be 20 x 35 bytes. * * @{ */ @@ -85,8 +87,8 @@ * When pixels are converted from RGB to YCbCr (see #TJCS_YCbCr) or from CMYK * to YCCK (see #TJCS_YCCK) as part of the JPEG compression process, some of * the Cb and Cr (chrominance) components can be discarded or averaged together - * to produce a smaller image with little perceptible loss of image clarity - * (the human eye is more sensitive to small changes in brightness than to + * to produce a smaller image with little perceptible loss of image clarity. + * (The human eye is more sensitive to small changes in brightness than to * small changes in color.) This is called "chrominance subsampling". */ enum TJSAMP { @@ -244,8 +246,8 @@ enum TJPF { * vice versa, but the mapping is typically not 1:1 or reversible, nor can it * be defined with a simple formula. Thus, such a conversion is out of scope * for a codec library. However, the TurboJPEG API allows for compressing - * CMYK pixels into a YCCK JPEG image (see #TJCS_YCCK) and decompressing YCCK - * JPEG images into CMYK pixels. + * packed-pixel CMYK images into YCCK JPEG images (see #TJCS_YCCK) and + * decompressing YCCK JPEG images into packed-pixel CMYK images. */ TJPF_CMYK, /** @@ -257,9 +259,10 @@ enum TJPF { /** * Red offset (in bytes) for a given pixel format. This specifies the number * of bytes that the red component is offset from the start of the pixel. For - * instance, if a pixel of format TJ_BGRX is stored in char pixel[], - * then the red component will be pixel[tjRedOffset[TJ_BGRX]]. This - * will be -1 if the pixel format does not have a red component. + * instance, if a pixel of format TJPF_BGRX is stored in + * `unsigned char pixel[]`, then the red component will be + *`pixel[tjRedOffset[TJPF_BGRX]]`. This will be -1 if the pixel format does + * not have a red component. */ static const int tjRedOffset[TJ_NUMPF] = { 0, 2, 0, 2, 3, 1, -1, 0, 2, 3, 1, -1 @@ -267,31 +270,32 @@ static const int tjRedOffset[TJ_NUMPF] = { /** * Green offset (in bytes) for a given pixel format. This specifies the number * of bytes that the green component is offset from the start of the pixel. - * For instance, if a pixel of format TJ_BGRX is stored in - * char pixel[], then the green component will be - * pixel[tjGreenOffset[TJ_BGRX]]. This will be -1 if the pixel format - * does not have a green component. + * For instance, if a pixel of format TJPF_BGRX is stored in + * `unsigned char pixel[]`, then the green component will be + * `pixel[tjGreenOffset[TJPF_BGRX]]`. This will be -1 if the pixel format does + * not have a green component. */ static const int tjGreenOffset[TJ_NUMPF] = { 1, 1, 1, 1, 2, 2, -1, 1, 1, 2, 2, -1 }; /** * Blue offset (in bytes) for a given pixel format. This specifies the number - * of bytes that the Blue component is offset from the start of the pixel. For - * instance, if a pixel of format TJ_BGRX is stored in char pixel[], - * then the blue component will be pixel[tjBlueOffset[TJ_BGRX]]. This - * will be -1 if the pixel format does not have a blue component. + * of bytes that the blue component is offset from the start of the pixel. For + * instance, if a pixel of format TJPF_BGRX is stored in + * `unsigned char pixel[]`, then the blue component will be + * `pixel[tjBlueOffset[TJPF_BGRX]]`. This will be -1 if the pixel format does + * not have a blue component. */ static const int tjBlueOffset[TJ_NUMPF] = { 2, 0, 2, 0, 1, 3, -1, 2, 0, 1, 3, -1 }; /** * Alpha offset (in bytes) for a given pixel format. This specifies the number - * of bytes that the Alpha component is offset from the start of the pixel. - * For instance, if a pixel of format TJ_BGRA is stored in - * char pixel[], then the alpha component will be - * pixel[tjAlphaOffset[TJ_BGRA]]. This will be -1 if the pixel format - * does not have an alpha component. + * of bytes that the alpha component is offset from the start of the pixel. + * For instance, if a pixel of format TJPF_BGRA is stored in + * `unsigned char pixel[]`, then the alpha component will be + * `pixel[tjAlphaOffset[TJPF_BGRA]]`. This will be -1 if the pixel format does + * not have an alpha component. */ static const int tjAlphaOffset[TJ_NUMPF] = { -1, -1, -1, -1, -1, -1, -1, 3, 3, 0, 0, -1 @@ -317,8 +321,9 @@ enum TJCS { * RGB colorspace. When compressing the JPEG image, the R, G, and B * components in the source image are reordered into image planes, but no * colorspace conversion or subsampling is performed. RGB JPEG images can be - * decompressed to any of the extended RGB pixel formats or grayscale, but - * they cannot be decompressed to YUV images. + * decompressed to packed-pixel images with any of the extended RGB or + * grayscale pixel formats, but they cannot be decompressed to planar YUV + * images. */ TJCS_RGB = 0, /** @@ -331,25 +336,27 @@ enum TJCS { * original image. Originally, the analog equivalent of this transformation * allowed the same signal to drive both black & white and color televisions, * but JPEG images use YCbCr primarily because it allows the color data to be - * optionally subsampled for the purposes of reducing bandwidth or disk - * space. YCbCr is the most common JPEG colorspace, and YCbCr JPEG images - * can be compressed from and decompressed to any of the extended RGB pixel - * formats or grayscale, or they can be decompressed to YUV planar images. + * optionally subsampled for the purposes of reducing network or disk usage. + * YCbCr is the most common JPEG colorspace, and YCbCr JPEG images can be + * compressed from and decompressed to packed-pixel images with any of the + * extended RGB or grayscale pixel formats. YCbCr JPEG images can also be + * compressed from and decompressed to planar YUV images. */ TJCS_YCbCr, /** * Grayscale colorspace. The JPEG image retains only the luminance data (Y * component), and any color data from the source image is discarded. - * Grayscale JPEG images can be compressed from and decompressed to any of - * the extended RGB pixel formats or grayscale, or they can be decompressed - * to YUV planar images. + * Grayscale JPEG images can be compressed from and decompressed to + * packed-pixel images with any of the extended RGB or grayscale pixel + * formats, or they can be compressed from and decompressed to planar YUV + * images. */ TJCS_GRAY, /** * CMYK colorspace. When compressing the JPEG image, the C, M, Y, and K * components in the source image are reordered into image planes, but no * colorspace conversion or subsampling is performed. CMYK JPEG images can - * only be decompressed to CMYK pixels. + * only be decompressed to packed-pixel images with the CMYK pixel format. */ TJCS_CMYK, /** @@ -359,56 +366,54 @@ enum TJCS { * reversibly transformed into YCCK, and as with YCbCr, the chrominance * components in the YCCK pixels can be subsampled without incurring major * perceptual loss. YCCK JPEG images can only be compressed from and - * decompressed to CMYK pixels. + * decompressed to packed-pixel images with the CMYK pixel format. */ TJCS_YCCK }; /** - * The uncompressed source/destination image is stored in bottom-up (Windows, - * OpenGL) order, not top-down (X11) order. + * Rows in the packed-pixel source/destination image are stored in bottom-up + * (Windows, OpenGL) order rather than in top-down (X11) order. */ #define TJFLAG_BOTTOMUP 2 /** * When decompressing an image that was compressed using chrominance - * subsampling, use the fastest chrominance upsampling algorithm available in - * the underlying codec. The default is to use smooth upsampling, which - * creates a smooth transition between neighboring chrominance components in - * order to reduce upsampling artifacts in the decompressed image. + * subsampling, use the fastest chrominance upsampling algorithm available. + * The default is to use smooth upsampling, which creates a smooth transition + * between neighboring chrominance components in order to reduce upsampling + * artifacts in the decompressed image. */ #define TJFLAG_FASTUPSAMPLE 256 /** - * Disable buffer (re)allocation. If passed to one of the JPEG compression or - * transform functions, this flag will cause those functions to generate an - * error if the JPEG image buffer is invalid or too small rather than - * attempting to allocate or reallocate that buffer. This reproduces the - * behavior of earlier versions of TurboJPEG. + * Disable JPEG buffer (re)allocation. If passed to one of the JPEG + * compression or transform functions, this flag will cause those functions to + * generate an error if the JPEG destination buffer is invalid or too small, + * rather than attempt to allocate or reallocate that buffer. */ #define TJFLAG_NOREALLOC 1024 /** - * Use the fastest DCT/IDCT algorithm available in the underlying codec. The - * default if this flag is not specified is implementation-specific. For - * example, the implementation of TurboJPEG for libjpeg[-turbo] uses the fast - * algorithm by default when compressing, because this has been shown to have - * only a very slight effect on accuracy, but it uses the accurate algorithm - * when decompressing, because this has been shown to have a larger effect. + * Use the fastest DCT/IDCT algorithm available. The default if this flag is + * not specified is implementation-specific. For example, the implementation + * of the TurboJPEG API in libjpeg-turbo uses the fast algorithm by default + * when compressing, because this has been shown to have only a very slight + * effect on accuracy, but it uses the accurate algorithm when decompressing, + * because this has been shown to have a larger effect. */ #define TJFLAG_FASTDCT 2048 /** - * Use the most accurate DCT/IDCT algorithm available in the underlying codec. - * The default if this flag is not specified is implementation-specific. For - * example, the implementation of TurboJPEG for libjpeg[-turbo] uses the fast - * algorithm by default when compressing, because this has been shown to have - * only a very slight effect on accuracy, but it uses the accurate algorithm - * when decompressing, because this has been shown to have a larger effect. + * Use the most accurate DCT/IDCT algorithm available. The default if this + * flag is not specified is implementation-specific. For example, the + * implementation of the TurboJPEG API in libjpeg-turbo uses the fast algorithm + * by default when compressing, because this has been shown to have only a very + * slight effect on accuracy, but it uses the accurate algorithm when + * decompressing, because this has been shown to have a larger effect. */ #define TJFLAG_ACCURATEDCT 4096 /** * Immediately discontinue the current compression/decompression/transform - * operation if the underlying codec throws a warning (non-fatal error). The - * default behavior is to allow the operation to complete unless a fatal error - * is encountered. + * operation if a warning (non-fatal error) occurs. The default behavior is to + * allow the operation to complete unless a fatal error is encountered. */ #define TJFLAG_STOPONWARNING 8192 /** @@ -418,6 +423,16 @@ enum TJCS { * reduce compression and decompression performance considerably. */ #define TJFLAG_PROGRESSIVE 16384 +/** + * Limit the number of progressive JPEG scans that the decompression and + * transform functions will process. If a progressive JPEG image contains an + * unreasonably large number of scans, then this flag will cause the + * decompression and transform functions to return an error. The primary + * purpose of this is to allow security-critical applications to guard against + * an exploit of the progressive JPEG format described in + * this report. + */ +#define TJFLAG_LIMITSCANS 32768 /** @@ -430,8 +445,8 @@ enum TJCS { */ enum TJERR { /** - * The error was non-fatal and recoverable, but the image may still be - * corrupt. + * The error was non-fatal and recoverable, but the destination image may + * still be corrupt. */ TJERR_WARNING = 0, /** @@ -498,9 +513,9 @@ enum TJXOP { /** * This option will cause #tjTransform() to return an error if the transform is * not perfect. Lossless transforms operate on MCU blocks, whose size depends - * on the level of chrominance subsampling used (see #tjMCUWidth - * and #tjMCUHeight.) If the image's width or height is not evenly divisible - * by the MCU block size, then there will be partial MCU blocks on the right + * on the level of chrominance subsampling used (see #tjMCUWidth and + * #tjMCUHeight.) If the image's width or height is not evenly divisible by + * the MCU block size, then there will be partial MCU blocks on the right * and/or bottom edges. It is not possible to move these partial MCU blocks to * the top or left of the image, so any transform that would require that is * "imperfect." If this option is not specified, then any partial MCU blocks @@ -519,29 +534,28 @@ enum TJXOP { */ #define TJXOPT_CROP 4 /** - * This option will discard the color data in the input image and produce - * a grayscale output image. + * This option will discard the color data in the source image and produce a + * grayscale destination image. */ #define TJXOPT_GRAY 8 /** * This option will prevent #tjTransform() from outputting a JPEG image for - * this particular transform (this can be used in conjunction with a custom + * this particular transform. (This can be used in conjunction with a custom * filter to capture the transformed DCT coefficients without transcoding * them.) */ #define TJXOPT_NOOUTPUT 16 /** - * This option will enable progressive entropy coding in the output image + * This option will enable progressive entropy coding in the JPEG image * generated by this particular transform. Progressive entropy coding will * generally improve compression relative to baseline entropy coding (the - * default), but it will reduce compression and decompression performance - * considerably. + * default), but it will reduce decompression performance considerably. */ #define TJXOPT_PROGRESSIVE 32 /** * This option will prevent #tjTransform() from copying any extra markers - * (including EXIF and ICC profile data) from the source image to the output - * image. + * (including EXIF and ICC profile data) from the source image to the + * destination image. */ #define TJXOPT_COPYNONE 64 @@ -575,12 +589,12 @@ typedef struct { */ int y; /** - * The width of the cropping region. Setting this to 0 is the equivalent of + * The width of the cropping region. Setting this to 0 is the equivalent of * setting it to the width of the source JPEG image - x. */ int w; /** - * The height of the cropping region. Setting this to 0 is the equivalent of + * The height of the cropping region. Setting this to 0 is the equivalent of * setting it to the height of the source JPEG image - y. */ int h; @@ -599,7 +613,8 @@ typedef struct tjtransform { */ int op; /** - * The bitwise OR of one of more of the @ref TJXOPT_CROP "transform options" + * The bitwise OR of one of more of the @ref TJXOPT_COPYNONE + * "transform options" */ int options; /** @@ -608,10 +623,10 @@ typedef struct tjtransform { */ void *data; /** - * A callback function that can be used to modify the DCT coefficients - * after they are losslessly transformed but before they are transcoded to a - * new JPEG image. This allows for custom filters or other transformations - * to be applied in the frequency domain. + * A callback function that can be used to modify the DCT coefficients after + * they are losslessly transformed but before they are transcoded to a new + * JPEG image. This allows for custom filters or other transformations to be + * applied in the frequency domain. * * @param coeffs pointer to an array of transformed DCT coefficients. (NOTE: * this pointer is not guaranteed to be valid once the callback returns, so @@ -619,21 +634,21 @@ typedef struct tjtransform { * or library should make a copy of them within the body of the callback.) * * @param arrayRegion #tjregion structure containing the width and height of - * the array pointed to by coeffs as well as its offset relative to - * the component plane. TurboJPEG implementations may choose to split each + * the array pointed to by `coeffs` as well as its offset relative to the + * component plane. TurboJPEG implementations may choose to split each * component plane into multiple DCT coefficient arrays and call the callback * function once for each array. * * @param planeRegion #tjregion structure containing the width and height of - * the component plane to which coeffs belongs + * the component plane to which `coeffs` belongs * - * @param componentID ID number of the component plane to which - * coeffs belongs (Y, Cb, and Cr have, respectively, ID's of 0, 1, - * and 2 in typical JPEG images.) + * @param componentID ID number of the component plane to which `coeffs` + * belongs. (Y, Cb, and Cr have, respectively, ID's of 0, 1, and 2 in + * typical JPEG images.) * - * @param transformID ID number of the transformed image to which - * coeffs belongs. This is the same as the index of the transform - * in the transforms array that was passed to #tjTransform(). + * @param transformID ID number of the transformed image to which `coeffs` + * belongs. This is the same as the index of the transform in the + * `transforms` array that was passed to #tjTransform(). * * @param transform a pointer to a #tjtransform structure that specifies the * parameters and/or cropping region for this transform @@ -641,8 +656,8 @@ typedef struct tjtransform { * @return 0 if the callback was successful, or -1 if an error occurred. */ int (*customFilter) (short *coeffs, tjregion arrayRegion, - tjregion planeRegion, int componentIndex, - int transformIndex, struct tjtransform *transform); + tjregion planeRegion, int componentID, int transformID, + struct tjtransform *transform); } tjtransform; /** @@ -652,17 +667,17 @@ typedef void *tjhandle; /** - * Pad the given width to the nearest 32-bit boundary + * Pad the given width to the nearest multiple of 4 */ #define TJPAD(width) (((width) + 3) & (~3)) /** - * Compute the scaled value of dimension using the given scaling - * factor. This macro performs the integer equivalent of ceil(dimension * - * scalingFactor). + * Compute the scaled value of `dimension` using the given scaling factor. + * This macro performs the integer equivalent of `ceil(dimension * + * scalingFactor)`. */ #define TJSCALED(dimension, scalingFactor) \ - ((dimension * scalingFactor.num + scalingFactor.denom - 1) / \ + (((dimension) * scalingFactor.num + scalingFactor.denom - 1) / \ scalingFactor.denom) @@ -674,27 +689,27 @@ extern "C" { /** * Create a TurboJPEG compressor instance. * - * @return a handle to the newly-created instance, or NULL if an error - * occurred (see #tjGetErrorStr2().) + * @return a handle to the newly-created instance, or NULL if an error occurred + * (see #tjGetErrorStr2().) */ DLLEXPORT tjhandle tjInitCompress(void); /** - * Compress an RGB, grayscale, or CMYK image into a JPEG image. + * Compress a packed-pixel RGB, grayscale, or CMYK image into a JPEG image. * * @param handle a handle to a TurboJPEG compressor or transformer instance * - * @param srcBuf pointer to an image buffer containing RGB, grayscale, or - * CMYK pixels to be compressed + * @param srcBuf pointer to a buffer containing a packed-pixel RGB, grayscale, + * or CMYK source image to be compressed * * @param width width (in pixels) of the source image * - * @param pitch bytes per line in the source image. Normally, this should be - * width * #tjPixelSize[pixelFormat] if the image is unpadded, or - * #TJPAD(width * #tjPixelSize[pixelFormat]) if each line of the image - * is padded to the nearest 32-bit boundary, as is the case for Windows - * bitmaps. You can also be clever and use this parameter to skip lines, etc. + * @param pitch bytes per row in the source image. Normally this should be + * width * #tjPixelSize[pixelFormat], if the image is unpadded, or + * #TJPAD(width * #tjPixelSize[pixelFormat]) if each row of the image + * is padded to the nearest multiple of 4 bytes, as is the case for Windows + * bitmaps. You can also be clever and use this parameter to skip rows, etc. * Setting this parameter to 0 is the equivalent of setting it to * width * #tjPixelSize[pixelFormat]. * @@ -703,29 +718,28 @@ DLLEXPORT tjhandle tjInitCompress(void); * @param pixelFormat pixel format of the source image (see @ref TJPF * "Pixel formats".) * - * @param jpegBuf address of a pointer to an image buffer that will receive the - * JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer - * to accommodate the size of the JPEG image. Thus, you can choose to: + * @param jpegBuf address of a pointer to a byte buffer that will receive the + * JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to + * accommodate the size of the JPEG image. Thus, you can choose to: * -# pre-allocate the JPEG buffer with an arbitrary size using #tjAlloc() and * let TurboJPEG grow the buffer as needed, - * -# set *jpegBuf to NULL to tell TurboJPEG to allocate the buffer - * for you, or + * -# set `*jpegBuf` to NULL to tell TurboJPEG to allocate the buffer for you, + * or * -# pre-allocate the buffer to a "worst case" size determined by calling * #tjBufSize(). This should ensure that the buffer never has to be - * re-allocated (setting #TJFLAG_NOREALLOC guarantees that it won't be.) + * re-allocated. (Setting #TJFLAG_NOREALLOC guarantees that it won't be.) * . - * If you choose option 1, *jpegSize should be set to the size of your + * If you choose option 1, then `*jpegSize` should be set to the size of your * pre-allocated buffer. In any case, unless you have set #TJFLAG_NOREALLOC, - * you should always check *jpegBuf upon return from this function, as - * it may have changed. + * you should always check `*jpegBuf` upon return from this function, as it may + * have changed. * * @param jpegSize pointer to an unsigned long variable that holds the size of - * the JPEG image buffer. If *jpegBuf points to a pre-allocated - * buffer, then *jpegSize should be set to the size of the buffer. - * Upon return, *jpegSize will contain the size of the JPEG image (in - * bytes.) If *jpegBuf points to a JPEG image buffer that is being - * reused from a previous call to one of the JPEG compression functions, then - * *jpegSize is ignored. + * the JPEG buffer. If `*jpegBuf` points to a pre-allocated buffer, then + * `*jpegSize` should be set to the size of the buffer. Upon return, + * `*jpegSize` will contain the size of the JPEG image (in bytes.) If + * `*jpegBuf` points to a JPEG buffer that is being reused from a previous call + * to one of the JPEG compression functions, then `*jpegSize` is ignored. * * @param jpegSubsamp the level of chrominance subsampling to be used when * generating the JPEG image (see @ref TJSAMP @@ -739,7 +753,7 @@ DLLEXPORT tjhandle tjInitCompress(void); * * @return 0 if successful, or -1 if an error occurred (see #tjGetErrorStr2() * and #tjGetErrorCode().) -*/ + */ DLLEXPORT int tjCompress2(tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char **jpegBuf, unsigned long *jpegSize, @@ -747,55 +761,55 @@ DLLEXPORT int tjCompress2(tjhandle handle, const unsigned char *srcBuf, /** - * Compress a YUV planar image into a JPEG image. + * Compress a unified planar YUV image into a JPEG image. * * @param handle a handle to a TurboJPEG compressor or transformer instance * - * @param srcBuf pointer to an image buffer containing a YUV planar image to be - * compressed. The size of this buffer should match the value returned by - * #tjBufSizeYUV2() for the given image width, height, padding, and level of - * chrominance subsampling. The Y, U (Cb), and V (Cr) image planes should be - * stored sequentially in the source buffer (refer to @ref YUVnotes - * "YUV Image Format Notes".) + * @param srcBuf pointer to a buffer containing a unified planar YUV source + * image to be compressed. The size of this buffer should match the value + * returned by #tjBufSizeYUV2() for the given image width, height, row + * alignment, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) + * image planes should be stored sequentially in the buffer. (Refer to + * @ref YUVnotes "YUV Image Format Notes".) * * @param width width (in pixels) of the source image. If the width is not an * even multiple of the MCU block width (see #tjMCUWidth), then an intermediate - * buffer copy will be performed within TurboJPEG. + * buffer copy will be performed. * - * @param pad the line padding used in the source image. For instance, if each - * line in each plane of the YUV image is padded to the nearest multiple of 4 - * bytes, then pad should be set to 4. + * @param align row alignment (in bytes) of the source image (must be a power + * of 2.) Setting this parameter to n indicates that each row in each plane of + * the source image is padded to the nearest multiple of n bytes + * (1 = unpadded.) * * @param height height (in pixels) of the source image. If the height is not * an even multiple of the MCU block height (see #tjMCUHeight), then an - * intermediate buffer copy will be performed within TurboJPEG. + * intermediate buffer copy will be performed. * - * @param subsamp the level of chrominance subsampling used in the source - * image (see @ref TJSAMP "Chrominance subsampling options".) + * @param subsamp the level of chrominance subsampling used in the source image + * (see @ref TJSAMP "Chrominance subsampling options".) * - * @param jpegBuf address of a pointer to an image buffer that will receive the + * @param jpegBuf address of a pointer to a byte buffer that will receive the * JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to * accommodate the size of the JPEG image. Thus, you can choose to: * -# pre-allocate the JPEG buffer with an arbitrary size using #tjAlloc() and * let TurboJPEG grow the buffer as needed, - * -# set *jpegBuf to NULL to tell TurboJPEG to allocate the buffer - * for you, or + * -# set `*jpegBuf` to NULL to tell TurboJPEG to allocate the buffer for you, + * or * -# pre-allocate the buffer to a "worst case" size determined by calling * #tjBufSize(). This should ensure that the buffer never has to be - * re-allocated (setting #TJFLAG_NOREALLOC guarantees that it won't be.) + * re-allocated. (Setting #TJFLAG_NOREALLOC guarantees that it won't be.) * . - * If you choose option 1, *jpegSize should be set to the size of your + * If you choose option 1, then `*jpegSize` should be set to the size of your * pre-allocated buffer. In any case, unless you have set #TJFLAG_NOREALLOC, - * you should always check *jpegBuf upon return from this function, as - * it may have changed. + * you should always check `*jpegBuf` upon return from this function, as it may + * have changed. * * @param jpegSize pointer to an unsigned long variable that holds the size of - * the JPEG image buffer. If *jpegBuf points to a pre-allocated - * buffer, then *jpegSize should be set to the size of the buffer. - * Upon return, *jpegSize will contain the size of the JPEG image (in - * bytes.) If *jpegBuf points to a JPEG image buffer that is being - * reused from a previous call to one of the JPEG compression functions, then - * *jpegSize is ignored. + * the JPEG buffer. If `*jpegBuf` points to a pre-allocated buffer, then + * `*jpegSize` should be set to the size of the buffer. Upon return, + * `*jpegSize` will contain the size of the JPEG image (in bytes.) If + * `*jpegBuf` points to a JPEG buffer that is being reused from a previous call + * to one of the JPEG compression functions, then `*jpegSize` is ignored. * * @param jpegQual the image quality of the generated JPEG image (1 = worst, * 100 = best) @@ -805,9 +819,9 @@ DLLEXPORT int tjCompress2(tjhandle handle, const unsigned char *srcBuf, * * @return 0 if successful, or -1 if an error occurred (see #tjGetErrorStr2() * and #tjGetErrorCode().) -*/ + */ DLLEXPORT int tjCompressFromYUV(tjhandle handle, const unsigned char *srcBuf, - int width, int pad, int height, int subsamp, + int width, int align, int height, int subsamp, unsigned char **jpegBuf, unsigned long *jpegSize, int jpegQual, int flags); @@ -820,55 +834,54 @@ DLLEXPORT int tjCompressFromYUV(tjhandle handle, const unsigned char *srcBuf, * * @param srcPlanes an array of pointers to Y, U (Cb), and V (Cr) image planes * (or just a Y plane, if compressing a grayscale image) that contain a YUV - * image to be compressed. These planes can be contiguous or non-contiguous in - * memory. The size of each plane should match the value returned by - * #tjPlaneSizeYUV() for the given image width, height, strides, and level of - * chrominance subsampling. Refer to @ref YUVnotes "YUV Image Format Notes" - * for more details. + * source image to be compressed. These planes can be contiguous or + * non-contiguous in memory. The size of each plane should match the value + * returned by #tjPlaneSizeYUV() for the given image width, height, strides, + * and level of chrominance subsampling. Refer to @ref YUVnotes + * "YUV Image Format Notes" for more details. * * @param width width (in pixels) of the source image. If the width is not an * even multiple of the MCU block width (see #tjMCUWidth), then an intermediate - * buffer copy will be performed within TurboJPEG. + * buffer copy will be performed. * * @param strides an array of integers, each specifying the number of bytes per - * line in the corresponding plane of the YUV source image. Setting the stride + * row in the corresponding plane of the YUV source image. Setting the stride * for any plane to 0 is the same as setting it to the plane width (see - * @ref YUVnotes "YUV Image Format Notes".) If strides is NULL, then - * the strides for all planes will be set to their respective plane widths. - * You can adjust the strides in order to specify an arbitrary amount of line + * @ref YUVnotes "YUV Image Format Notes".) If `strides` is NULL, then the + * strides for all planes will be set to their respective plane widths. You + * can adjust the strides in order to specify an arbitrary amount of row * padding in each plane or to create a JPEG image from a subregion of a larger - * YUV planar image. + * planar YUV image. * * @param height height (in pixels) of the source image. If the height is not * an even multiple of the MCU block height (see #tjMCUHeight), then an - * intermediate buffer copy will be performed within TurboJPEG. + * intermediate buffer copy will be performed. * - * @param subsamp the level of chrominance subsampling used in the source - * image (see @ref TJSAMP "Chrominance subsampling options".) + * @param subsamp the level of chrominance subsampling used in the source image + * (see @ref TJSAMP "Chrominance subsampling options".) * - * @param jpegBuf address of a pointer to an image buffer that will receive the + * @param jpegBuf address of a pointer to a byte buffer that will receive the * JPEG image. TurboJPEG has the ability to reallocate the JPEG buffer to * accommodate the size of the JPEG image. Thus, you can choose to: * -# pre-allocate the JPEG buffer with an arbitrary size using #tjAlloc() and * let TurboJPEG grow the buffer as needed, - * -# set *jpegBuf to NULL to tell TurboJPEG to allocate the buffer - * for you, or + * -# set `*jpegBuf` to NULL to tell TurboJPEG to allocate the buffer for you, + * or * -# pre-allocate the buffer to a "worst case" size determined by calling * #tjBufSize(). This should ensure that the buffer never has to be - * re-allocated (setting #TJFLAG_NOREALLOC guarantees that it won't be.) + * re-allocated. (Setting #TJFLAG_NOREALLOC guarantees that it won't be.) * . - * If you choose option 1, *jpegSize should be set to the size of your + * If you choose option 1, then `*jpegSize` should be set to the size of your * pre-allocated buffer. In any case, unless you have set #TJFLAG_NOREALLOC, - * you should always check *jpegBuf upon return from this function, as - * it may have changed. + * you should always check `*jpegBuf` upon return from this function, as it may + * have changed. * * @param jpegSize pointer to an unsigned long variable that holds the size of - * the JPEG image buffer. If *jpegBuf points to a pre-allocated - * buffer, then *jpegSize should be set to the size of the buffer. - * Upon return, *jpegSize will contain the size of the JPEG image (in - * bytes.) If *jpegBuf points to a JPEG image buffer that is being - * reused from a previous call to one of the JPEG compression functions, then - * *jpegSize is ignored. + * the JPEG buffer. If `*jpegBuf` points to a pre-allocated buffer, then + * `*jpegSize` should be set to the size of the buffer. Upon return, + * `*jpegSize` will contain the size of the JPEG image (in bytes.) If + * `*jpegBuf` points to a JPEG buffer that is being reused from a previous call + * to one of the JPEG compression functions, then `*jpegSize` is ignored. * * @param jpegQual the image quality of the generated JPEG image (1 = worst, * 100 = best) @@ -878,7 +891,7 @@ DLLEXPORT int tjCompressFromYUV(tjhandle handle, const unsigned char *srcBuf, * * @return 0 if successful, or -1 if an error occurred (see #tjGetErrorStr2() * and #tjGetErrorCode().) -*/ + */ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, const unsigned char **srcPlanes, int width, const int *strides, @@ -892,11 +905,11 @@ DLLEXPORT int tjCompressFromYUVPlanes(tjhandle handle, * The maximum size of the buffer (in bytes) required to hold a JPEG image with * the given parameters. The number of bytes returned by this function is * larger than the size of the uncompressed source image. The reason for this - * is that the JPEG format uses 16-bit coefficients, and it is thus possible - * for a very high-quality JPEG image with very high-frequency content to - * expand rather than compress when converted to the JPEG format. Such images - * represent a very rare corner case, but since there is no way to predict the - * size of a JPEG image prior to compression, the corner case has to be + * is that the JPEG format uses 16-bit coefficients, so it is possible for a + * very high-quality source image with very high-frequency content to expand + * rather than compress when converted to the JPEG format. Such images + * represent very rare corner cases, but since there is no way to predict the + * size of a JPEG image prior to compression, the corner cases have to be * handled. * * @param width width (in pixels) of the image @@ -914,23 +927,24 @@ DLLEXPORT unsigned long tjBufSize(int width, int height, int jpegSubsamp); /** - * The size of the buffer (in bytes) required to hold a YUV planar image with - * the given parameters. + * The size of the buffer (in bytes) required to hold a unified planar YUV + * image with the given parameters. * * @param width width (in pixels) of the image * - * @param pad the width of each line in each plane of the image is padded to - * the nearest multiple of this number of bytes (must be a power of 2.) + * @param align row alignment (in bytes) of the image (must be a power of 2.) + * Setting this parameter to n specifies that each row in each plane of the + * image will be padded to the nearest multiple of n bytes (1 = unpadded.) * * @param height height (in pixels) of the image * * @param subsamp level of chrominance subsampling in the image (see * @ref TJSAMP "Chrominance subsampling options".) * - * @return the size of the buffer (in bytes) required to hold the image, or - * -1 if the arguments are out of bounds. + * @return the size of the buffer (in bytes) required to hold the image, or -1 + * if the arguments are out of bounds. */ -DLLEXPORT unsigned long tjBufSizeYUV2(int width, int pad, int height, +DLLEXPORT unsigned long tjBufSizeYUV2(int width, int align, int height, int subsamp); @@ -943,7 +957,7 @@ DLLEXPORT unsigned long tjBufSizeYUV2(int width, int pad, int height, * @param width width (in pixels) of the YUV image. NOTE: this is the width of * the whole image, not the plane width. * - * @param stride bytes per line in the image plane. Setting this to 0 is the + * @param stride bytes per row in the image plane. Setting this to 0 is the * equivalent of setting it to the plane width. * * @param height height (in pixels) of the YUV image. NOTE: this is the height @@ -994,23 +1008,23 @@ DLLEXPORT int tjPlaneHeight(int componentID, int height, int subsamp); /** - * Encode an RGB or grayscale image into a YUV planar image. This function - * uses the accelerated color conversion routines in the underlying - * codec but does not execute any of the other steps in the JPEG compression - * process. + * Encode a packed-pixel RGB or grayscale image into a unified planar YUV + * image. This function performs color conversion (which is accelerated in the + * libjpeg-turbo implementation) but does not execute any of the other steps in + * the JPEG compression process. * * @param handle a handle to a TurboJPEG compressor or transformer instance * - * @param srcBuf pointer to an image buffer containing RGB or grayscale pixels - * to be encoded + * @param srcBuf pointer to a buffer containing a packed-pixel RGB or grayscale + * source image to be encoded * * @param width width (in pixels) of the source image * - * @param pitch bytes per line in the source image. Normally, this should be - * width * #tjPixelSize[pixelFormat] if the image is unpadded, or - * #TJPAD(width * #tjPixelSize[pixelFormat]) if each line of the image - * is padded to the nearest 32-bit boundary, as is the case for Windows - * bitmaps. You can also be clever and use this parameter to skip lines, etc. + * @param pitch bytes per row in the source image. Normally this should be + * width * #tjPixelSize[pixelFormat], if the image is unpadded, or + * #TJPAD(width * #tjPixelSize[pixelFormat]) if each row of the image + * is padded to the nearest multiple of 4 bytes, as is the case for Windows + * bitmaps. You can also be clever and use this parameter to skip rows, etc. * Setting this parameter to 0 is the equivalent of setting it to * width * #tjPixelSize[pixelFormat]. * @@ -1019,53 +1033,54 @@ DLLEXPORT int tjPlaneHeight(int componentID, int height, int subsamp); * @param pixelFormat pixel format of the source image (see @ref TJPF * "Pixel formats".) * - * @param dstBuf pointer to an image buffer that will receive the YUV image. - * Use #tjBufSizeYUV2() to determine the appropriate size for this buffer based - * on the image width, height, padding, and level of chrominance subsampling. - * The Y, U (Cb), and V (Cr) image planes will be stored sequentially in the - * buffer (refer to @ref YUVnotes "YUV Image Format Notes".) + * @param dstBuf pointer to a buffer that will receive the unified planar YUV + * image. Use #tjBufSizeYUV2() to determine the appropriate size for this + * buffer based on the image width, height, row alignment, and level of + * chrominance subsampling. The Y, U (Cb), and V (Cr) image planes will be + * stored sequentially in the buffer. (Refer to @ref YUVnotes + * "YUV Image Format Notes".) * - * @param pad the width of each line in each plane of the YUV image will be - * padded to the nearest multiple of this number of bytes (must be a power of - * 2.) To generate images suitable for X Video, pad should be set to - * 4. + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n will cause each row in each plane of the + * YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.) + * To generate images suitable for X Video, `align` should be set to 4. * * @param subsamp the level of chrominance subsampling to be used when * generating the YUV image (see @ref TJSAMP * "Chrominance subsampling options".) To generate images suitable for X - * Video, subsamp should be set to @ref TJSAMP_420. This produces an - * image compatible with the I420 (AKA "YUV420P") format. + * Video, `subsamp` should be set to @ref TJSAMP_420. This produces an image + * compatible with the I420 (AKA "YUV420P") format. * * @param flags the bitwise OR of one or more of the @ref TJFLAG_ACCURATEDCT * "flags" * * @return 0 if successful, or -1 if an error occurred (see #tjGetErrorStr2() * and #tjGetErrorCode().) -*/ + */ DLLEXPORT int tjEncodeYUV3(tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, - unsigned char *dstBuf, int pad, int subsamp, + unsigned char *dstBuf, int align, int subsamp, int flags); /** - * Encode an RGB or grayscale image into separate Y, U (Cb), and V (Cr) image - * planes. This function uses the accelerated color conversion routines in the - * underlying codec but does not execute any of the other steps in the JPEG - * compression process. + * Encode a packed-pixel RGB or grayscale image into separate Y, U (Cb), and + * V (Cr) image planes. This function performs color conversion (which is + * accelerated in the libjpeg-turbo implementation) but does not execute any of + * the other steps in the JPEG compression process. * * @param handle a handle to a TurboJPEG compressor or transformer instance * - * @param srcBuf pointer to an image buffer containing RGB or grayscale pixels - * to be encoded + * @param srcBuf pointer to a buffer containing a packed-pixel RGB or grayscale + * source image to be encoded * * @param width width (in pixels) of the source image * - * @param pitch bytes per line in the source image. Normally, this should be - * width * #tjPixelSize[pixelFormat] if the image is unpadded, or - * #TJPAD(width * #tjPixelSize[pixelFormat]) if each line of the image - * is padded to the nearest 32-bit boundary, as is the case for Windows - * bitmaps. You can also be clever and use this parameter to skip lines, etc. + * @param pitch bytes per row in the source image. Normally this should be + * width * #tjPixelSize[pixelFormat], if the image is unpadded, or + * #TJPAD(width * #tjPixelSize[pixelFormat]) if each row of the image + * is padded to the nearest multiple of 4 bytes, as is the case for Windows + * bitmaps. You can also be clever and use this parameter to skip rows, etc. * Setting this parameter to 0 is the equivalent of setting it to * width * #tjPixelSize[pixelFormat]. * @@ -1082,26 +1097,26 @@ DLLEXPORT int tjEncodeYUV3(tjhandle handle, const unsigned char *srcBuf, * Refer to @ref YUVnotes "YUV Image Format Notes" for more details. * * @param strides an array of integers, each specifying the number of bytes per - * line in the corresponding plane of the output image. Setting the stride for - * any plane to 0 is the same as setting it to the plane width (see - * @ref YUVnotes "YUV Image Format Notes".) If strides is NULL, then - * the strides for all planes will be set to their respective plane widths. - * You can adjust the strides in order to add an arbitrary amount of line - * padding to each plane or to encode an RGB or grayscale image into a - * subregion of a larger YUV planar image. + * row in the corresponding plane of the YUV image. Setting the stride for any + * plane to 0 is the same as setting it to the plane width (see @ref YUVnotes + * "YUV Image Format Notes".) If `strides` is NULL, then the strides for all + * planes will be set to their respective plane widths. You can adjust the + * strides in order to add an arbitrary amount of row padding to each plane or + * to encode an RGB or grayscale image into a subregion of a larger planar YUV + * image. * * @param subsamp the level of chrominance subsampling to be used when * generating the YUV image (see @ref TJSAMP * "Chrominance subsampling options".) To generate images suitable for X - * Video, subsamp should be set to @ref TJSAMP_420. This produces an - * image compatible with the I420 (AKA "YUV420P") format. + * Video, `subsamp` should be set to @ref TJSAMP_420. This produces an image + * compatible with the I420 (AKA "YUV420P") format. * * @param flags the bitwise OR of one or more of the @ref TJFLAG_ACCURATEDCT * "flags" * * @return 0 if successful, or -1 if an error occurred (see #tjGetErrorStr2() * and #tjGetErrorCode().) -*/ + */ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat, unsigned char **dstPlanes, @@ -1111,38 +1126,49 @@ DLLEXPORT int tjEncodeYUVPlanes(tjhandle handle, const unsigned char *srcBuf, /** * Create a TurboJPEG decompressor instance. * - * @return a handle to the newly-created instance, or NULL if an error - * occurred (see #tjGetErrorStr2().) -*/ + * @return a handle to the newly-created instance, or NULL if an error occurred + * (see #tjGetErrorStr2().) + */ DLLEXPORT tjhandle tjInitDecompress(void); /** - * Retrieve information about a JPEG image without decompressing it. + * Retrieve information about a JPEG image without decompressing it, or prime + * the decompressor with quantization and Huffman tables. * * @param handle a handle to a TurboJPEG decompressor or transformer instance * - * @param jpegBuf pointer to a buffer containing a JPEG image + * @param jpegBuf pointer to a byte buffer containing a JPEG image or an + * "abbreviated table specification" (AKA "tables-only") datastream. Passing a + * tables-only datastream to this function primes the decompressor with + * quantization and Huffman tables that can be used when decompressing + * subsequent "abbreviated image" datastreams. This is useful, for instance, + * when decompressing video streams in which all frames share the same + * quantization and Huffman tables. * - * @param jpegSize size of the JPEG image (in bytes) + * @param jpegSize size of the JPEG image or tables-only datastream (in bytes) * * @param width pointer to an integer variable that will receive the width (in - * pixels) of the JPEG image + * pixels) of the JPEG image. If `jpegBuf` points to a tables-only datastream, + * then `width` is ignored. * * @param height pointer to an integer variable that will receive the height - * (in pixels) of the JPEG image + * (in pixels) of the JPEG image. If `jpegBuf` points to a tables-only + * datastream, then `height` is ignored. * * @param jpegSubsamp pointer to an integer variable that will receive the * level of chrominance subsampling used when the JPEG image was compressed - * (see @ref TJSAMP "Chrominance subsampling options".) + * (see @ref TJSAMP "Chrominance subsampling options".) If `jpegBuf` points to + * a tables-only datastream, then `jpegSubsamp` is ignored. * * @param jpegColorspace pointer to an integer variable that will receive one * of the JPEG colorspace constants, indicating the colorspace of the JPEG - * image (see @ref TJCS "JPEG colorspaces".) + * image (see @ref TJCS "JPEG colorspaces".) If `jpegBuf` points to a + * tables-only datastream, then `jpegColorspace` is ignored. * * @return 0 if successful, or -1 if an error occurred (see #tjGetErrorStr2() * and #tjGetErrorCode().) -*/ + */ DLLEXPORT int tjDecompressHeader3(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, int *width, @@ -1151,58 +1177,60 @@ DLLEXPORT int tjDecompressHeader3(tjhandle handle, /** - * Returns a list of fractional scaling factors that the JPEG decompressor in - * this implementation of TurboJPEG supports. + * Returns a list of fractional scaling factors that the JPEG decompressor + * supports. * - * @param numscalingfactors pointer to an integer variable that will receive + * @param numScalingFactors pointer to an integer variable that will receive * the number of elements in the list * * @return a pointer to a list of fractional scaling factors, or NULL if an * error is encountered (see #tjGetErrorStr2().) -*/ -DLLEXPORT tjscalingfactor *tjGetScalingFactors(int *numscalingfactors); + */ +DLLEXPORT tjscalingfactor *tjGetScalingFactors(int *numScalingFactors); /** - * Decompress a JPEG image to an RGB, grayscale, or CMYK image. + * Decompress a JPEG image into a packed-pixel RGB, grayscale, or CMYK image. * * @param handle a handle to a TurboJPEG decompressor or transformer instance * - * @param jpegBuf pointer to a buffer containing the JPEG image to decompress + * @param jpegBuf pointer to a byte buffer containing the JPEG image to + * decompress * * @param jpegSize size of the JPEG image (in bytes) * - * @param dstBuf pointer to an image buffer that will receive the decompressed - * image. This buffer should normally be pitch * scaledHeight bytes - * in size, where scaledHeight can be determined by calling - * #TJSCALED() with the JPEG image height and one of the scaling factors - * returned by #tjGetScalingFactors(). The dstBuf pointer may also be - * used to decompress into a specific region of a larger buffer. + * @param dstBuf pointer to a buffer that will receive the packed-pixel + * decompressed image. This buffer should normally be `pitch * scaledHeight` + * bytes in size, where `scaledHeight` can be determined by calling #TJSCALED() + * with the JPEG image height and one of the scaling factors returned by + * #tjGetScalingFactors(). The `dstBuf` pointer may also be used to decompress + * into a specific region of a larger buffer. * * @param width desired width (in pixels) of the destination image. If this is * different than the width of the JPEG image being decompressed, then * TurboJPEG will use scaling in the JPEG decompressor to generate the largest - * possible image that will fit within the desired width. If width is - * set to 0, then only the height will be considered when determining the - * scaled image size. - * - * @param pitch bytes per line in the destination image. Normally, this is - * scaledWidth * #tjPixelSize[pixelFormat] if the decompressed image - * is unpadded, else #TJPAD(scaledWidth * #tjPixelSize[pixelFormat]) - * if each line of the decompressed image is padded to the nearest 32-bit - * boundary, as is the case for Windows bitmaps. (NOTE: scaledWidth - * can be determined by calling #TJSCALED() with the JPEG image width and one - * of the scaling factors returned by #tjGetScalingFactors().) You can also be - * clever and use the pitch parameter to skip lines, etc. Setting this - * parameter to 0 is the equivalent of setting it to + * possible image that will fit within the desired width. If `width` is set to + * 0, then only the height will be considered when determining the scaled image + * size. + * + * @param pitch bytes per row in the destination image. Normally this should + * be set to scaledWidth * #tjPixelSize[pixelFormat], if the + * destination image should be unpadded, or + * #TJPAD(scaledWidth * #tjPixelSize[pixelFormat]) if each row of the + * destination image should be padded to the nearest multiple of 4 bytes, as is + * the case for Windows bitmaps. (NOTE: `scaledWidth` can be determined by + * calling #TJSCALED() with the JPEG image width and one of the scaling factors + * returned by #tjGetScalingFactors().) You can also be clever and use the + * pitch parameter to skip rows, etc. Setting this parameter to 0 is the + * equivalent of setting it to * scaledWidth * #tjPixelSize[pixelFormat]. * * @param height desired height (in pixels) of the destination image. If this * is different than the height of the JPEG image being decompressed, then * TurboJPEG will use scaling in the JPEG decompressor to generate the largest - * possible image that will fit within the desired height. If height - * is set to 0, then only the width will be considered when determining the - * scaled image size. + * possible image that will fit within the desired height. If `height` is set + * to 0, then only the width will be considered when determining the scaled + * image size. * * @param pixelFormat pixel format of the destination image (see @ref * TJPF "Pixel formats".) @@ -1220,44 +1248,45 @@ DLLEXPORT int tjDecompress2(tjhandle handle, const unsigned char *jpegBuf, /** - * Decompress a JPEG image to a YUV planar image. This function performs JPEG - * decompression but leaves out the color conversion step, so a planar YUV - * image is generated instead of an RGB image. + * Decompress a JPEG image into a unified planar YUV image. This function + * performs JPEG decompression but leaves out the color conversion step, so a + * planar YUV image is generated instead of a packed-pixel image. * * @param handle a handle to a TurboJPEG decompressor or transformer instance * - * @param jpegBuf pointer to a buffer containing the JPEG image to decompress + * @param jpegBuf pointer to a byte buffer containing the JPEG image to + * decompress * * @param jpegSize size of the JPEG image (in bytes) * - * @param dstBuf pointer to an image buffer that will receive the YUV image. - * Use #tjBufSizeYUV2() to determine the appropriate size for this buffer based - * on the image width, height, padding, and level of subsampling. The Y, - * U (Cb), and V (Cr) image planes will be stored sequentially in the buffer - * (refer to @ref YUVnotes "YUV Image Format Notes".) + * @param dstBuf pointer to a buffer that will receive the unified planar YUV + * decompressed image. Use #tjBufSizeYUV2() to determine the appropriate size + * for this buffer based on the scaled image width, scaled image height, row + * alignment, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) + * image planes will be stored sequentially in the buffer. (Refer to + * @ref YUVnotes "YUV Image Format Notes".) * * @param width desired width (in pixels) of the YUV image. If this is * different than the width of the JPEG image being decompressed, then * TurboJPEG will use scaling in the JPEG decompressor to generate the largest - * possible image that will fit within the desired width. If width is - * set to 0, then only the height will be considered when determining the - * scaled image size. If the scaled width is not an even multiple of the MCU - * block width (see #tjMCUWidth), then an intermediate buffer copy will be - * performed within TurboJPEG. + * possible image that will fit within the desired width. If `width` is set to + * 0, then only the height will be considered when determining the scaled image + * size. If the scaled width is not an even multiple of the MCU block width + * (see #tjMCUWidth), then an intermediate buffer copy will be performed. * - * @param pad the width of each line in each plane of the YUV image will be - * padded to the nearest multiple of this number of bytes (must be a power of - * 2.) To generate images suitable for X Video, pad should be set to - * 4. + * @param align row alignment (in bytes) of the YUV image (must be a power of + * 2.) Setting this parameter to n will cause each row in each plane of the + * YUV image to be padded to the nearest multiple of n bytes (1 = unpadded.) + * To generate images suitable for X Video, `align` should be set to 4. * * @param height desired height (in pixels) of the YUV image. If this is * different than the height of the JPEG image being decompressed, then * TurboJPEG will use scaling in the JPEG decompressor to generate the largest - * possible image that will fit within the desired height. If height - * is set to 0, then only the width will be considered when determining the - * scaled image size. If the scaled height is not an even multiple of the MCU - * block height (see #tjMCUHeight), then an intermediate buffer copy will be - * performed within TurboJPEG. + * possible image that will fit within the desired height. If `height` is set + * to 0, then only the width will be considered when determining the scaled + * image size. If the scaled height is not an even multiple of the MCU block + * height (see #tjMCUHeight), then an intermediate buffer copy will be + * performed. * * @param flags the bitwise OR of one or more of the @ref TJFLAG_ACCURATEDCT * "flags" @@ -1267,54 +1296,55 @@ DLLEXPORT int tjDecompress2(tjhandle handle, const unsigned char *jpegBuf, */ DLLEXPORT int tjDecompressToYUV2(tjhandle handle, const unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, - int width, int pad, int height, int flags); + int width, int align, int height, int flags); /** * Decompress a JPEG image into separate Y, U (Cb), and V (Cr) image * planes. This function performs JPEG decompression but leaves out the color - * conversion step, so a planar YUV image is generated instead of an RGB image. + * conversion step, so a planar YUV image is generated instead of a + * packed-pixel image. * * @param handle a handle to a TurboJPEG decompressor or transformer instance * - * @param jpegBuf pointer to a buffer containing the JPEG image to decompress + * @param jpegBuf pointer to a byte buffer containing the JPEG image to + * decompress * * @param jpegSize size of the JPEG image (in bytes) * * @param dstPlanes an array of pointers to Y, U (Cb), and V (Cr) image planes * (or just a Y plane, if decompressing a grayscale image) that will receive - * the YUV image. These planes can be contiguous or non-contiguous in memory. - * Use #tjPlaneSizeYUV() to determine the appropriate size for each plane based - * on the scaled image width, scaled image height, strides, and level of - * chrominance subsampling. Refer to @ref YUVnotes "YUV Image Format Notes" - * for more details. + * the decompressed image. These planes can be contiguous or non-contiguous in + * memory. Use #tjPlaneSizeYUV() to determine the appropriate size for each + * plane based on the scaled image width, scaled image height, strides, and + * level of chrominance subsampling. Refer to @ref YUVnotes + * "YUV Image Format Notes" for more details. * * @param width desired width (in pixels) of the YUV image. If this is * different than the width of the JPEG image being decompressed, then * TurboJPEG will use scaling in the JPEG decompressor to generate the largest - * possible image that will fit within the desired width. If width is - * set to 0, then only the height will be considered when determining the - * scaled image size. If the scaled width is not an even multiple of the MCU - * block width (see #tjMCUWidth), then an intermediate buffer copy will be - * performed within TurboJPEG. + * possible image that will fit within the desired width. If `width` is set to + * 0, then only the height will be considered when determining the scaled image + * size. If the scaled width is not an even multiple of the MCU block width + * (see #tjMCUWidth), then an intermediate buffer copy will be performed. * * @param strides an array of integers, each specifying the number of bytes per - * line in the corresponding plane of the output image. Setting the stride for - * any plane to 0 is the same as setting it to the scaled plane width (see - * @ref YUVnotes "YUV Image Format Notes".) If strides is NULL, then - * the strides for all planes will be set to their respective scaled plane - * widths. You can adjust the strides in order to add an arbitrary amount of - * line padding to each plane or to decompress the JPEG image into a subregion - * of a larger YUV planar image. + * row in the corresponding plane of the YUV image. Setting the stride for any + * plane to 0 is the same as setting it to the scaled plane width (see + * @ref YUVnotes "YUV Image Format Notes".) If `strides` is NULL, then the + * strides for all planes will be set to their respective scaled plane widths. + * You can adjust the strides in order to add an arbitrary amount of row + * padding to each plane or to decompress the JPEG image into a subregion of a + * larger planar YUV image. * * @param height desired height (in pixels) of the YUV image. If this is * different than the height of the JPEG image being decompressed, then * TurboJPEG will use scaling in the JPEG decompressor to generate the largest - * possible image that will fit within the desired height. If height - * is set to 0, then only the width will be considered when determining the - * scaled image size. If the scaled height is not an even multiple of the MCU - * block height (see #tjMCUHeight), then an intermediate buffer copy will be - * performed within TurboJPEG. + * possible image that will fit within the desired height. If `height` is set + * to 0, then only the width will be considered when determining the scaled + * image size. If the scaled height is not an even multiple of the MCU block + * height (see #tjMCUHeight), then an intermediate buffer copy will be + * performed. * * @param flags the bitwise OR of one or more of the @ref TJFLAG_ACCURATEDCT * "flags" @@ -1330,40 +1360,42 @@ DLLEXPORT int tjDecompressToYUVPlanes(tjhandle handle, /** - * Decode a YUV planar image into an RGB or grayscale image. This function - * uses the accelerated color conversion routines in the underlying - * codec but does not execute any of the other steps in the JPEG decompression - * process. + * Decode a unified planar YUV image into a packed-pixel RGB or grayscale + * image. This function performs color conversion (which is accelerated in the + * libjpeg-turbo implementation) but does not execute any of the other steps in + * the JPEG decompression process. * * @param handle a handle to a TurboJPEG decompressor or transformer instance * - * @param srcBuf pointer to an image buffer containing a YUV planar image to be - * decoded. The size of this buffer should match the value returned by - * #tjBufSizeYUV2() for the given image width, height, padding, and level of - * chrominance subsampling. The Y, U (Cb), and V (Cr) image planes should be - * stored sequentially in the source buffer (refer to @ref YUVnotes - * "YUV Image Format Notes".) + * @param srcBuf pointer to a buffer containing a unified planar YUV source + * image to be decoded. The size of this buffer should match the value + * returned by #tjBufSizeYUV2() for the given image width, height, row + * alignment, and level of chrominance subsampling. The Y, U (Cb), and V (Cr) + * image planes should be stored sequentially in the source buffer. (Refer to + * @ref YUVnotes "YUV Image Format Notes".) * - * @param pad Use this parameter to specify that the width of each line in each - * plane of the YUV source image is padded to the nearest multiple of this - * number of bytes (must be a power of 2.) + * @param align row alignment (in bytes) of the YUV source image (must be a + * power of 2.) Setting this parameter to n indicates that each row in each + * plane of the YUV source image is padded to the nearest multiple of n bytes + * (1 = unpadded.) * * @param subsamp the level of chrominance subsampling used in the YUV source * image (see @ref TJSAMP "Chrominance subsampling options".) * - * @param dstBuf pointer to an image buffer that will receive the decoded - * image. This buffer should normally be pitch * height bytes in - * size, but the dstBuf pointer can also be used to decode into a - * specific region of a larger buffer. + * @param dstBuf pointer to a buffer that will receive the packed-pixel decoded + * image. This buffer should normally be `pitch * height` bytes in size, but + * the `dstBuf` pointer can also be used to decode into a specific region of a + * larger buffer. * * @param width width (in pixels) of the source and destination images * - * @param pitch bytes per line in the destination image. Normally, this should - * be width * #tjPixelSize[pixelFormat] if the destination image is - * unpadded, or #TJPAD(width * #tjPixelSize[pixelFormat]) if each line - * of the destination image should be padded to the nearest 32-bit boundary, as - * is the case for Windows bitmaps. You can also be clever and use the pitch - * parameter to skip lines, etc. Setting this parameter to 0 is the equivalent + * @param pitch bytes per row in the destination image. Normally this should + * be set to width * #tjPixelSize[pixelFormat], if the destination + * image should be unpadded, or + * #TJPAD(width * #tjPixelSize[pixelFormat]) if each row of the + * destination image should be padded to the nearest multiple of 4 bytes, as is + * the case for Windows bitmaps. You can also be clever and use the pitch + * parameter to skip rows, etc. Setting this parameter to 0 is the equivalent * of setting it to width * #tjPixelSize[pixelFormat]. * * @param height height (in pixels) of the source and destination images @@ -1378,16 +1410,16 @@ DLLEXPORT int tjDecompressToYUVPlanes(tjhandle handle, * and #tjGetErrorCode().) */ DLLEXPORT int tjDecodeYUV(tjhandle handle, const unsigned char *srcBuf, - int pad, int subsamp, unsigned char *dstBuf, + int align, int subsamp, unsigned char *dstBuf, int width, int pitch, int height, int pixelFormat, int flags); /** - * Decode a set of Y, U (Cb), and V (Cr) image planes into an RGB or grayscale - * image. This function uses the accelerated color conversion routines in the - * underlying codec but does not execute any of the other steps in the JPEG - * decompression process. + * Decode a set of Y, U (Cb), and V (Cr) image planes into a packed-pixel RGB + * or grayscale image. This function performs color conversion (which is + * accelerated in the libjpeg-turbo implementation) but does not execute any of + * the other steps in the JPEG decompression process. * * @param handle a handle to a TurboJPEG decompressor or transformer instance * @@ -1400,29 +1432,30 @@ DLLEXPORT int tjDecodeYUV(tjhandle handle, const unsigned char *srcBuf, * details. * * @param strides an array of integers, each specifying the number of bytes per - * line in the corresponding plane of the YUV source image. Setting the stride + * row in the corresponding plane of the YUV source image. Setting the stride * for any plane to 0 is the same as setting it to the plane width (see - * @ref YUVnotes "YUV Image Format Notes".) If strides is NULL, then - * the strides for all planes will be set to their respective plane widths. - * You can adjust the strides in order to specify an arbitrary amount of line - * padding in each plane or to decode a subregion of a larger YUV planar image. + * @ref YUVnotes "YUV Image Format Notes".) If `strides` is NULL, then the + * strides for all planes will be set to their respective plane widths. You + * can adjust the strides in order to specify an arbitrary amount of row + * padding in each plane or to decode a subregion of a larger planar YUV image. * * @param subsamp the level of chrominance subsampling used in the YUV source * image (see @ref TJSAMP "Chrominance subsampling options".) * - * @param dstBuf pointer to an image buffer that will receive the decoded - * image. This buffer should normally be pitch * height bytes in - * size, but the dstBuf pointer can also be used to decode into a - * specific region of a larger buffer. + * @param dstBuf pointer to a buffer that will receive the packed-pixel decoded + * image. This buffer should normally be `pitch * height` bytes in size, but + * the `dstBuf` pointer can also be used to decode into a specific region of a + * larger buffer. * * @param width width (in pixels) of the source and destination images * - * @param pitch bytes per line in the destination image. Normally, this should - * be width * #tjPixelSize[pixelFormat] if the destination image is - * unpadded, or #TJPAD(width * #tjPixelSize[pixelFormat]) if each line - * of the destination image should be padded to the nearest 32-bit boundary, as - * is the case for Windows bitmaps. You can also be clever and use the pitch - * parameter to skip lines, etc. Setting this parameter to 0 is the equivalent + * @param pitch bytes per row in the destination image. Normally this should + * be set to width * #tjPixelSize[pixelFormat], if the destination + * image should be unpadded, or + * #TJPAD(width * #tjPixelSize[pixelFormat]) if each row of the + * destination image should be padded to the nearest multiple of 4 bytes, as is + * the case for Windows bitmaps. You can also be clever and use the pitch + * parameter to skip rows, etc. Setting this parameter to 0 is the equivalent * of setting it to width * #tjPixelSize[pixelFormat]. * * @param height height (in pixels) of the source and destination images @@ -1461,50 +1494,52 @@ DLLEXPORT tjhandle tjInitTransform(void); * transform requires reading and performing Huffman decoding on all of the * coefficients in the source image, regardless of the size of the destination * image. Thus, this function provides a means of generating multiple - * transformed images from the same source or applying multiple - * transformations simultaneously, in order to eliminate the need to read the - * source coefficients multiple times. + * transformed images from the same source or applying multiple transformations + * simultaneously, in order to eliminate the need to read the source + * coefficients multiple times. * * @param handle a handle to a TurboJPEG transformer instance * - * @param jpegBuf pointer to a buffer containing the JPEG source image to + * @param jpegBuf pointer to a byte buffer containing the JPEG source image to * transform * * @param jpegSize size of the JPEG source image (in bytes) * * @param n the number of transformed JPEG images to generate * - * @param dstBufs pointer to an array of n image buffers. dstBufs[i] - * will receive a JPEG image that has been transformed using the parameters in - * transforms[i]. TurboJPEG has the ability to reallocate the JPEG - * buffer to accommodate the size of the JPEG image. Thus, you can choose to: - * -# pre-allocate the JPEG buffer with an arbitrary size using #tjAlloc() and - * let TurboJPEG grow the buffer as needed, - * -# set dstBufs[i] to NULL to tell TurboJPEG to allocate the buffer - * for you, or + * @param dstBufs pointer to an array of n byte buffers. `dstBufs[i]` will + * receive a JPEG image that has been transformed using the parameters in + * `transforms[i]`. TurboJPEG has the ability to reallocate the JPEG + * destination buffer to accommodate the size of the transformed JPEG image. + * Thus, you can choose to: + * -# pre-allocate the JPEG destination buffer with an arbitrary size using + * #tjAlloc() and let TurboJPEG grow the buffer as needed, + * -# set `dstBufs[i]` to NULL to tell TurboJPEG to allocate the buffer for + * you, or * -# pre-allocate the buffer to a "worst case" size determined by calling - * #tjBufSize() with the transformed or cropped width and height. Under normal - * circumstances, this should ensure that the buffer never has to be - * re-allocated (setting #TJFLAG_NOREALLOC guarantees that it won't be.) Note, - * however, that there are some rare cases (such as transforming images with a - * large amount of embedded EXIF or ICC profile data) in which the output image - * will be larger than the worst-case size, and #TJFLAG_NOREALLOC cannot be - * used in those cases. + * #tjBufSize() with the transformed or cropped width and height and the level + * of subsampling used in the source image. Under normal circumstances, this + * should ensure that the buffer never has to be re-allocated. (Setting + * #TJFLAG_NOREALLOC guarantees that it won't be.) Note, however, that there + * are some rare cases (such as transforming images with a large amount of + * embedded EXIF or ICC profile data) in which the transformed JPEG image will + * be larger than the worst-case size, and #TJFLAG_NOREALLOC cannot be used in + * those cases. * . - * If you choose option 1, dstSizes[i] should be set to the size of - * your pre-allocated buffer. In any case, unless you have set - * #TJFLAG_NOREALLOC, you should always check dstBufs[i] upon return - * from this function, as it may have changed. + * If you choose option 1, then `dstSizes[i]` should be set to the size of your + * pre-allocated buffer. In any case, unless you have set #TJFLAG_NOREALLOC, + * you should always check `dstBufs[i]` upon return from this function, as it + * may have changed. * * @param dstSizes pointer to an array of n unsigned long variables that will * receive the actual sizes (in bytes) of each transformed JPEG image. If - * dstBufs[i] points to a pre-allocated buffer, then - * dstSizes[i] should be set to the size of the buffer. Upon return, - * dstSizes[i] will contain the size of the JPEG image (in bytes.) + * `dstBufs[i]` points to a pre-allocated buffer, then `dstSizes[i]` should be + * set to the size of the buffer. Upon return, `dstSizes[i]` will contain the + * size of the transformed JPEG image (in bytes.) * * @param transforms pointer to an array of n #tjtransform structures, each of * which specifies the transform parameters and/or cropping region for the - * corresponding transformed output image. + * corresponding transformed JPEG image. * * @param flags the bitwise OR of one or more of the @ref TJFLAG_ACCURATEDCT * "flags" @@ -1530,10 +1565,10 @@ DLLEXPORT int tjDestroy(tjhandle handle); /** - * Allocate an image buffer for use with TurboJPEG. You should always use - * this function to allocate the JPEG destination buffer(s) for the compression - * and transform functions unless you are disabling automatic buffer - * (re)allocation (by setting #TJFLAG_NOREALLOC.) + * Allocate a byte buffer for use with TurboJPEG. You should always use this + * function to allocate the JPEG destination buffer(s) for the compression and + * transform functions unless you are disabling automatic buffer (re)allocation + * (by setting #TJFLAG_NOREALLOC.) * * @param bytes the number of bytes to allocate * @@ -1546,44 +1581,43 @@ DLLEXPORT unsigned char *tjAlloc(int bytes); /** - * Load an uncompressed image from disk into memory. + * Load a packed-pixel image from disk into memory. * - * @param filename name of a file containing an uncompressed image in Windows + * @param filename name of a file containing a packed-pixel image in Windows * BMP or PBMPLUS (PPM/PGM) format * * @param width pointer to an integer variable that will receive the width (in - * pixels) of the uncompressed image + * pixels) of the packed-pixel image * - * @param align row alignment of the image buffer to be returned (must be a - * power of 2.) For instance, setting this parameter to 4 will cause all rows - * in the image buffer to be padded to the nearest 32-bit boundary, and setting - * this parameter to 1 will cause all rows in the image buffer to be unpadded. + * @param align row alignment of the packed-pixel buffer to be returned (must + * be a power of 2.) Setting this parameter to n will cause all rows in the + * buffer to be padded to the nearest multiple of n bytes (1 = unpadded.) * * @param height pointer to an integer variable that will receive the height - * (in pixels) of the uncompressed image + * (in pixels) of the packed-pixel image * * @param pixelFormat pointer to an integer variable that specifies or will - * receive the pixel format of the uncompressed image buffer. The behavior of - * #tjLoadImage() will vary depending on the value of *pixelFormat - * passed to the function: - * - @ref TJPF_UNKNOWN : The uncompressed image buffer returned by the function - * will use the most optimal pixel format for the file type, and - * *pixelFormat will contain the ID of this pixel format upon - * successful return from the function. - * - @ref TJPF_GRAY : Only PGM files and 8-bit BMP files with a grayscale - * colormap can be loaded. + * receive the pixel format of the packed-pixel buffer. The behavior of + * #tjLoadImage() will vary depending on the value of `*pixelFormat` passed to + * the function: + * - @ref TJPF_UNKNOWN : The packed-pixel buffer returned by this function will + * use the most optimal pixel format for the file type, and `*pixelFormat` will + * contain the ID of that pixel format upon successful return from this + * function. + * - @ref TJPF_GRAY : Only PGM files and 8-bit-per-pixel BMP files with a + * grayscale colormap can be loaded. * - @ref TJPF_CMYK : The RGB or grayscale pixels stored in the file will be * converted using a quick & dirty algorithm that is suitable only for testing - * purposes (proper conversion between CMYK and other formats requires a color - * management system.) - * - Other @ref TJPF "pixel formats" : The uncompressed image buffer will use - * the specified pixel format, and pixel format conversion will be performed if + * purposes. (Proper conversion between CMYK and other formats requires a + * color management system.) + * - Other @ref TJPF "pixel formats" : The packed-pixel buffer will use the + * specified pixel format, and pixel format conversion will be performed if * necessary. * * @param flags the bitwise OR of one or more of the @ref TJFLAG_BOTTOMUP * "flags". * - * @return a pointer to a newly-allocated buffer containing the uncompressed + * @return a pointer to a newly-allocated buffer containing the packed-pixel * image, converted to the chosen pixel format and with the chosen row * alignment, or NULL if an error occurred (see #tjGetErrorStr2().) This * buffer should be freed using #tjFree(). @@ -1594,31 +1628,31 @@ DLLEXPORT unsigned char *tjLoadImage(const char *filename, int *width, /** - * Save an uncompressed image from memory to disk. + * Save a packed-pixel image from memory to disk. * - * @param filename name of a file to which to save the uncompressed image. - * The image will be stored in Windows BMP or PBMPLUS (PPM/PGM) format, - * depending on the file extension. + * @param filename name of a file to which to save the packed-pixel image. The + * image will be stored in Windows BMP or PBMPLUS (PPM/PGM) format, depending + * on the file extension. * - * @param buffer pointer to an image buffer containing RGB, grayscale, or - * CMYK pixels to be saved + * @param buffer pointer to a buffer containing a packed-pixel RGB, grayscale, + * or CMYK image to be saved * - * @param width width (in pixels) of the uncompressed image + * @param width width (in pixels) of the packed-pixel image * - * @param pitch bytes per line in the image buffer. Setting this parameter to - * 0 is the equivalent of setting it to + * @param pitch bytes per row in the packed-pixel image. Setting this + * parameter to 0 is the equivalent of setting it to * width * #tjPixelSize[pixelFormat]. * - * @param height height (in pixels) of the uncompressed image + * @param height height (in pixels) of the packed-pixel image * - * @param pixelFormat pixel format of the image buffer (see @ref TJPF + * @param pixelFormat pixel format of the packed-pixel image (see @ref TJPF * "Pixel formats".) If this parameter is set to @ref TJPF_GRAY, then the - * image will be stored in PGM or 8-bit (indexed color) BMP format. Otherwise, - * the image will be stored in PPM or 24-bit BMP format. If this parameter - * is set to @ref TJPF_CMYK, then the CMYK pixels will be converted to RGB - * using a quick & dirty algorithm that is suitable only for testing (proper - * conversion between CMYK and other formats requires a color management - * system.) + * image will be stored in PGM or 8-bit-per-pixel (indexed color) BMP format. + * Otherwise, the image will be stored in PPM or 24-bit-per-pixel BMP format. + * If this parameter is set to @ref TJPF_CMYK, then the CMYK pixels will be + * converted to RGB using a quick & dirty algorithm that is suitable only for + * testing purposes. (Proper conversion between CMYK and other formats + * requires a color management system.) * * @param flags the bitwise OR of one or more of the @ref TJFLAG_BOTTOMUP * "flags". @@ -1631,8 +1665,8 @@ DLLEXPORT int tjSaveImage(const char *filename, unsigned char *buffer, /** - * Free an image buffer previously allocated by TurboJPEG. You should always - * use this function to free JPEG destination buffer(s) that were automatically + * Free a byte buffer previously allocated by TurboJPEG. You should always use + * this function to free JPEG destination buffer(s) that were automatically * (re)allocated by the compression and transform functions or that were * manually allocated using #tjAlloc(). * @@ -1650,7 +1684,7 @@ DLLEXPORT void tjFree(unsigned char *buffer); * @param handle a handle to a TurboJPEG compressor, decompressor, or * transformer instance, or NULL if the error was generated by a global * function (but note that retrieving the error message for a global function - * is not thread-safe.) + * is thread-safe only on platforms that support thread-local storage.) * * @return a descriptive error message explaining why the last command failed. */ @@ -1670,14 +1704,10 @@ DLLEXPORT char *tjGetErrorStr2(tjhandle handle); DLLEXPORT int tjGetErrorCode(tjhandle handle); -/* Deprecated functions and macros */ -#define TJFLAG_FORCEMMX 8 -#define TJFLAG_FORCESSE 16 -#define TJFLAG_FORCESSE2 32 -#define TJFLAG_FORCESSE3 128 +/* Backward compatibility functions and macros (nothing to see here) */ +/* TurboJPEG 1.0+ */ -/* Backward compatibility functions and macros (nothing to see here) */ #define NUMSUBOPT TJ_NUMSAMP #define TJ_444 TJSAMP_444 #define TJ_422 TJSAMP_422 @@ -1693,46 +1723,55 @@ DLLEXPORT int tjGetErrorCode(tjhandle handle); #define TJ_ALPHAFIRST 64 #define TJ_FORCESSE3 TJFLAG_FORCESSE3 #define TJ_FASTUPSAMPLE TJFLAG_FASTUPSAMPLE -#define TJ_YUV 512 DLLEXPORT unsigned long TJBUFSIZE(int width, int height); -DLLEXPORT unsigned long TJBUFSIZEYUV(int width, int height, int jpegSubsamp); - -DLLEXPORT unsigned long tjBufSizeYUV(int width, int height, int subsamp); - DLLEXPORT int tjCompress(tjhandle handle, unsigned char *srcBuf, int width, int pitch, int height, int pixelSize, unsigned char *dstBuf, unsigned long *compressedSize, int jpegSubsamp, int jpegQual, int flags); -DLLEXPORT int tjEncodeYUV(tjhandle handle, unsigned char *srcBuf, int width, - int pitch, int height, int pixelSize, - unsigned char *dstBuf, int subsamp, int flags); - -DLLEXPORT int tjEncodeYUV2(tjhandle handle, unsigned char *srcBuf, int width, - int pitch, int height, int pixelFormat, - unsigned char *dstBuf, int subsamp, int flags); +DLLEXPORT int tjDecompress(tjhandle handle, unsigned char *jpegBuf, + unsigned long jpegSize, unsigned char *dstBuf, + int width, int pitch, int height, int pixelSize, + int flags); DLLEXPORT int tjDecompressHeader(tjhandle handle, unsigned char *jpegBuf, unsigned long jpegSize, int *width, int *height); +DLLEXPORT char *tjGetErrorStr(void); + +/* TurboJPEG 1.1+ */ + +#define TJ_YUV 512 + +DLLEXPORT unsigned long TJBUFSIZEYUV(int width, int height, int jpegSubsamp); + DLLEXPORT int tjDecompressHeader2(tjhandle handle, unsigned char *jpegBuf, unsigned long jpegSize, int *width, int *height, int *jpegSubsamp); -DLLEXPORT int tjDecompress(tjhandle handle, unsigned char *jpegBuf, - unsigned long jpegSize, unsigned char *dstBuf, - int width, int pitch, int height, int pixelSize, - int flags); - DLLEXPORT int tjDecompressToYUV(tjhandle handle, unsigned char *jpegBuf, unsigned long jpegSize, unsigned char *dstBuf, int flags); -DLLEXPORT char *tjGetErrorStr(void); +DLLEXPORT int tjEncodeYUV(tjhandle handle, unsigned char *srcBuf, int width, + int pitch, int height, int pixelSize, + unsigned char *dstBuf, int subsamp, int flags); + +/* TurboJPEG 1.2+ */ +#define TJFLAG_FORCEMMX 8 +#define TJFLAG_FORCESSE 16 +#define TJFLAG_FORCESSE2 32 +#define TJFLAG_FORCESSE3 128 + +DLLEXPORT unsigned long tjBufSizeYUV(int width, int height, int subsamp); + +DLLEXPORT int tjEncodeYUV2(tjhandle handle, unsigned char *srcBuf, int width, + int pitch, int height, int pixelFormat, + unsigned char *dstBuf, int subsamp, int flags); /** * @} diff --git a/third-party/mozjpeg/mozjpeg/usage.txt b/third-party/mozjpeg/mozjpeg/usage.txt index bd807026645..b5076264be6 100644 --- a/third-party/mozjpeg/mozjpeg/usage.txt +++ b/third-party/mozjpeg/mozjpeg/usage.txt @@ -25,7 +25,7 @@ GENERAL USAGE We provide two programs, cjpeg to compress an image file into JPEG format, and djpeg to decompress a JPEG file back into a conventional image format. -On Unix-like systems, you say: +On most systems, you say: cjpeg [switches] [imagefile] >jpegfile or djpeg [switches] [jpegfile] >imagefile @@ -34,27 +34,25 @@ named. They always write to standard output (with trace/error messages to standard error). These conventions are handy for piping images between programs. -On most non-Unix systems, you say: +If you defined TWO_FILE_COMMANDLINE when compiling the programs, you can +instead say: cjpeg [switches] imagefile jpegfile or djpeg [switches] jpegfile imagefile i.e., both the input and output files are named on the command line. This style is a little more foolproof, and it loses no functionality if you don't -have pipes. (You can get this style on Unix too, if you prefer, by defining -TWO_FILE_COMMANDLINE when you compile the programs; see install.txt.) +have pipes. You can also say: cjpeg [switches] -outfile jpegfile imagefile or - djpeg [switches] -outfile imagefile jpegfile + djpeg [switches] -outfile imagefile jpegfile This syntax works on all systems, so it is useful for scripts. The currently supported image file formats are: PPM (PBMPLUS color format), -PGM (PBMPLUS grayscale format), BMP, Targa, and RLE (Utah Raster Toolkit -format). (RLE is supported only if the URT library is available, which it -isn't on most non-Unix systems.) cjpeg recognizes the input image format -automatically, with the exception of some Targa files. You have to tell djpeg -which format to generate. +PGM (PBMPLUS grayscale format), BMP, GIF, and Targa. cjpeg recognizes the +input image format automatically, with the exception of some Targa files. You +have to tell djpeg which format to generate. JPEG files are in the defacto standard JFIF file format. There are other, less widely used JPEG-based file formats, but we don't support them. @@ -74,10 +72,7 @@ The basic command line switches for cjpeg are: Quality is 0 (worst) to 100 (best); default is 75. (See below for more info.) - -grayscale Create monochrome JPEG file from color input. - Be sure to use this switch when compressing a grayscale - BMP file, because cjpeg isn't bright enough to notice - whether a BMP file uses only shades of gray. By + -grayscale Create monochrome JPEG file from color input. By saying -grayscale, you'll get a smaller JPEG file that takes less time to process. @@ -148,8 +143,8 @@ customized) quantization tables can be set with the -qtables option and assigned to components with the -qslots option (see the "wizard" switches below.) -JPEG files generated with separate luminance and chrominance quality are -fully compliant with standard JPEG decoders. +JPEG files generated with separate luminance and chrominance quality are fully +compliant with standard JPEG decoders. CAUTION: For this setting to be useful, be sure to pass an argument of -sample 1x1 to cjpeg to disable chrominance subsampling. Otherwise, the @@ -170,35 +165,43 @@ Switches for advanced users: be unable to view an arithmetic coded JPEG file at all. - -dct int Use integer DCT method (default). - -dct fast Use fast integer DCT (less accurate). - In libjpeg-turbo, the fast method is generally about - 5-15% faster than the int method when using the - x86/x86-64 SIMD extensions (results may vary with other - SIMD implementations, or when using libjpeg-turbo - without SIMD extensions.) For quality levels of 90 and - below, there should be little or no perceptible - difference between the two algorithms. For quality - levels above 90, however, the difference between - the fast and the int methods becomes more pronounced. - With quality=97, for instance, the fast method incurs - generally about a 1-3 dB loss (in PSNR) relative to - the int method, but this can be larger for some images. - Do not use the fast method with quality levels above - 97. The algorithm often degenerates at quality=98 and - above and can actually produce a more lossy image than - if lower quality levels had been used. Also, in - libjpeg-turbo, the fast method is not fully accerated - for quality levels above 97, so it will be slower than - the int method. - -dct float Use floating-point DCT method. - The float method is mainly a legacy feature. It does - not produce significantly more accurate results than - the int method, and it is much slower. The float - method may also give different results on different - machines due to varying roundoff behavior, whereas the - integer methods should give the same results on all - machines. + -dct int Use accurate integer DCT method (default). + -dct fast Use less accurate integer DCT method [legacy feature]. + When the Independent JPEG Group's software was first + released in 1991, the compression time for a + 1-megapixel JPEG image on a mainstream PC was measured + in minutes. Thus, the fast integer DCT algorithm + provided noticeable performance benefits. On modern + CPUs running libjpeg-turbo, however, the compression + time for a 1-megapixel JPEG image is measured in + milliseconds, and thus the performance benefits of the + fast algorithm are much less noticeable. On modern + x86/x86-64 CPUs that support AVX2 instructions, the + fast and int methods have similar performance. On + other types of CPUs, the fast method is generally about + 5-15% faster than the int method. + + For quality levels of 90 and below, there should be + little or no perceptible quality difference between the + two algorithms. For quality levels above 90, however, + the difference between the fast and int methods becomes + more pronounced. With quality=97, for instance, the + fast method incurs generally about a 1-3 dB loss in + PSNR relative to the int method, but this can be larger + for some images. Do not use the fast method with + quality levels above 97. The algorithm often + degenerates at quality=98 and above and can actually + produce a more lossy image than if lower quality levels + had been used. Also, in libjpeg-turbo, the fast method + is not fully accelerated for quality levels above 97, + so it will be slower than the int method. + -dct float Use floating-point DCT method [legacy feature]. + The float method does not produce significantly more + accurate results than the int method, and it is much + slower. The float method may also give different + results on different machines due to varying roundoff + behavior, whereas the integer methods should give the + same results on all machines. -restart N Emit a JPEG restart marker every N MCU rows, or every N MCU blocks if "B" is attached to the number. @@ -215,7 +218,7 @@ Switches for advanced users: space is needed, an error will occur. -verbose Enable debug printout. More -v's give more printout. - or -debug Also, version information is printed at startup. + or -debug Also, version information is printed at startup. The -restart option inserts extra markers that allow a JPEG decoder to resynchronize after a transmission error. Without restart markers, any damage @@ -285,10 +288,17 @@ The basic command line switches for djpeg are: is specified, or if the JPEG file is grayscale; otherwise, 24-bit full-color format is emitted. - -gif Select GIF output format. Since GIF does not support - more than 256 colors, -colors 256 is assumed (unless - you specify a smaller number of colors). If you - specify -fast, the default number of colors is 216. + -gif Select GIF output format (LZW-compressed). Since GIF + does not support more than 256 colors, -colors 256 is + assumed (unless you specify a smaller number of + colors). If you specify -fast, the default number of + colors is 216. + + -gif0 Select GIF output format (uncompressed). Since GIF + does not support more than 256 colors, -colors 256 is + assumed (unless you specify a smaller number of + colors). If you specify -fast, the default number of + colors is 216. -os2 Select BMP output format (OS/2 1.x flavor). 8-bit colormapped format is emitted if -colors or -grayscale @@ -300,8 +310,6 @@ The basic command line switches for djpeg are: grayscale or if -grayscale is specified; otherwise PPM is emitted. - -rle Select RLE output format. (Requires URT library.) - -targa Select Targa output format. Grayscale format is emitted if the JPEG file is grayscale or if -grayscale is specified; otherwise, colormapped format @@ -310,36 +318,45 @@ The basic command line switches for djpeg are: Switches for advanced users: - -dct int Use integer DCT method (default). - -dct fast Use fast integer DCT (less accurate). - In libjpeg-turbo, the fast method is generally about - 5-15% faster than the int method when using the - x86/x86-64 SIMD extensions (results may vary with other - SIMD implementations, or when using libjpeg-turbo - without SIMD extensions.) If the JPEG image was - compressed using a quality level of 85 or below, then - there should be little or no perceptible difference - between the two algorithms. When decompressing images - that were compressed using quality levels above 85, - however, the difference between the fast and int - methods becomes more pronounced. With images - compressed using quality=97, for instance, the fast - method incurs generally about a 4-6 dB loss (in PSNR) - relative to the int method, but this can be larger for - some images. If you can avoid it, do not use the fast - method when decompressing images that were compressed - using quality levels above 97. The algorithm often - degenerates for such images and can actually produce - a more lossy output image than if the JPEG image had - been compressed using lower quality levels. - -dct float Use floating-point DCT method. - The float method is mainly a legacy feature. It does - not produce significantly more accurate results than - the int method, and it is much slower. The float - method may also give different results on different - machines due to varying roundoff behavior, whereas the - integer methods should give the same results on all - machines. + -dct int Use accurate integer DCT method (default). + -dct fast Use less accurate integer DCT method [legacy feature]. + When the Independent JPEG Group's software was first + released in 1991, the decompression time for a + 1-megapixel JPEG image on a mainstream PC was measured + in minutes. Thus, the fast integer DCT algorithm + provided noticeable performance benefits. On modern + CPUs running libjpeg-turbo, however, the decompression + time for a 1-megapixel JPEG image is measured in + milliseconds, and thus the performance benefits of the + fast algorithm are much less noticeable. On modern + x86/x86-64 CPUs that support AVX2 instructions, the + fast and int methods have similar performance. On + other types of CPUs, the fast method is generally about + 5-15% faster than the int method. + + If the JPEG image was compressed using a quality level + of 85 or below, then there should be little or no + perceptible quality difference between the two + algorithms. When decompressing images that were + compressed using quality levels above 85, however, the + difference between the fast and int methods becomes + more pronounced. With images compressed using + quality=97, for instance, the fast method incurs + generally about a 4-6 dB loss in PSNR relative to the + int method, but this can be larger for some images. If + you can avoid it, do not use the fast method when + decompressing images that were compressed using quality + levels above 97. The algorithm often degenerates for + such images and can actually produce a more lossy + output image than if the JPEG image had been compressed + using lower quality levels. + -dct float Use floating-point DCT method [legacy feature]. + The float method does not produce significantly more + accurate results than the int method, and it is much + slower. The float method may also give different + results on different machines due to varying roundoff + behavior, whereas the integer methods should give the + same results on all machines. -dither fs Use Floyd-Steinberg dithering in color quantization. -dither ordered Use ordered dithering in color quantization. @@ -399,11 +416,6 @@ quality settings to make very small JPEG files; the percentage improvement is often a lot more than it is on larger files. (At present, -optimize mode is always selected when generating progressive JPEG files.) -Support for GIF input files was removed in cjpeg v6b due to concerns over -the Unisys LZW patent. Although this patent expired in 2006, cjpeg still -lacks GIF support, for these historical reasons. (Conversion of GIF files to -JPEG is usually a bad idea anyway.) - HINTS FOR DJPEG @@ -418,10 +430,6 @@ When producing a color-quantized image, "-onepass -dither ordered" is fast but much lower quality than the default behavior. "-dither none" may give acceptable results in two-pass mode, but is seldom tolerable in one-pass mode. -To avoid the Unisys LZW patent (now expired), djpeg produces uncompressed GIF -files. These are larger than they should be, but are readable by standard GIF -decoders. - HINTS FOR BOTH PROGRAMS @@ -454,9 +462,10 @@ quality. However, while the image data is losslessly transformed, metadata can be removed. See the -copy option for specifics. jpegtran uses a command line syntax similar to cjpeg or djpeg. -On Unix-like systems, you say: +On most systems, you say: jpegtran [switches] [inputfile] >outputfile -On most non-Unix systems, you say: +If you defined TWO_FILE_COMMANDLINE when compiling the program, you can instead +say: jpegtran [switches] inputfile outputfile where both the input and output files are JPEG files. @@ -528,6 +537,43 @@ The image can be losslessly cropped by giving the switch: -crop WxH+X+Y Crop to a rectangular region of width W and height H, starting at point X,Y. +If W or H is larger than the width/height of the input image, then the output +image is expanded in size, and the expanded region is filled in with zeros +(neutral gray). Attaching an 'f' character ("flatten") to the width number +will cause each block in the expanded region to be filled in with the DC +coefficient of the nearest block in the input image rather than grayed out. +Attaching an 'r' character ("reflect") to the width number will cause the +expanded region to be filled in with repeated reflections of the input image +rather than grayed out. + +A complementary lossless wipe option is provided to discard (gray out) data +inside a given image region while losslessly preserving what is outside: + -wipe WxH+X+Y Wipe (gray out) a rectangular region of width W and + height H from the input image, starting at point X,Y. + +Attaching an 'f' character ("flatten") to the width number will cause the +region to be filled with the average of adjacent blocks rather than grayed out. +If the wipe region and the region outside the wipe region, when adjusted to the +nearest iMCU boundary, form two horizontally adjacent rectangles, then +attaching an 'r' character ("reflect") to the width number will cause the wipe +region to be filled with repeated reflections of the outside region rather than +grayed out. + +A lossless drop option is also provided, which allows another JPEG image to be +inserted ("dropped") into the input image data at a given position, replacing +the existing image data at that position: + -drop +X+Y filename Drop (insert) another image at point X,Y + +Both the input image and the drop image must have the same subsampling level. +It is best if they also have the same quantization (quality.) Otherwise, the +quantization of the output image will be adapted to accommodate the higher of +the input image quality and the drop image quality. The trim option can be +used with the drop option to requantize the drop image to match the input +image. Note that a grayscale image can be dropped into a full-color image or +vice versa, as long as the full-color image has no vertical subsampling. If +the input image is grayscale and the drop image is full-color, then the +chrominance channels from the drop image will be discarded. + Other not-strictly-lossless transformation switches are: -grayscale Force grayscale output. @@ -548,6 +594,9 @@ markers, such as comment blocks: -copy comments Copy only comment markers. This setting copies comments from the source file but discards any other metadata. + -copy icc Copy only ICC profile markers. This setting copies the + ICC profile from the source file but discards any other + metadata. -copy all Copy all extra markers. This setting preserves miscellaneous markers found in the source file, such as JFIF thumbnails, Exif data, and Photoshop settings. @@ -596,13 +645,13 @@ file; it does not modify the input file. DO NOT try to overwrite the input file by directing wrjpgcom's output back into it; on most systems this will just destroy your file. -The command line syntax for wrjpgcom is similar to cjpeg's. On Unix-like -systems, it is +The command line syntax for wrjpgcom is similar to cjpeg's. On most systems, +it is wrjpgcom [switches] [inputfilename] The output file is written to standard output. The input file comes from the named file, or from standard input if no input file is named. -On most non-Unix systems, the syntax is +If you defined TWO_FILE_COMMANDLINE when compiling the program, the syntax is: wrjpgcom [switches] inputfilename outputfilename where both input and output file names must be given explicitly. diff --git a/third-party/mozjpeg/mozjpeg/win/gcc/projectTargets-release.cmake.in b/third-party/mozjpeg/mozjpeg/win/gcc/projectTargets-release.cmake.in new file mode 100644 index 00000000000..1e1a8a34aff --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/win/gcc/projectTargets-release.cmake.in @@ -0,0 +1,49 @@ +#---------------------------------------------------------------- +# Generated CMake target import file for configuration "Release". +#---------------------------------------------------------------- + +# Commands may need to know the format version. +set(CMAKE_IMPORT_FILE_VERSION 1) + +# Import target "@CMAKE_PROJECT_NAME@::jpeg" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::jpeg APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::jpeg PROPERTIES + IMPORTED_IMPLIB_RELEASE "${_IMPORT_PREFIX}/lib/libjpeg.dll.a" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/bin/libjpeg-62.dll" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::jpeg ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::jpeg "${_IMPORT_PREFIX}/lib/libjpeg.dll.a" "${_IMPORT_PREFIX}/bin/libjpeg-62.dll" ) + +# Import target "@CMAKE_PROJECT_NAME@::turbojpeg" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::turbojpeg APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::turbojpeg PROPERTIES + IMPORTED_IMPLIB_RELEASE "${_IMPORT_PREFIX}/lib/libturbojpeg.dll.a" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/bin/libturbojpeg.dll" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::turbojpeg ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::turbojpeg "${_IMPORT_PREFIX}/lib/libturbojpeg.dll.a" "${_IMPORT_PREFIX}/bin/libturbojpeg.dll" ) + +# Import target "@CMAKE_PROJECT_NAME@::turbojpeg-static" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::turbojpeg-static APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::turbojpeg-static PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "ASM_NASM;C" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/libturbojpeg.a" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::turbojpeg-static ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::turbojpeg-static "${_IMPORT_PREFIX}/lib/libturbojpeg.a" ) + +# Import target "@CMAKE_PROJECT_NAME@::jpeg-static" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::jpeg-static APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::jpeg-static PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "ASM_NASM;C" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/libjpeg.a" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::jpeg-static ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::jpeg-static "${_IMPORT_PREFIX}/lib/libjpeg.a" ) + +# Commands beyond this point should not need to know the version. +set(CMAKE_IMPORT_FILE_VERSION) diff --git a/third-party/mozjpeg/mozjpeg/win/jconfig.h.in b/third-party/mozjpeg/mozjpeg/win/jconfig.h.in index 6db0b345b2d..0fca77b230c 100644 --- a/third-party/mozjpeg/mozjpeg/win/jconfig.h.in +++ b/third-party/mozjpeg/mozjpeg/win/jconfig.h.in @@ -9,16 +9,7 @@ #define BITS_IN_JSAMPLE @BITS_IN_JSAMPLE@ /* use 8 or 12 */ -#define HAVE_STDDEF_H -#define HAVE_STDLIB_H -#undef NEED_SYS_TYPES_H -#undef NEED_BSD_STRINGS - -#define HAVE_UNSIGNED_CHAR -#define HAVE_UNSIGNED_SHORT -#undef INCOMPLETE_TYPES_BROKEN #undef RIGHT_SHIFT_IS_UNSIGNED -#undef __CHAR_UNSIGNED__ /* Define "boolean" as unsigned char, not int, per Windows custom */ #ifndef __RPCNDR_H__ /* don't conflict if rpcndr.h already read */ diff --git a/third-party/mozjpeg/mozjpeg/win/jpeg.rc.in b/third-party/mozjpeg/mozjpeg/win/jpeg.rc.in new file mode 100644 index 00000000000..650fbe97a6b --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/win/jpeg.rc.in @@ -0,0 +1,35 @@ +#include "Winver.h" +#include "winres.h" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION @SO_MAJOR_VERSION@,@SO_AGE@,@SO_MINOR_VERSION@,0 + PRODUCTVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_REVISION@,0 + FILEFLAGSMASK 0x17L +#ifndef NDEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "@PKGVENDOR@" + VALUE "FileDescription", "libjpeg API DLL" + VALUE "FileVersion", "@SO_MAJOR_VERSION@,@SO_AGE@,@SO_MINOR_VERSION@,0" + VALUE "ProductVersion", "@VERSION@" + VALUE "ProductName", "@CMAKE_PROJECT_NAME@" + VALUE "InternalName", "jpeg@SO_MAJOR_VERSION@" + VALUE "LegalCopyright", L"Copyright \xA9 @COPYRIGHT_YEAR@ The libjpeg-turbo Project and many others" + VALUE "OriginalFilename", "jpeg@SO_MAJOR_VERSION@.dll" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/third-party/mozjpeg/mozjpeg/win/projectTargets.cmake.in b/third-party/mozjpeg/mozjpeg/win/projectTargets.cmake.in new file mode 100644 index 00000000000..05ab4984db1 --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/win/projectTargets.cmake.in @@ -0,0 +1,115 @@ +# Generated by CMake + +if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}" LESS 2.5) + message(FATAL_ERROR "CMake >= 2.6.0 required") +endif() +cmake_policy(PUSH) +cmake_policy(VERSION 2.6) +#---------------------------------------------------------------- +# Generated CMake target import file. +#---------------------------------------------------------------- + +# Commands may need to know the format version. +set(CMAKE_IMPORT_FILE_VERSION 1) + +# Protect against multiple inclusion, which would fail when already imported targets are added once more. +set(_targetsDefined) +set(_targetsNotDefined) +set(_expectedTargets) +foreach(_expectedTarget @CMAKE_PROJECT_NAME@::jpeg @CMAKE_PROJECT_NAME@::turbojpeg @CMAKE_PROJECT_NAME@::turbojpeg-static @CMAKE_PROJECT_NAME@::jpeg-static) + list(APPEND _expectedTargets ${_expectedTarget}) + if(NOT TARGET ${_expectedTarget}) + list(APPEND _targetsNotDefined ${_expectedTarget}) + endif() + if(TARGET ${_expectedTarget}) + list(APPEND _targetsDefined ${_expectedTarget}) + endif() +endforeach() +if("${_targetsDefined}" STREQUAL "${_expectedTargets}") + unset(_targetsDefined) + unset(_targetsNotDefined) + unset(_expectedTargets) + set(CMAKE_IMPORT_FILE_VERSION) + cmake_policy(POP) + return() +endif() +if(NOT "${_targetsDefined}" STREQUAL "") + message(FATAL_ERROR "Some (but not all) targets in this export set were already defined.\nTargets Defined: ${_targetsDefined}\nTargets not yet defined: ${_targetsNotDefined}\n") +endif() +unset(_targetsDefined) +unset(_targetsNotDefined) +unset(_expectedTargets) + + +# Compute the installation prefix relative to this file. +get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +if(_IMPORT_PREFIX STREQUAL "/") + set(_IMPORT_PREFIX "") +endif() + +# Create imported target @CMAKE_PROJECT_NAME@::jpeg +add_library(@CMAKE_PROJECT_NAME@::jpeg SHARED IMPORTED) + +set_target_properties(@CMAKE_PROJECT_NAME@::jpeg PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" +) + +# Create imported target @CMAKE_PROJECT_NAME@::turbojpeg +add_library(@CMAKE_PROJECT_NAME@::turbojpeg SHARED IMPORTED) + +set_target_properties(@CMAKE_PROJECT_NAME@::turbojpeg PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" +) + +# Create imported target @CMAKE_PROJECT_NAME@::turbojpeg-static +add_library(@CMAKE_PROJECT_NAME@::turbojpeg-static STATIC IMPORTED) + +set_target_properties(@CMAKE_PROJECT_NAME@::turbojpeg-static PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" +) + +# Create imported target @CMAKE_PROJECT_NAME@::jpeg-static +add_library(@CMAKE_PROJECT_NAME@::jpeg-static STATIC IMPORTED) + +set_target_properties(@CMAKE_PROJECT_NAME@::jpeg-static PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" +) + +# Load information for each installed configuration. +get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +file(GLOB CONFIG_FILES "${_DIR}/@CMAKE_PROJECT_NAME@Targets-*.cmake") +foreach(f ${CONFIG_FILES}) + include(${f}) +endforeach() + +# Cleanup temporary variables. +set(_IMPORT_PREFIX) + +# Loop over all imported files and verify that they actually exist +foreach(target ${_IMPORT_CHECK_TARGETS} ) + foreach(file ${_IMPORT_CHECK_FILES_FOR_${target}} ) + if(NOT EXISTS "${file}" ) + message(FATAL_ERROR "The imported target \"${target}\" references the file + \"${file}\" +but this file does not exist. Possible reasons include: +* The file was deleted, renamed, or moved to another location. +* An install or uninstall procedure did not complete successfully. +* The installation package was faulty and contained + \"${CMAKE_CURRENT_LIST_FILE}\" +but not all the files it references. +") + endif() + endforeach() + unset(_IMPORT_CHECK_FILES_FOR_${target}) +endforeach() +unset(_IMPORT_CHECK_TARGETS) + +# This file does not depend on other imported targets which have +# been exported from the same project but in a separate export set. + +# Commands beyond this point should not need to know the version. +set(CMAKE_IMPORT_FILE_VERSION) +cmake_policy(POP) diff --git a/third-party/mozjpeg/mozjpeg/win/turbojpeg.rc.in b/third-party/mozjpeg/mozjpeg/win/turbojpeg.rc.in new file mode 100644 index 00000000000..c6cfc2dfedd --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/win/turbojpeg.rc.in @@ -0,0 +1,35 @@ +#include "Winver.h" +#include "winres.h" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 0,@TURBOJPEG_SO_AGE@,0,0 + PRODUCTVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_REVISION@,0 + FILEFLAGSMASK 0x17L +#ifndef NDEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "@PKGVENDOR@" + VALUE "FileDescription", "TurboJPEG API DLL" + VALUE "FileVersion", "0,@TURBOJPEG_SO_AGE@,0,0" + VALUE "ProductVersion", "@VERSION@" + VALUE "ProductName", "@CMAKE_PROJECT_NAME@" + VALUE "InternalName", "turbojpeg" + VALUE "LegalCopyright", L"Copyright \xA9 @COPYRIGHT_YEAR@ The libjpeg-turbo Project and many others" + VALUE "OriginalFilename", "turbojpeg.dll" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/third-party/mozjpeg/mozjpeg/win/vc/projectTargets-release.cmake.in b/third-party/mozjpeg/mozjpeg/win/vc/projectTargets-release.cmake.in new file mode 100644 index 00000000000..7abb281b70c --- /dev/null +++ b/third-party/mozjpeg/mozjpeg/win/vc/projectTargets-release.cmake.in @@ -0,0 +1,49 @@ +#---------------------------------------------------------------- +# Generated CMake target import file for configuration "Release". +#---------------------------------------------------------------- + +# Commands may need to know the format version. +set(CMAKE_IMPORT_FILE_VERSION 1) + +# Import target "@CMAKE_PROJECT_NAME@::jpeg" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::jpeg APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::jpeg PROPERTIES + IMPORTED_IMPLIB_RELEASE "${_IMPORT_PREFIX}/lib/jpeg.lib" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/bin/jpeg62.dll" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::jpeg ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::jpeg "${_IMPORT_PREFIX}/lib/jpeg.lib" "${_IMPORT_PREFIX}/bin/jpeg62.dll" ) + +# Import target "@CMAKE_PROJECT_NAME@::turbojpeg" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::turbojpeg APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::turbojpeg PROPERTIES + IMPORTED_IMPLIB_RELEASE "${_IMPORT_PREFIX}/lib/turbojpeg.lib" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/bin/turbojpeg.dll" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::turbojpeg ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::turbojpeg "${_IMPORT_PREFIX}/lib/turbojpeg.lib" "${_IMPORT_PREFIX}/bin/turbojpeg.dll" ) + +# Import target "@CMAKE_PROJECT_NAME@::turbojpeg-static" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::turbojpeg-static APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::turbojpeg-static PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "ASM_NASM;C" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/turbojpeg-static.lib" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::turbojpeg-static ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::turbojpeg-static "${_IMPORT_PREFIX}/lib/turbojpeg-static.lib" ) + +# Import target "@CMAKE_PROJECT_NAME@::jpeg-static" for configuration "Release" +set_property(TARGET @CMAKE_PROJECT_NAME@::jpeg-static APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +set_target_properties(@CMAKE_PROJECT_NAME@::jpeg-static PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "ASM_NASM;C" + IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/jpeg-static.lib" + ) + +list(APPEND _IMPORT_CHECK_TARGETS @CMAKE_PROJECT_NAME@::jpeg-static ) +list(APPEND _IMPORT_CHECK_FILES_FOR_@CMAKE_PROJECT_NAME@::jpeg-static "${_IMPORT_PREFIX}/lib/jpeg-static.lib" ) + +# Commands beyond this point should not need to know the version. +set(CMAKE_IMPORT_FILE_VERSION) diff --git a/third-party/mozjpeg/mozjpeg/wizard.txt b/third-party/mozjpeg/mozjpeg/wizard.txt index c57fe38a542..0e155f9e7da 100644 --- a/third-party/mozjpeg/mozjpeg/wizard.txt +++ b/third-party/mozjpeg/mozjpeg/wizard.txt @@ -149,7 +149,15 @@ the script represents a progressive or sequential file, by observing whether Ss and Se values other than 0 and 63 appear. (The -progressive switch is not needed to specify this; in fact, it is ignored when -scans appears.) The scan script must meet the JPEG restrictions on progression sequences. -(cjpeg checks that the spec's requirements are obeyed.) +(cjpeg checks that the spec's requirements are obeyed.) More specifically: + + * An AC scan cannot include coefficients from more than one component. + + * An AC scan for a particular component must be preceded by a DC scan + that includes the same component. + + * Only the first AC scan that includes a particular coefficient for a + particular component can include more than one bit from that coefficient. Scan script files are free format, in that arbitrary whitespace can appear between numbers and around punctuation. Also, comments can be included: a diff --git a/third-party/mozjpeg/mozjpeg/wrbmp.c b/third-party/mozjpeg/mozjpeg/wrbmp.c index 239f64eb3c3..45fff684d82 100644 --- a/third-party/mozjpeg/mozjpeg/wrbmp.c +++ b/third-party/mozjpeg/mozjpeg/wrbmp.c @@ -5,7 +5,7 @@ * Copyright (C) 1994-1996, Thomas G. Lane. * libjpeg-turbo Modifications: * Copyright (C) 2013, Linaro Limited. - * Copyright (C) 2014-2015, 2017, 2019, D. R. Commander. + * Copyright (C) 2014-2015, 2017, 2019, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -121,7 +121,7 @@ put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, inptr = dest->pub.buffer[0]; if (cinfo->out_color_space == JCS_EXT_BGR) { - MEMCOPY(outptr, inptr, dest->row_width); + memcpy(outptr, inptr, dest->row_width); outptr += cinfo->output_width * 3; } else if (cinfo->out_color_space == JCS_RGB565) { boolean big_endian = is_big_endian(); @@ -141,7 +141,6 @@ put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, } } else if (cinfo->out_color_space == JCS_CMYK) { for (col = cinfo->output_width; col > 0; col--) { - /* can omit GETJSAMPLE() safely */ JSAMPLE c = *inptr++, m = *inptr++, y = *inptr++, k = *inptr++; cmyk_to_rgb(c, m, y, k, outptr + 2, outptr + 1, outptr); outptr += 3; @@ -153,7 +152,6 @@ put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, register int ps = rgb_pixelsize[cinfo->out_color_space]; for (col = cinfo->output_width; col > 0; col--) { - /* can omit GETJSAMPLE() safely */ outptr[0] = inptr[bindex]; outptr[1] = inptr[gindex]; outptr[2] = inptr[rindex]; @@ -167,7 +165,7 @@ put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, *outptr++ = 0; if (!dest->use_inversion_array) - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->row_width); + fwrite(dest->iobuffer, 1, dest->row_width, dest->pub.output_file); } METHODDEF(void) @@ -193,7 +191,7 @@ put_gray_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, /* Transfer data. */ inptr = dest->pub.buffer[0]; - MEMCOPY(outptr, inptr, cinfo->output_width); + memcpy(outptr, inptr, cinfo->output_width); outptr += cinfo->output_width; /* Zero out the pad bytes. */ @@ -202,7 +200,7 @@ put_gray_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, *outptr++ = 0; if (!dest->use_inversion_array) - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->row_width); + fwrite(dest->iobuffer, 1, dest->row_width, dest->pub.output_file); } @@ -258,8 +256,8 @@ write_bmp_header(j_decompress_ptr cinfo, bmp_dest_ptr dest) bfSize = headersize + (long)dest->row_width * (long)cinfo->output_height; /* Set unused fields of header to 0 */ - MEMZERO(bmpfileheader, sizeof(bmpfileheader)); - MEMZERO(bmpinfoheader, sizeof(bmpinfoheader)); + memset(bmpfileheader, 0, sizeof(bmpfileheader)); + memset(bmpinfoheader, 0, sizeof(bmpinfoheader)); /* Fill the file header */ bmpfileheader[0] = 0x42; /* first 2 bytes are ASCII 'B', 'M' */ @@ -283,9 +281,9 @@ write_bmp_header(j_decompress_ptr cinfo, bmp_dest_ptr dest) PUT_2B(bmpinfoheader, 32, cmap_entries); /* biClrUsed */ /* we leave biClrImportant = 0 */ - if (JFWRITE(dest->pub.output_file, bmpfileheader, 14) != (size_t)14) + if (fwrite(bmpfileheader, 1, 14, dest->pub.output_file) != (size_t)14) ERREXIT(cinfo, JERR_FILE_WRITE); - if (JFWRITE(dest->pub.output_file, bmpinfoheader, 40) != (size_t)40) + if (fwrite(bmpinfoheader, 1, 40, dest->pub.output_file) != (size_t)40) ERREXIT(cinfo, JERR_FILE_WRITE); if (cmap_entries > 0) @@ -327,8 +325,8 @@ write_os2_header(j_decompress_ptr cinfo, bmp_dest_ptr dest) bfSize = headersize + (long)dest->row_width * (long)cinfo->output_height; /* Set unused fields of header to 0 */ - MEMZERO(bmpfileheader, sizeof(bmpfileheader)); - MEMZERO(bmpcoreheader, sizeof(bmpcoreheader)); + memset(bmpfileheader, 0, sizeof(bmpfileheader)); + memset(bmpcoreheader, 0, sizeof(bmpcoreheader)); /* Fill the file header */ bmpfileheader[0] = 0x42; /* first 2 bytes are ASCII 'B', 'M' */ @@ -344,9 +342,9 @@ write_os2_header(j_decompress_ptr cinfo, bmp_dest_ptr dest) PUT_2B(bmpcoreheader, 8, 1); /* bcPlanes - must be 1 */ PUT_2B(bmpcoreheader, 10, bits_per_pixel); /* bcBitCount */ - if (JFWRITE(dest->pub.output_file, bmpfileheader, 14) != (size_t)14) + if (fwrite(bmpfileheader, 1, 14, dest->pub.output_file) != (size_t)14) ERREXIT(cinfo, JERR_FILE_WRITE); - if (JFWRITE(dest->pub.output_file, bmpcoreheader, 12) != (size_t)12) + if (fwrite(bmpcoreheader, 1, 12, dest->pub.output_file) != (size_t)12) ERREXIT(cinfo, JERR_FILE_WRITE); if (cmap_entries > 0) @@ -372,18 +370,18 @@ write_colormap(j_decompress_ptr cinfo, bmp_dest_ptr dest, int map_colors, if (cinfo->out_color_components == 3) { /* Normal case with RGB colormap */ for (i = 0; i < num_colors; i++) { - putc(GETJSAMPLE(colormap[2][i]), outfile); - putc(GETJSAMPLE(colormap[1][i]), outfile); - putc(GETJSAMPLE(colormap[0][i]), outfile); + putc(colormap[2][i], outfile); + putc(colormap[1][i], outfile); + putc(colormap[0][i], outfile); if (map_entry_size == 4) putc(0, outfile); } } else { /* Grayscale colormap (only happens with grayscale quantization) */ for (i = 0; i < num_colors; i++) { - putc(GETJSAMPLE(colormap[0][i]), outfile); - putc(GETJSAMPLE(colormap[0][i]), outfile); - putc(GETJSAMPLE(colormap[0][i]), outfile); + putc(colormap[0][i], outfile); + putc(colormap[0][i], outfile); + putc(colormap[0][i], outfile); if (map_entry_size == 4) putc(0, outfile); } @@ -438,7 +436,6 @@ finish_output_bmp(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) JSAMPARRAY image_ptr; register JSAMPROW data_ptr; JDIMENSION row; - register JDIMENSION col; cd_progress_ptr progress = (cd_progress_ptr)cinfo->progress; if (dest->use_inversion_array) { @@ -459,10 +456,7 @@ finish_output_bmp(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) ((j_common_ptr)cinfo, dest->whole_image, row - 1, (JDIMENSION)1, FALSE); data_ptr = image_ptr[0]; - for (col = dest->row_width; col > 0; col--) { - putc(GETJSAMPLE(*data_ptr), outfile); - data_ptr++; - } + fwrite(data_ptr, 1, dest->row_width, outfile); } if (progress != NULL) progress->completed_extra_passes++; diff --git a/third-party/mozjpeg/mozjpeg/wrgif.c b/third-party/mozjpeg/mozjpeg/wrgif.c index 1804e0bb39c..620a3ba9177 100644 --- a/third-party/mozjpeg/mozjpeg/wrgif.c +++ b/third-party/mozjpeg/mozjpeg/wrgif.c @@ -3,19 +3,14 @@ * * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1997, Thomas G. Lane. + * Modified 2015-2019 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2015, 2017, D. R. Commander. + * Copyright (C) 2015, 2017, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * * This file contains routines to write output images in GIF format. * - ************************************************************************** - * NOTE: to avoid entanglements with Unisys' patent on LZW compression, * - * this code has been modified to output "uncompressed GIF" files. * - * There is no trace of the LZW algorithm in this file. * - ************************************************************************** - * * These routines may need modification for non-Unix environments or * specialized applications. As they stand, they assume output to * an ordinary stdio stream. @@ -33,11 +28,6 @@ * copyright notice and this permission notice appear in supporting * documentation. This software is provided "as is" without express or * implied warranty. - * - * We are also required to state that - * "The Graphics Interchange Format(c) is the Copyright property of - * CompuServe Incorporated. GIF(sm) is a Service Mark property of - * CompuServe Incorporated." */ #include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ @@ -45,6 +35,37 @@ #ifdef GIF_SUPPORTED +#define MAX_LZW_BITS 12 /* maximum LZW code size (4096 symbols) */ + +typedef INT16 code_int; /* must hold -1 .. 2**MAX_LZW_BITS */ + +#define LZW_TABLE_SIZE ((code_int)1 << MAX_LZW_BITS) + +#define HSIZE 5003 /* hash table size for 80% occupancy */ + +typedef int hash_int; /* must hold -2*HSIZE..2*HSIZE */ + +#define MAXCODE(n_bits) (((code_int)1 << (n_bits)) - 1) + + +/* + * The LZW hash table consists of two parallel arrays: + * hash_code[i] code of symbol in slot i, or 0 if empty slot + * hash_value[i] symbol's value; undefined if empty slot + * where slot values (i) range from 0 to HSIZE-1. The symbol value is + * its prefix symbol's code concatenated with its suffix character. + * + * Algorithm: use open addressing double hashing (no chaining) on the + * prefix code / suffix character combination. We do a variant of Knuth's + * algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + * secondary probe. + */ + +typedef int hash_entry; /* must hold (code_int << 8) | byte */ + +#define HASH_ENTRY(prefix, suffix) ((((hash_entry)(prefix)) << 8) | (suffix)) + + /* Private version of data destination object */ typedef struct { @@ -54,14 +75,24 @@ typedef struct { /* State for packing variable-width codes into a bitstream */ int n_bits; /* current number of bits/code */ - int maxcode; /* maximum code, given n_bits */ - long cur_accum; /* holds bits not yet output */ + code_int maxcode; /* maximum code, given n_bits */ + int init_bits; /* initial n_bits ... restored after clear */ + int cur_accum; /* holds bits not yet output */ int cur_bits; /* # of bits in cur_accum */ + /* LZW string construction */ + code_int waiting_code; /* symbol not yet output; may be extendable */ + boolean first_byte; /* if TRUE, waiting_code is not valid */ + /* State for GIF code assignment */ - int ClearCode; /* clear code (doesn't change) */ - int EOFCode; /* EOF code (ditto) */ - int code_counter; /* counts output symbols */ + code_int ClearCode; /* clear code (doesn't change) */ + code_int EOFCode; /* EOF code (ditto) */ + code_int free_code; /* LZW: first not-yet-used symbol code */ + code_int code_counter; /* not LZW: counts output symbols */ + + /* LZW hash table */ + code_int *hash_code; /* => hash table of symbol codes */ + hash_entry *hash_value; /* => hash table of symbol values */ /* GIF data packet construction buffer */ int bytesinpkt; /* # of bytes in current packet */ @@ -71,9 +102,6 @@ typedef struct { typedef gif_dest_struct *gif_dest_ptr; -/* Largest value that will fit in N bits */ -#define MAXCODE(n_bits) ((1 << (n_bits)) - 1) - /* * Routines to package finished data bytes into GIF data blocks. @@ -86,8 +114,8 @@ flush_packet(gif_dest_ptr dinfo) { if (dinfo->bytesinpkt > 0) { /* never write zero-length packet */ dinfo->packetbuf[0] = (char)dinfo->bytesinpkt++; - if (JFWRITE(dinfo->pub.output_file, dinfo->packetbuf, dinfo->bytesinpkt) != - (size_t)dinfo->bytesinpkt) + if (fwrite(dinfo->packetbuf, 1, dinfo->bytesinpkt, + dinfo->pub.output_file) != (size_t)dinfo->bytesinpkt) ERREXIT(dinfo->cinfo, JERR_FILE_WRITE); dinfo->bytesinpkt = 0; } @@ -105,7 +133,7 @@ flush_packet(gif_dest_ptr dinfo) /* Routine to convert variable-width codes into a byte stream */ LOCAL(void) -output(gif_dest_ptr dinfo, int code) +output(gif_dest_ptr dinfo, code_int code) /* Emit a code of n_bits bits */ /* Uses cur_accum and cur_bits to reblock into 8-bit bytes */ { @@ -117,74 +145,76 @@ output(gif_dest_ptr dinfo, int code) dinfo->cur_accum >>= 8; dinfo->cur_bits -= 8; } + + /* + * If the next entry is going to be too big for the code size, + * then increase it, if possible. We do this here to ensure + * that it's done in sync with the decoder's codesize increases. + */ + if (dinfo->free_code > dinfo->maxcode) { + dinfo->n_bits++; + if (dinfo->n_bits == MAX_LZW_BITS) + dinfo->maxcode = LZW_TABLE_SIZE; /* free_code will never exceed this */ + else + dinfo->maxcode = MAXCODE(dinfo->n_bits); + } } -/* The pseudo-compression algorithm. - * - * In this module we simply output each pixel value as a separate symbol; - * thus, no compression occurs. In fact, there is expansion of one bit per - * pixel, because we use a symbol width one bit wider than the pixel width. - * - * GIF ordinarily uses variable-width symbols, and the decoder will expect - * to ratchet up the symbol width after a fixed number of symbols. - * To simplify the logic and keep the expansion penalty down, we emit a - * GIF Clear code to reset the decoder just before the width would ratchet up. - * Thus, all the symbols in the output file will have the same bit width. - * Note that emitting the Clear codes at the right times is a mere matter of - * counting output symbols and is in no way dependent on the LZW patent. - * - * With a small basic pixel width (low color count), Clear codes will be - * needed very frequently, causing the file to expand even more. So this - * simplistic approach wouldn't work too well on bilevel images, for example. - * But for output of JPEG conversions the pixel width will usually be 8 bits - * (129 to 256 colors), so the overhead added by Clear symbols is only about - * one symbol in every 256. - */ +/* Compression initialization & termination */ + + +LOCAL(void) +clear_hash(gif_dest_ptr dinfo) +/* Fill the hash table with empty entries */ +{ + /* It's sufficient to zero hash_code[] */ + memset(dinfo->hash_code, 0, HSIZE * sizeof(code_int)); +} + + +LOCAL(void) +clear_block(gif_dest_ptr dinfo) +/* Reset compressor and issue a Clear code */ +{ + clear_hash(dinfo); /* delete all the symbols */ + dinfo->free_code = dinfo->ClearCode + 2; + output(dinfo, dinfo->ClearCode); /* inform decoder */ + dinfo->n_bits = dinfo->init_bits; /* reset code size */ + dinfo->maxcode = MAXCODE(dinfo->n_bits); +} + LOCAL(void) compress_init(gif_dest_ptr dinfo, int i_bits) -/* Initialize pseudo-compressor */ +/* Initialize compressor */ { /* init all the state variables */ - dinfo->n_bits = i_bits; + dinfo->n_bits = dinfo->init_bits = i_bits; dinfo->maxcode = MAXCODE(dinfo->n_bits); - dinfo->ClearCode = (1 << (i_bits - 1)); + dinfo->ClearCode = ((code_int) 1 << (i_bits - 1)); dinfo->EOFCode = dinfo->ClearCode + 1; - dinfo->code_counter = dinfo->ClearCode + 2; + dinfo->code_counter = dinfo->free_code = dinfo->ClearCode + 2; + dinfo->first_byte = TRUE; /* no waiting symbol yet */ /* init output buffering vars */ dinfo->bytesinpkt = 0; dinfo->cur_accum = 0; dinfo->cur_bits = 0; + /* clear hash table */ + if (dinfo->hash_code != NULL) + clear_hash(dinfo); /* GIF specifies an initial Clear code */ output(dinfo, dinfo->ClearCode); } -LOCAL(void) -compress_pixel(gif_dest_ptr dinfo, int c) -/* Accept and "compress" one pixel value. - * The given value must be less than n_bits wide. - */ -{ - /* Output the given pixel value as a symbol. */ - output(dinfo, c); - /* Issue Clear codes often enough to keep the reader from ratcheting up - * its symbol size. - */ - if (dinfo->code_counter < dinfo->maxcode) { - dinfo->code_counter++; - } else { - output(dinfo, dinfo->ClearCode); - dinfo->code_counter = dinfo->ClearCode + 2; /* reset the counter */ - } -} - - LOCAL(void) compress_term(gif_dest_ptr dinfo) /* Clean up at end */ { + /* Flush out the buffered LZW code */ + if (!dinfo->first_byte) + output(dinfo, dinfo->waiting_code); /* Send an EOF code */ output(dinfo, dinfo->EOFCode); /* Flush the bit-packing buffer */ @@ -221,7 +251,7 @@ put_3bytes(gif_dest_ptr dinfo, int val) LOCAL(void) emit_header(gif_dest_ptr dinfo, int num_colors, JSAMPARRAY colormap) /* Output the GIF file header, including color map */ -/* If colormap==NULL, synthesize a grayscale colormap */ +/* If colormap == NULL, synthesize a grayscale colormap */ { int BitsPerPixel, ColorMapSize, InitCodeSize, FlagByte; int cshift = dinfo->cinfo->data_precision - 8; @@ -265,12 +295,12 @@ emit_header(gif_dest_ptr dinfo, int num_colors, JSAMPARRAY colormap) if (colormap != NULL) { if (dinfo->cinfo->out_color_space == JCS_RGB) { /* Normal case: RGB color map */ - putc(GETJSAMPLE(colormap[0][i]) >> cshift, dinfo->pub.output_file); - putc(GETJSAMPLE(colormap[1][i]) >> cshift, dinfo->pub.output_file); - putc(GETJSAMPLE(colormap[2][i]) >> cshift, dinfo->pub.output_file); + putc(colormap[0][i] >> cshift, dinfo->pub.output_file); + putc(colormap[1][i] >> cshift, dinfo->pub.output_file); + putc(colormap[2][i] >> cshift, dinfo->pub.output_file); } else { /* Grayscale "color map": possible if quantizing grayscale image */ - put_3bytes(dinfo, GETJSAMPLE(colormap[0][i]) >> cshift); + put_3bytes(dinfo, colormap[0][i] >> cshift); } } else { /* Create a grayscale map of num_colors values, range 0..255 */ @@ -278,7 +308,7 @@ emit_header(gif_dest_ptr dinfo, int num_colors, JSAMPARRAY colormap) } } else { /* fill out the map to a power of 2 */ - put_3bytes(dinfo, 0); + put_3bytes(dinfo, CENTERJSAMPLE >> cshift); } } /* Write image separator and Image Descriptor */ @@ -292,7 +322,7 @@ emit_header(gif_dest_ptr dinfo, int num_colors, JSAMPARRAY colormap) /* Write Initial Code Size byte */ putc(InitCodeSize, dinfo->pub.output_file); - /* Initialize for "compression" of image data */ + /* Initialize for compression of image data */ compress_init(dinfo, InitCodeSize + 1); } @@ -318,17 +348,139 @@ start_output_gif(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) * In this module rows_supplied will always be 1. */ + +/* + * The LZW algorithm proper + */ + +METHODDEF(void) +put_LZW_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, + JDIMENSION rows_supplied) +{ + gif_dest_ptr dest = (gif_dest_ptr)dinfo; + register JSAMPROW ptr; + register JDIMENSION col; + code_int c; + register hash_int i; + register hash_int disp; + register hash_entry probe_value; + + ptr = dest->pub.buffer[0]; + for (col = cinfo->output_width; col > 0; col--) { + /* Accept and compress one 8-bit byte */ + c = (code_int)(*ptr++); + + if (dest->first_byte) { /* need to initialize waiting_code */ + dest->waiting_code = c; + dest->first_byte = FALSE; + continue; + } + + /* Probe hash table to see if a symbol exists for + * waiting_code followed by c. + * If so, replace waiting_code by that symbol and continue. + */ + i = ((hash_int)c << (MAX_LZW_BITS - 8)) + dest->waiting_code; + /* i is less than twice 2**MAX_LZW_BITS, therefore less than twice HSIZE */ + if (i >= HSIZE) + i -= HSIZE; + + probe_value = HASH_ENTRY(dest->waiting_code, c); + + if (dest->hash_code[i] == 0) { + /* hit empty slot; desired symbol not in table */ + output(dest, dest->waiting_code); + if (dest->free_code < LZW_TABLE_SIZE) { + dest->hash_code[i] = dest->free_code++; /* add symbol to hashtable */ + dest->hash_value[i] = probe_value; + } else + clear_block(dest); + dest->waiting_code = c; + continue; + } + if (dest->hash_value[i] == probe_value) { + dest->waiting_code = dest->hash_code[i]; + continue; + } + + if (i == 0) /* secondary hash (after G. Knott) */ + disp = 1; + else + disp = HSIZE - i; + for (;;) { + i -= disp; + if (i < 0) + i += HSIZE; + if (dest->hash_code[i] == 0) { + /* hit empty slot; desired symbol not in table */ + output(dest, dest->waiting_code); + if (dest->free_code < LZW_TABLE_SIZE) { + dest->hash_code[i] = dest->free_code++; /* add symbol to hashtable */ + dest->hash_value[i] = probe_value; + } else + clear_block(dest); + dest->waiting_code = c; + break; + } + if (dest->hash_value[i] == probe_value) { + dest->waiting_code = dest->hash_code[i]; + break; + } + } + } +} + + +/* + * The pseudo-compression algorithm. + * + * In this version we simply output each pixel value as a separate symbol; + * thus, no compression occurs. In fact, there is expansion of one bit per + * pixel, because we use a symbol width one bit wider than the pixel width. + * + * GIF ordinarily uses variable-width symbols, and the decoder will expect + * to ratchet up the symbol width after a fixed number of symbols. + * To simplify the logic and keep the expansion penalty down, we emit a + * GIF Clear code to reset the decoder just before the width would ratchet up. + * Thus, all the symbols in the output file will have the same bit width. + * Note that emitting the Clear codes at the right times is a mere matter of + * counting output symbols and is in no way dependent on the LZW algorithm. + * + * With a small basic pixel width (low color count), Clear codes will be + * needed very frequently, causing the file to expand even more. So this + * simplistic approach wouldn't work too well on bilevel images, for example. + * But for output of JPEG conversions the pixel width will usually be 8 bits + * (129 to 256 colors), so the overhead added by Clear symbols is only about + * one symbol in every 256. + */ + METHODDEF(void) -put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, - JDIMENSION rows_supplied) +put_raw_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, + JDIMENSION rows_supplied) { gif_dest_ptr dest = (gif_dest_ptr)dinfo; register JSAMPROW ptr; register JDIMENSION col; + code_int c; ptr = dest->pub.buffer[0]; for (col = cinfo->output_width; col > 0; col--) { - compress_pixel(dest, GETJSAMPLE(*ptr++)); + c = (code_int)(*ptr++); + /* Accept and output one pixel value. + * The given value must be less than n_bits wide. + */ + + /* Output the given pixel value as a symbol. */ + output(dest, c); + /* Issue Clear codes often enough to keep the reader from ratcheting up + * its symbol size. + */ + if (dest->code_counter < dest->maxcode) { + dest->code_counter++; + } else { + output(dest, dest->ClearCode); + dest->code_counter = dest->ClearCode + 2; /* reset the counter */ + } } } @@ -342,7 +494,7 @@ finish_output_gif(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) { gif_dest_ptr dest = (gif_dest_ptr)dinfo; - /* Flush "compression" mechanism */ + /* Flush compression mechanism */ compress_term(dest); /* Write a zero-length data block to end the series */ putc(0, dest->pub.output_file); @@ -370,7 +522,7 @@ calc_buffer_dimensions_gif(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) */ GLOBAL(djpeg_dest_ptr) -jinit_write_gif(j_decompress_ptr cinfo) +jinit_write_gif(j_decompress_ptr cinfo, boolean is_lzw) { gif_dest_ptr dest; @@ -380,7 +532,6 @@ jinit_write_gif(j_decompress_ptr cinfo) sizeof(gif_dest_struct)); dest->cinfo = cinfo; /* make back link for subroutines */ dest->pub.start_output = start_output_gif; - dest->pub.put_pixel_rows = put_pixel_rows; dest->pub.finish_output = finish_output_gif; dest->pub.calc_buffer_dimensions = calc_buffer_dimensions_gif; @@ -407,6 +558,22 @@ jinit_write_gif(j_decompress_ptr cinfo) ((j_common_ptr)cinfo, JPOOL_IMAGE, cinfo->output_width, (JDIMENSION)1); dest->pub.buffer_height = 1; + if (is_lzw) { + dest->pub.put_pixel_rows = put_LZW_pixel_rows; + /* Allocate space for hash table */ + dest->hash_code = (code_int *) + (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, + HSIZE * sizeof(code_int)); + dest->hash_value = (hash_entry *) + (*cinfo->mem->alloc_large) ((j_common_ptr)cinfo, JPOOL_IMAGE, + HSIZE * sizeof(hash_entry)); + } else { + dest->pub.put_pixel_rows = put_raw_pixel_rows; + /* Mark tables unused */ + dest->hash_code = NULL; + dest->hash_value = NULL; + } + return (djpeg_dest_ptr)dest; } diff --git a/third-party/mozjpeg/mozjpeg/wrjpgcom.c b/third-party/mozjpeg/mozjpeg/wrjpgcom.c index 8a4e74161e7..060925fe7a8 100644 --- a/third-party/mozjpeg/mozjpeg/wrjpgcom.c +++ b/third-party/mozjpeg/mozjpeg/wrjpgcom.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1994-1997, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2014, D. R. Commander. + * Copyright (C) 2014, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -14,12 +14,13 @@ * JPEG markers. */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_DEPRECATE +#endif + #define JPEG_CJPEG_DJPEG /* to get the command-line config symbols */ #include "jinclude.h" /* get auto-config symbols, */ -#ifndef HAVE_STDLIB_H /* should declare malloc() */ -extern void *malloc(); -#endif #include /* to declare isupper(), tolower() */ #ifdef USE_SETMODE #include /* to declare setmode()'s parameter macros */ @@ -27,16 +28,6 @@ extern void *malloc(); #include /* to declare setmode() */ #endif -#ifdef USE_CCOMMAND /* command-line reader for Macintosh */ -#ifdef __MWERKS__ -#include /* Metrowerks needs this */ -#include /* ... and this */ -#endif -#ifdef THINK_C -#include /* Think declares it here */ -#endif -#endif - #ifdef DONT_USE_B_MODE /* define mode parameters for fopen() */ #define READ_BINARY "r" #define WRITE_BINARY "w" @@ -414,11 +405,6 @@ main(int argc, char **argv) unsigned int comment_length = 0; int marker; - /* On Mac, fetch a command line. */ -#ifdef USE_CCOMMAND - argc = ccommand(&argv); -#endif - progname = argv[0]; if (progname == NULL || progname[0] == 0) progname = "wrjpgcom"; /* in case C library doesn't provide it */ diff --git a/third-party/mozjpeg/mozjpeg/wrppm.c b/third-party/mozjpeg/mozjpeg/wrppm.c index 69f91e816ae..57c8aaffacb 100644 --- a/third-party/mozjpeg/mozjpeg/wrppm.c +++ b/third-party/mozjpeg/mozjpeg/wrppm.c @@ -5,7 +5,7 @@ * Copyright (C) 1991-1996, Thomas G. Lane. * Modified 2009 by Guido Vollbeding. * libjpeg-turbo Modifications: - * Copyright (C) 2017, 2019, D. R. Commander. + * Copyright (C) 2017, 2019-2020, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -92,7 +92,7 @@ put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, { ppm_dest_ptr dest = (ppm_dest_ptr)dinfo; - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -108,20 +108,20 @@ copy_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, ppm_dest_ptr dest = (ppm_dest_ptr)dinfo; register char *bufferptr; register JSAMPROW ptr; -#if BITS_IN_JSAMPLE != 8 || (!defined(HAVE_UNSIGNED_CHAR) && !defined(__CHAR_UNSIGNED__)) +#if BITS_IN_JSAMPLE != 8 register JDIMENSION col; #endif ptr = dest->pub.buffer[0]; bufferptr = dest->iobuffer; -#if BITS_IN_JSAMPLE == 8 && (defined(HAVE_UNSIGNED_CHAR) || defined(__CHAR_UNSIGNED__)) - MEMCOPY(bufferptr, ptr, dest->samples_per_row); +#if BITS_IN_JSAMPLE == 8 + memcpy(bufferptr, ptr, dest->samples_per_row); #else for (col = dest->samples_per_row; col > 0; col--) { - PUTPPMSAMPLE(bufferptr, GETJSAMPLE(*ptr++)); + PUTPPMSAMPLE(bufferptr, *ptr++); } #endif - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -149,7 +149,7 @@ put_rgb(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, JDIMENSION rows_supplied) PUTPPMSAMPLE(bufferptr, ptr[bindex]); ptr += ps; } - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -175,7 +175,7 @@ put_cmyk(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, PUTPPMSAMPLE(bufferptr, g); PUTPPMSAMPLE(bufferptr, b); } - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -200,12 +200,12 @@ put_demapped_rgb(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, ptr = dest->pub.buffer[0]; bufferptr = dest->iobuffer; for (col = cinfo->output_width; col > 0; col--) { - pixval = GETJSAMPLE(*ptr++); - PUTPPMSAMPLE(bufferptr, GETJSAMPLE(color_map0[pixval])); - PUTPPMSAMPLE(bufferptr, GETJSAMPLE(color_map1[pixval])); - PUTPPMSAMPLE(bufferptr, GETJSAMPLE(color_map2[pixval])); + pixval = *ptr++; + PUTPPMSAMPLE(bufferptr, color_map0[pixval]); + PUTPPMSAMPLE(bufferptr, color_map1[pixval]); + PUTPPMSAMPLE(bufferptr, color_map2[pixval]); } - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -222,9 +222,9 @@ put_demapped_gray(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, ptr = dest->pub.buffer[0]; bufferptr = dest->iobuffer; for (col = cinfo->output_width; col > 0; col--) { - PUTPPMSAMPLE(bufferptr, GETJSAMPLE(color_map[GETJSAMPLE(*ptr++)])); + PUTPPMSAMPLE(bufferptr, color_map[*ptr++]); } - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -326,11 +326,12 @@ jinit_write_ppm(j_decompress_ptr cinfo) if (cinfo->quantize_colors || BITS_IN_JSAMPLE != 8 || sizeof(JSAMPLE) != sizeof(char) || - (cinfo->out_color_space != JCS_EXT_RGB #if RGB_RED == 0 && RGB_GREEN == 1 && RGB_BLUE == 2 && RGB_PIXELSIZE == 3 - && cinfo->out_color_space != JCS_RGB + (cinfo->out_color_space != JCS_EXT_RGB && + cinfo->out_color_space != JCS_RGB)) { +#else + cinfo->out_color_space != JCS_EXT_RGB) { #endif - )) { /* When quantizing, we need an output buffer for colormap indexes * that's separate from the physical I/O buffer. We also need a * separate buffer if pixel format translation must take place. diff --git a/third-party/mozjpeg/mozjpeg/wrrle.c b/third-party/mozjpeg/mozjpeg/wrrle.c deleted file mode 100644 index 5c98ec060ef..00000000000 --- a/third-party/mozjpeg/mozjpeg/wrrle.c +++ /dev/null @@ -1,309 +0,0 @@ -/* - * wrrle.c - * - * This file was part of the Independent JPEG Group's software: - * Copyright (C) 1991-1996, Thomas G. Lane. - * libjpeg-turbo Modifications: - * Copyright (C) 2017, D. R. Commander. - * For conditions of distribution and use, see the accompanying README.ijg - * file. - * - * This file contains routines to write output images in RLE format. - * The Utah Raster Toolkit library is required (version 3.1 or later). - * - * These routines may need modification for non-Unix environments or - * specialized applications. As they stand, they assume output to - * an ordinary stdio stream. - * - * Based on code contributed by Mike Lijewski, - * with updates from Robert Hutchinson. - */ - -#include "cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ - -#ifdef RLE_SUPPORTED - -/* rle.h is provided by the Utah Raster Toolkit. */ - -#include - -/* - * We assume that JSAMPLE has the same representation as rle_pixel, - * to wit, "unsigned char". Hence we can't cope with 12- or 16-bit samples. - */ - -#if BITS_IN_JSAMPLE != 8 - Sorry, this code only copes with 8-bit JSAMPLEs. /* deliberate syntax err */ -#endif - - -/* - * Since RLE stores scanlines bottom-to-top, we have to invert the image - * from JPEG's top-to-bottom order. To do this, we save the outgoing data - * in a virtual array during put_pixel_row calls, then actually emit the - * RLE file during finish_output. - */ - - -/* - * For now, if we emit an RLE color map then it is always 256 entries long, - * though not all of the entries need be used. - */ - -#define CMAPBITS 8 -#define CMAPLENGTH (1 << (CMAPBITS)) - -typedef struct { - struct djpeg_dest_struct pub; /* public fields */ - - jvirt_sarray_ptr image; /* virtual array to store the output image */ - rle_map *colormap; /* RLE-style color map, or NULL if none */ - rle_pixel **rle_row; /* To pass rows to rle_putrow() */ - -} rle_dest_struct; - -typedef rle_dest_struct *rle_dest_ptr; - -/* Forward declarations */ -METHODDEF(void) rle_put_pixel_rows(j_decompress_ptr cinfo, - djpeg_dest_ptr dinfo, - JDIMENSION rows_supplied); - - -/* - * Write the file header. - * - * In this module it's easier to wait till finish_output to write anything. - */ - -METHODDEF(void) -start_output_rle(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) -{ - rle_dest_ptr dest = (rle_dest_ptr)dinfo; - size_t cmapsize; - int i, ci; -#ifdef PROGRESS_REPORT - cd_progress_ptr progress = (cd_progress_ptr)cinfo->progress; -#endif - - /* - * Make sure the image can be stored in RLE format. - * - * - RLE stores image dimensions as *signed* 16 bit integers. JPEG - * uses unsigned, so we have to check the width. - * - * - Colorspace is expected to be grayscale or RGB. - * - * - The number of channels (components) is expected to be 1 (grayscale/ - * pseudocolor) or 3 (truecolor/directcolor). - * (could be 2 or 4 if using an alpha channel, but we aren't) - */ - - if (cinfo->output_width > 32767 || cinfo->output_height > 32767) - ERREXIT2(cinfo, JERR_RLE_DIMENSIONS, cinfo->output_width, - cinfo->output_height); - - if (cinfo->out_color_space != JCS_GRAYSCALE && - cinfo->out_color_space != JCS_RGB) - ERREXIT(cinfo, JERR_RLE_COLORSPACE); - - if (cinfo->output_components != 1 && cinfo->output_components != 3) - ERREXIT1(cinfo, JERR_RLE_TOOMANYCHANNELS, cinfo->num_components); - - /* Convert colormap, if any, to RLE format. */ - - dest->colormap = NULL; - - if (cinfo->quantize_colors) { - /* Allocate storage for RLE-style cmap, zero any extra entries */ - cmapsize = cinfo->out_color_components * CMAPLENGTH * sizeof(rle_map); - dest->colormap = (rle_map *)(*cinfo->mem->alloc_small) - ((j_common_ptr)cinfo, JPOOL_IMAGE, cmapsize); - MEMZERO(dest->colormap, cmapsize); - - /* Save away data in RLE format --- note 8-bit left shift! */ - /* Shifting would need adjustment for JSAMPLEs wider than 8 bits. */ - for (ci = 0; ci < cinfo->out_color_components; ci++) { - for (i = 0; i < cinfo->actual_number_of_colors; i++) { - dest->colormap[ci * CMAPLENGTH + i] = - GETJSAMPLE(cinfo->colormap[ci][i]) << 8; - } - } - } - - /* Set the output buffer to the first row */ - dest->pub.buffer = (*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, dest->image, (JDIMENSION)0, (JDIMENSION)1, TRUE); - dest->pub.buffer_height = 1; - - dest->pub.put_pixel_rows = rle_put_pixel_rows; - -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->total_extra_passes++; /* count file writing as separate pass */ - } -#endif -} - - -/* - * Write some pixel data. - * - * This routine just saves the data away in a virtual array. - */ - -METHODDEF(void) -rle_put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, - JDIMENSION rows_supplied) -{ - rle_dest_ptr dest = (rle_dest_ptr)dinfo; - - if (cinfo->output_scanline < cinfo->output_height) { - dest->pub.buffer = (*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, dest->image, - cinfo->output_scanline, (JDIMENSION)1, TRUE); - } -} - -/* - * Finish up at the end of the file. - * - * Here is where we really output the RLE file. - */ - -METHODDEF(void) -finish_output_rle(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) -{ - rle_dest_ptr dest = (rle_dest_ptr)dinfo; - rle_hdr header; /* Output file information */ - rle_pixel **rle_row, *red, *green, *blue; - JSAMPROW output_row; - char cmapcomment[80]; - int row, col; - int ci; -#ifdef PROGRESS_REPORT - cd_progress_ptr progress = (cd_progress_ptr)cinfo->progress; -#endif - - /* Initialize the header info */ - header = *rle_hdr_init(NULL); - header.rle_file = dest->pub.output_file; - header.xmin = 0; - header.xmax = cinfo->output_width - 1; - header.ymin = 0; - header.ymax = cinfo->output_height - 1; - header.alpha = 0; - header.ncolors = cinfo->output_components; - for (ci = 0; ci < cinfo->output_components; ci++) { - RLE_SET_BIT(header, ci); - } - if (cinfo->quantize_colors) { - header.ncmap = cinfo->out_color_components; - header.cmaplen = CMAPBITS; - header.cmap = dest->colormap; - /* Add a comment to the output image with the true colormap length. */ - sprintf(cmapcomment, "color_map_length=%d", - cinfo->actual_number_of_colors); - rle_putcom(cmapcomment, &header); - } - - /* Emit the RLE header and color map (if any) */ - rle_put_setup(&header); - - /* Now output the RLE data from our virtual array. - * We assume here that rle_pixel is represented the same as JSAMPLE. - */ - -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->pub.pass_limit = cinfo->output_height; - progress->pub.pass_counter = 0; - (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); - } -#endif - - if (cinfo->output_components == 1) { - for (row = cinfo->output_height - 1; row >= 0; row--) { - rle_row = (rle_pixel **)(*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, dest->image, - (JDIMENSION)row, (JDIMENSION)1, FALSE); - rle_putrow(rle_row, (int)cinfo->output_width, &header); -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->pub.pass_counter++; - (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); - } -#endif - } - } else { - for (row = cinfo->output_height - 1; row >= 0; row--) { - rle_row = (rle_pixel **)dest->rle_row; - output_row = *(*cinfo->mem->access_virt_sarray) - ((j_common_ptr)cinfo, dest->image, - (JDIMENSION)row, (JDIMENSION)1, FALSE); - red = rle_row[0]; - green = rle_row[1]; - blue = rle_row[2]; - for (col = cinfo->output_width; col > 0; col--) { - *red++ = GETJSAMPLE(*output_row++); - *green++ = GETJSAMPLE(*output_row++); - *blue++ = GETJSAMPLE(*output_row++); - } - rle_putrow(rle_row, (int)cinfo->output_width, &header); -#ifdef PROGRESS_REPORT - if (progress != NULL) { - progress->pub.pass_counter++; - (*progress->pub.progress_monitor) ((j_common_ptr)cinfo); - } -#endif - } - } - -#ifdef PROGRESS_REPORT - if (progress != NULL) - progress->completed_extra_passes++; -#endif - - /* Emit file trailer */ - rle_puteof(&header); - fflush(dest->pub.output_file); - if (ferror(dest->pub.output_file)) - ERREXIT(cinfo, JERR_FILE_WRITE); -} - - -/* - * The module selection routine for RLE format output. - */ - -GLOBAL(djpeg_dest_ptr) -jinit_write_rle(j_decompress_ptr cinfo) -{ - rle_dest_ptr dest; - - /* Create module interface object, fill in method pointers */ - dest = (rle_dest_ptr) - (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_IMAGE, - sizeof(rle_dest_struct)); - dest->pub.start_output = start_output_rle; - dest->pub.finish_output = finish_output_rle; - dest->pub.calc_buffer_dimensions = NULL; - - /* Calculate output image dimensions so we can allocate space */ - jpeg_calc_output_dimensions(cinfo); - - /* Allocate a work array for output to the RLE library. */ - dest->rle_row = (*cinfo->mem->alloc_sarray) - ((j_common_ptr)cinfo, JPOOL_IMAGE, - cinfo->output_width, (JDIMENSION)cinfo->output_components); - - /* Allocate a virtual array to hold the image. */ - dest->image = (*cinfo->mem->request_virt_sarray) - ((j_common_ptr)cinfo, JPOOL_IMAGE, FALSE, - (JDIMENSION)(cinfo->output_width * cinfo->output_components), - cinfo->output_height, (JDIMENSION)1); - - return (djpeg_dest_ptr)dest; -} - -#endif /* RLE_SUPPORTED */ diff --git a/third-party/mozjpeg/mozjpeg/wrtarga.c b/third-party/mozjpeg/mozjpeg/wrtarga.c index 9dfa9201936..67ca1f00a48 100644 --- a/third-party/mozjpeg/mozjpeg/wrtarga.c +++ b/third-party/mozjpeg/mozjpeg/wrtarga.c @@ -4,7 +4,7 @@ * This file was part of the Independent JPEG Group's software: * Copyright (C) 1991-1996, Thomas G. Lane. * libjpeg-turbo Modifications: - * Copyright (C) 2017, D. R. Commander. + * Copyright (C) 2017, 2019, 2022, D. R. Commander. * For conditions of distribution and use, see the accompanying README.ijg * file. * @@ -51,7 +51,7 @@ write_header(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, int num_colors) char targaheader[18]; /* Set unused fields of header to 0 */ - MEMZERO(targaheader, sizeof(targaheader)); + memset(targaheader, 0, sizeof(targaheader)); if (num_colors > 0) { targaheader[1] = 1; /* color map type 1 */ @@ -79,7 +79,7 @@ write_header(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, int num_colors) } } - if (JFWRITE(dinfo->output_file, targaheader, 18) != (size_t)18) + if (fwrite(targaheader, 1, 18, dinfo->output_file) != (size_t)18) ERREXIT(cinfo, JERR_FILE_WRITE); } @@ -102,12 +102,12 @@ put_pixel_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, inptr = dest->pub.buffer[0]; outptr = dest->iobuffer; for (col = cinfo->output_width; col > 0; col--) { - outptr[0] = (char)GETJSAMPLE(inptr[2]); /* RGB to BGR order */ - outptr[1] = (char)GETJSAMPLE(inptr[1]); - outptr[2] = (char)GETJSAMPLE(inptr[0]); + outptr[0] = inptr[2]; /* RGB to BGR order */ + outptr[1] = inptr[1]; + outptr[2] = inptr[0]; inptr += 3, outptr += 3; } - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } METHODDEF(void) @@ -118,14 +118,11 @@ put_gray_rows(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, tga_dest_ptr dest = (tga_dest_ptr)dinfo; register JSAMPROW inptr; register char *outptr; - register JDIMENSION col; inptr = dest->pub.buffer[0]; outptr = dest->iobuffer; - for (col = cinfo->output_width; col > 0; col--) { - *outptr++ = (char)GETJSAMPLE(*inptr++); - } - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + memcpy(outptr, inptr, cinfo->output_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -147,9 +144,9 @@ put_demapped_gray(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo, inptr = dest->pub.buffer[0]; outptr = dest->iobuffer; for (col = cinfo->output_width; col > 0; col--) { - *outptr++ = (char)GETJSAMPLE(color_map0[GETJSAMPLE(*inptr++)]); + *outptr++ = color_map0[*inptr++]; } - (void)JFWRITE(dest->pub.output_file, dest->iobuffer, dest->buffer_width); + fwrite(dest->iobuffer, 1, dest->buffer_width, dest->pub.output_file); } @@ -182,9 +179,9 @@ start_output_tga(j_decompress_ptr cinfo, djpeg_dest_ptr dinfo) /* Write the colormap. Note Targa uses BGR byte order */ outfile = dest->pub.output_file; for (i = 0; i < num_colors; i++) { - putc(GETJSAMPLE(cinfo->colormap[2][i]), outfile); - putc(GETJSAMPLE(cinfo->colormap[1][i]), outfile); - putc(GETJSAMPLE(cinfo->colormap[0][i]), outfile); + putc(cinfo->colormap[2][i], outfile); + putc(cinfo->colormap[1][i], outfile); + putc(cinfo->colormap[0][i], outfile); } dest->pub.put_pixel_rows = put_gray_rows; } else { diff --git a/third-party/rnnoise/Package.swift b/third-party/rnnoise/Package.swift index 7d0fb525f09..6cd01cbfe8e 100644 --- a/third-party/rnnoise/Package.swift +++ b/third-party/rnnoise/Package.swift @@ -36,7 +36,7 @@ func replaceSymbols() -> [String] { let package = Package( name: "rnoise", - platforms: [.macOS(.v10_12)], + 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/versions.json b/versions.json index 4d6adbdb1e2..6ca26fda6dc 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.4.4", + "app": "1.4.5", "bazel": "6.3.2", - "xcode": "15.0" + "xcode": "15.0.1" }