From d36e7e3a6efb4a7e22db15e3e098564806aa7fac Mon Sep 17 00:00:00 2001 From: Peter Iakovlev Date: Fri, 23 Feb 2018 20:28:31 +0400 Subject: [PATCH] no message --- .../IconExpandInput.imageset/Contents.json | 22 ++ .../StickersTabArrow@2x.png | Bin 0 -> 541 bytes .../StickersTabArrow@3x.png | Bin 0 -> 693 bytes .../Peek/Arrow.imageset/Contents.json | 21 ++ .../Peek/Arrow.imageset/PreviewUpArrow@2x.png | Bin 0 -> 1478 bytes Images.xcassets/Peek/Contents.json | 9 + TelegramUI.xcodeproj/project.pbxproj | 36 +++ TelegramUI/ActivityIndicator.swift | 18 +- .../ArhivedStickerPacksController.swift | 2 +- TelegramUI/AvatarGalleryController.swift | 94 +++++- TelegramUI/BotCheckoutControllerNode.swift | 2 +- TelegramUI/BotReceiptControllerNode.swift | 2 +- TelegramUI/CalculatingCacheSizeItem.swift | 4 +- TelegramUI/CallListCallItem.swift | 2 +- TelegramUI/ChannelInfoController.swift | 27 +- TelegramUI/ChatButtonKeyboardInputNode.swift | 14 +- .../ChatContextResultPeekContentNode.swift | 262 +++++++++++++++ TelegramUI/ChatController.swift | 269 ++++++++++++++-- TelegramUI/ChatControllerInteraction.swift | 5 +- TelegramUI/ChatControllerNode.swift | 197 ++++++++++-- TelegramUI/ChatDocumentGalleryItem.swift | 16 +- TelegramUI/ChatHistoryListNode.swift | 72 ++++- .../ChatHistorySearchContainerNode.swift | 4 +- TelegramUI/ChatHistoryViewForLocation.swift | 4 +- TelegramUI/ChatImageGalleryItem.swift | 38 +-- TelegramUI/ChatInputNode.swift | 2 +- .../ChatInterfaceStateContextMenus.swift | 86 +++-- TelegramUI/ChatListController.swift | 26 +- TelegramUI/ChatListItem.swift | 53 ++++ TelegramUI/ChatListNode.swift | 64 ++++ TelegramUI/ChatListTypingNode.swift | 29 +- TelegramUI/ChatListViewTransition.swift | 2 +- TelegramUI/ChatLoadingNode.swift | 2 +- TelegramUI/ChatMediaInputGifPane.swift | 27 +- .../ChatMediaInputMetaSectionItemNode.swift | 8 + TelegramUI/ChatMediaInputNode.swift | 297 +++++++++++++++--- TelegramUI/ChatMediaInputPane.swift | 13 + TelegramUI/ChatMediaInputRecentGifsItem.swift | 8 + TelegramUI/ChatMediaInputSettingsItem.swift | 7 + .../ChatMediaInputStickerPackItem.swift | 14 +- TelegramUI/ChatMediaInputStickerPane.swift | 39 ++- TelegramUI/ChatMediaInputTrendingItem.swift | 8 + TelegramUI/ChatMediaInputTrendingPane.swift | 184 ++++++++++- TelegramUI/ChatMessageActionItemNode.swift | 2 +- .../ChatMessageActionSheetController.swift | 30 ++ ...ChatMessageActionSheetControllerNode.swift | 153 +++++++++ .../ChatMessageAttachedContentNode.swift | 32 +- TelegramUI/ChatMessageBubbleContentNode.swift | 10 +- TelegramUI/ChatMessageBubbleImages.swift | 12 +- TelegramUI/ChatMessageBubbleItemNode.swift | 133 ++++++-- .../ChatMessageCallBubbleContentNode.swift | 10 +- .../ChatMessageContactBubbleContentNode.swift | 14 +- TelegramUI/ChatMessageDateAndStatusNode.swift | 20 ++ ...entLogPreviousDescriptionContentNode.swift | 6 +- ...ssageEventLogPreviousLinkContentNode.swift | 6 +- ...geEventLogPreviousMessageContentNode.swift | 6 +- .../ChatMessageFileBubbleContentNode.swift | 10 +- .../ChatMessageGameBubbleContentNode.swift | 6 +- .../ChatMessageInstantVideoItemNode.swift | 18 +- .../ChatMessageInteractiveFileNode.swift | 32 +- .../ChatMessageInteractiveMediaNode.swift | 2 +- .../ChatMessageInvoiceBubbleContentNode.swift | 6 +- TelegramUI/ChatMessageItem.swift | 42 ++- TelegramUI/ChatMessageItemView.swift | 15 +- .../ChatMessageMapBubbleContentNode.swift | 24 +- .../ChatMessageMediaBubbleContentNode.swift | 82 ++--- TelegramUI/ChatMessageStickerItemNode.swift | 20 +- .../ChatMessageTextBubbleContentNode.swift | 14 +- .../ChatMessageWebpageBubbleContentNode.swift | 8 +- .../ChatPanelInterfaceInteraction.swift | 8 +- .../ChatPinnedMessageTitlePanelNode.swift | 6 +- TelegramUI/ChatRecentActionsController.swift | 3 + .../ChatRecentActionsControllerNode.swift | 14 +- TelegramUI/ChatTextInputPanelNode.swift | 1 + TelegramUI/ChatTitleView.swift | 2 +- TelegramUI/ComponentsThemes.swift | 7 + TelegramUI/ComposeControllerNode.swift | 2 +- .../ContactMultiselectionControllerNode.swift | 6 +- .../ContactSelectionControllerNode.swift | 2 +- TelegramUI/ContactsControllerNode.swift | 2 +- .../DefaultDarkAccentPresentationTheme.swift | 3 +- TelegramUI/DefaultDarkPresentationTheme.swift | 3 +- TelegramUI/DefaultPresentationTheme.swift | 3 +- TelegramUI/EditAccessoryPanelNode.swift | 2 +- TelegramUI/EditSettingsController.swift | 2 +- .../FeaturedStickerPacksController.swift | 12 +- TelegramUI/FetchManager.swift | 2 + TelegramUI/FetchMediaUtils.swift | 1 - TelegramUI/FetchVideoMediaResource.swift | 73 ++++- TelegramUI/GalleryController.swift | 199 ++++++++---- TelegramUI/GalleryControllerNode.swift | 27 +- TelegramUI/GalleryItemNode.swift | 8 +- TelegramUI/GridMessageItem.swift | 7 +- TelegramUI/GroupInfoController.swift | 2 +- ...textResultsChatInputContextPanelNode.swift | 33 ++ ...ListContextResultsChatInputPanelItem.swift | 9 +- TelegramUI/InAppNotificationSettings.swift | 26 +- .../InstalledStickerPacksController.swift | 67 +++- TelegramUI/InstantImageGalleryItem.swift | 20 +- TelegramUI/InstantPageAudioNode.swift | 2 +- TelegramUI/InstantPageControllerNode.swift | 2 +- TelegramUI/InstantPageImageNode.swift | 7 +- TelegramUI/InstantPageLayout.swift | 8 +- TelegramUI/InstantPageNode.swift | 2 +- TelegramUI/InstantPagePeerReferenceNode.swift | 6 +- TelegramUI/InstantPagePlayableVideoNode.swift | 7 +- TelegramUI/InstantPageSlideshowItemNode.swift | 8 +- TelegramUI/InstantPageTheme.swift | 11 +- TelegramUI/InstantPageTileNode.swift | 2 +- TelegramUI/InstantPageWebEmbedNode.swift | 2 +- TelegramUI/ItemListActivityTextItem.swift | 4 +- TelegramUI/ItemListAvatarAndNameItem.swift | 7 +- TelegramUI/ItemListController.swift | 14 +- TelegramUI/ItemListControllerNode.swift | 9 + .../ItemListEditableReorderControlNode.swift | 39 +++ ...emListLoadingIndicatorEmptyStateItem.swift | 4 +- TelegramUI/ItemListStickerPackItem.swift | 65 +++- TelegramUI/ItemListSwitchItem.swift | 24 +- TelegramUI/LegacyController.swift | 22 +- TelegramUI/LegacyInstantVideoController.swift | 4 +- TelegramUI/LegacyMediaPickers.swift | 45 ++- TelegramUI/ListMessageFileItemNode.swift | 7 +- TelegramUI/ListMessageNode.swift | 2 +- TelegramUI/ListMessageSnippetItemNode.swift | 20 +- TelegramUI/ManagedAudioRecorder.swift | 20 +- TelegramUI/MediaInputPaneTrendingItem.swift | 287 +++++++++++++++++ TelegramUI/MediaManager.swift | 6 +- ...MediaNavigationAccessoryItemListNode.swift | 2 +- TelegramUI/MediaPlayerTimeTextNode.swift | 41 ++- TelegramUI/MediaResources.swift | 72 ++++- TelegramUI/MultiplexedVideoNode.swift | 37 ++- TelegramUI/NetworkStatusTitleView.swift | 11 +- TelegramUI/NotificationsAndSounds.swift | 32 +- TelegramUI/OngoingCallThreadLocalContext.mm | 32 +- TelegramUI/OpenChatMessage.swift | 295 ++++++++++------- TelegramUI/OpenResolvedUrl.swift | 13 +- TelegramUI/OpenUrl.swift | 201 +++++++++++- TelegramUI/OverlayMediaController.swift | 2 +- TelegramUI/OverlayPlayerControllerNode.swift | 2 +- TelegramUI/OverlayPlayerControlsNode.swift | 8 +- TelegramUI/PasscodeOptionsController.swift | 65 +++- TelegramUI/PeerAvatarImageGalleryItem.swift | 20 +- .../PeerMediaCollectionController.swift | 9 +- .../PeerMediaCollectionControllerNode.swift | 4 +- TelegramUI/PeerMediaCollectionEmptyNode.swift | 2 +- TelegramUI/PeerSelectionControllerNode.swift | 2 +- TelegramUI/PresentationResourceKey.swift | 3 + TelegramUI/PresentationResourcesChat.swift | 12 + .../PresentationResourcesItemList.swift | 13 + .../PresentationResourcesRootController.swift | 3 +- TelegramUI/PresentationStrings.swift | 9 + TelegramUI/PresentationTheme.swift | 4 +- TelegramUI/RaiseToListen.swift | 8 + TelegramUI/RaiseToListenActivator.h | 3 + TelegramUI/RaiseToListenActivator.m | 30 +- TelegramUI/RenderedTotalUnreadCount.swift | 26 ++ TelegramUI/ReplyAccessoryPanelNode.swift | 12 + TelegramUI/Resources/currencies.json | 2 +- TelegramUI/SearchDisplayController.swift | 4 +- TelegramUI/SelectablePeerNode.swift | 2 +- TelegramUI/SettingsController.swift | 25 +- TelegramUI/ShareActionButtonNode.swift | 16 +- TelegramUI/ShareControllerPeerGridItem.swift | 2 +- TelegramUI/ShareLoadingContainerNode.swift | 2 +- .../SoftwareVideoLayerFrameManager.swift | 2 +- TelegramUI/StickerPreviewControllerNode.swift | 4 - TelegramUI/StickerPreviewPeekContent.swift | 84 +++++ TelegramUI/StorageUsageController.swift | 25 +- .../StringForMessageTimestampStatus.swift | 29 ++ TelegramUI/StringWithAppliedEntities.swift | 15 + TelegramUI/TelegramApplicationContext.swift | 11 +- TelegramUI/TelegramController.swift | 10 + .../TelegramInitializeLegacyComponents.swift | 4 + TelegramUI/TextNode.swift | 4 + TelegramUI/ThemeGalleryItem.swift | 20 +- TelegramUI/ThemeSettingsChatPreviewItem.swift | 2 +- TelegramUI/UniversalVideoCalleryItem.swift | 38 +-- TelegramUI/UrlHandling.swift | 46 ++- TelegramUI/UserInfoController.swift | 3 +- .../ZoomableContentGalleryItemNode.swift | 8 + 180 files changed, 4220 insertions(+), 1001 deletions(-) create mode 100644 Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/StickersTabArrow@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/StickersTabArrow@3x.png create mode 100644 Images.xcassets/Peek/Arrow.imageset/Contents.json create mode 100644 Images.xcassets/Peek/Arrow.imageset/PreviewUpArrow@2x.png create mode 100644 Images.xcassets/Peek/Contents.json create mode 100644 TelegramUI/ChatContextResultPeekContentNode.swift create mode 100644 TelegramUI/ChatMediaInputPane.swift create mode 100644 TelegramUI/ChatMessageActionSheetController.swift create mode 100644 TelegramUI/ChatMessageActionSheetControllerNode.swift create mode 100644 TelegramUI/ItemListEditableReorderControlNode.swift create mode 100644 TelegramUI/MediaInputPaneTrendingItem.swift create mode 100644 TelegramUI/RenderedTotalUnreadCount.swift create mode 100644 TelegramUI/StickerPreviewPeekContent.swift create mode 100644 TelegramUI/StringForMessageTimestampStatus.swift diff --git a/Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/Contents.json new file mode 100644 index 0000000000..75089117ff --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StickersTabArrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "StickersTabArrow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/StickersTabArrow@2x.png b/Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/StickersTabArrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d2632791199a7139c42211b3d4db1025eb4cdfee GIT binary patch literal 541 zcmV+&0^OJxQ*k=rTA zM`)vbP!XPy3Gx7eoT^SFlgYMJVvqy5yjr;rO$Amr56%QSRcuyq)v5})>Iq|oY zvB@m{R>2@9V!OGJyCkh56t?sH_Uv`ef-@S+Jc4orKJZ11iq3tQwYA}GzZZ%huQ7HeWeIHI`_o6(#^asH1Ml$7wS_NYP&J`9C3@+A zQ{E(eL2u@}+NDl_UH! z4rfSy1skd*R5&_L8@_}BxCh?h8`UDAZM(X;dn?@I9n~-xqMIudU5x|xBnW~a2!bF8 fg3$1M8EpUnCcYL`wUX8^00000NkvXXu0mjfPLuSl literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/StickersTabArrow@3x.png b/Images.xcassets/Chat/Input/Text/IconExpandInput.imageset/StickersTabArrow@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..13786695d349ff5af40737c58d0ed057a84f6cbe GIT binary patch literal 693 zcmeAS@N?(olHy`uVBq!ia0vp^ejv=j3?z4!T+jznhXZ^ zwQnYnZ0n!X)<3g-!mQT*S@Y-52a0q|1PadT2Pv2ZWVBA42?VVZKx`12I2)+Gt#1}k zu(NM2&=8JM|nzL#bI|Bn_f~SjPNCo5DtIbJA90b@N*jwD*mb!eqm ze3bWtSwQH+?wGXh;F4eV5={*OGu&Qs#dWx>6zV>_v7+agi*kkP#3fN$yzQ4Xr6&3u z@KN)A8Y9Q|q>nZ!6vEA-TrR|!|Lr>qG}SbT9w$m+X|y?(o0 zccw9a+hxo3jeUd3i{NF~S+gw`y{Ta0o@}V<=4`RRN7Hup;VU(6Usp1@i=CeO`dZdr z1I=~4kBS(Vn@y8GkZV<6@q_vF<#+z~#QbyK`|oCRoIAHL^R2Xj$+u&!J6|^(kNxpR z_ppZiynEBWn}1oz!)Kf+W9;zkgrE7I$%aekp8qn_>S637aXlroP~*1_8zvf@_1baP zaBavQPUGHtwVnLJok8z^Yjm=>0K?03QVDPASK0gt$*rq^5y#-^>gTe~DWM4f&h1X4 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Peek/Arrow.imageset/Contents.json b/Images.xcassets/Peek/Arrow.imageset/Contents.json new file mode 100644 index 0000000000..329effa030 --- /dev/null +++ b/Images.xcassets/Peek/Arrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PreviewUpArrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Peek/Arrow.imageset/PreviewUpArrow@2x.png b/Images.xcassets/Peek/Arrow.imageset/PreviewUpArrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eb133303b0964b6831bfb4c7e9d7c7e064e2d113 GIT binary patch literal 1478 zcmeAS@N?(olHy`uVBq!ia0vp^Ev4 zq}24xJX@vryZ0+8WTx0Eg`4^s_!c;)W@LI)6{QAO`Gq7`WhYyvDB0U7*i={n4aiL` zNmQuF&B-gas<2f8n`;GRgM{^!6u?SKvTcwn`GuBNuFf>#!Gt)CP zF*P$Y)KM@pFf`IP03tJ8LlY}gGbUo-h6WQb!1OB;3-k^33_xCjDfIQluQWFouDZA+C>7yetOgf{R2HP_ z2c;J0mlh=hBQ8xDWL1Hcb5UwyNq$jCetr%t6azByOY(~|@(UE4gUu8)d=ry1^FRWc zU>&}`R-SpqC5d^-sh%#jN-xZPrm(>$o& z6x?nx!Kqgt=oo!a)FMSSObD2MKumbT1#;j?KQ#}S-iv?NF{?!~PSoLrbx3j`-ka(OR!u$4Pgk;7?a$0Mf> zo;3$N)Kn!5IHFhrC$PBsc1bLLnmlLWF`2L5YzveGkMBHZTl_xs*Wq;Y?{ohDER=ul z)3q$K@Bi2JLArBx^L&4JTR6WwD_7 z!&}d{#$W8M=1|{#F@;@8te)4`NJLrmh{7*}@(WCwj>cINmsPyckb9_i@0O*&$EJ1D z9OoFm6#ca9?&?_MBHZ^X<6Niqk2M+ZzVGNbhff=7vyd(q|$ z$3EWo9Ti0vyYj8BS!(H7>^$!MmG{+zI)>&On#|%K^o~WjvK_tsUH)-|Glze{H-Q!b nZ_8r>({8h@KXc%tdjlgw*DbYQ@hQ>kK_#oFtDnm{r-UW|z~voD literal 0 HcmV?d00001 diff --git a/Images.xcassets/Peek/Contents.json b/Images.xcassets/Peek/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Peek/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 90e16dc7a4..d4ef9335b2 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -65,6 +65,10 @@ D02F4AE91FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */; }; D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */; }; D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; }; + D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */; }; + D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */; }; + D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */; }; + D04203152037162700490EA5 /* MediaInputPaneTrendingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04203142037162700490EA5 /* MediaInputPaneTrendingItem.swift */; }; D04281ED200E3B28009DDE36 /* ItemListControllerSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281EC200E3B28009DDE36 /* ItemListControllerSearch.swift */; }; D04281EF200E3D88009DDE36 /* GroupInfoSearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281EE200E3D88009DDE36 /* GroupInfoSearchItem.swift */; }; D04281F1200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F0200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift */; }; @@ -97,6 +101,8 @@ D0477D1D1F617E8900412B44 /* NativeVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */; }; D0477D1F1F619E0700412B44 /* GalleryVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */; }; D0477D211F61A47600412B44 /* UniversalVideoContentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0477D201F61A47600412B44 /* UniversalVideoContentManager.swift */; }; + D048B339203C532800038D05 /* ChatMediaInputPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048B338203C532800038D05 /* ChatMediaInputPane.swift */; }; + D048B33B203C777500038D05 /* RenderedTotalUnreadCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048B33A203C777500038D05 /* RenderedTotalUnreadCount.swift */; }; D048EA851F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA841F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift */; }; D048EA871F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA861F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift */; }; D048EA891F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA881F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift */; }; @@ -202,6 +208,9 @@ D0B85C1E1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C1D1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift */; }; D0B85C211FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C201FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift */; }; D0B85C231FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C221FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift */; }; + D0BCC3D2203F0A6C008126C2 /* StringForMessageTimestampStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC3D1203F0A6C008126C2 /* StringForMessageTimestampStatus.swift */; }; + D0BCC3D420404CC7008126C2 /* ChatMessageActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC3D320404CC7008126C2 /* ChatMessageActionSheetController.swift */; }; + D0BCC3D620404CD8008126C2 /* ChatMessageActionSheetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC3D520404CD8008126C2 /* ChatMessageActionSheetControllerNode.swift */; }; D0BDB09B1F79C658002ABF2F /* SaveToCameraRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BDB09A1F79C658002ABF2F /* SaveToCameraRoll.swift */; }; D0C0B5901EDB505E000F4D2C /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */; }; D0C0B5921EDC5A3B000F4D2C /* LinkHighlightingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */; }; @@ -1024,6 +1033,9 @@ D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = ""; }; D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = ""; }; D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingCancelIndicator.swift; sourceTree = ""; }; + D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatContextResultPeekContentNode.swift; sourceTree = ""; }; + D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewPeekContent.swift; sourceTree = ""; }; + D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListEditableReorderControlNode.swift; sourceTree = ""; }; D03AADA81EAF931300D23738 /* PresentationResourcesChatList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesChatList.swift; sourceTree = ""; }; D03ADB471D703268005A521C /* ChatInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceState.swift; sourceTree = ""; }; D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = ""; }; @@ -1031,6 +1043,7 @@ D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = ""; }; D03E5E081E55C49C0029569A /* DebugAccountsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugAccountsController.swift; sourceTree = ""; }; D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelVisibilityController.swift; sourceTree = ""; }; + D04203142037162700490EA5 /* MediaInputPaneTrendingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInputPaneTrendingItem.swift; sourceTree = ""; }; D04281EC200E3B28009DDE36 /* ItemListControllerSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListControllerSearch.swift; sourceTree = ""; }; D04281EE200E3D88009DDE36 /* GroupInfoSearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoSearchItem.swift; sourceTree = ""; }; D04281F0200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoSearchNavigationContentNode.swift; sourceTree = ""; }; @@ -1070,6 +1083,8 @@ D0477D201F61A47600412B44 /* UniversalVideoContentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalVideoContentManager.swift; sourceTree = ""; }; D04791661E79A22000F18979 /* ItemListStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListStickerPackItem.swift; sourceTree = ""; }; D0486F091E523C8500091F0C /* GroupInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoController.swift; sourceTree = ""; }; + D048B338203C532800038D05 /* ChatMediaInputPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPane.swift; sourceTree = ""; }; + D048B33A203C777500038D05 /* RenderedTotalUnreadCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderedTotalUnreadCount.swift; sourceTree = ""; }; D048EA841F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsBacklightItemNode.swift; sourceTree = ""; }; D048EA861F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsFontSizeItemNode.swift; sourceTree = ""; }; D048EA881F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsFontFamilyItemNode.swift; sourceTree = ""; }; @@ -1380,6 +1395,9 @@ D0BC38691E3FB94D0044D6FE /* CreateGroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupController.swift; sourceTree = ""; }; D0BC387E1E40F1CF0044D6FE /* ContactSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactSelectionController.swift; sourceTree = ""; }; D0BC38801E40F1D80044D6FE /* ContactSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactSelectionControllerNode.swift; sourceTree = ""; }; + D0BCC3D1203F0A6C008126C2 /* StringForMessageTimestampStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringForMessageTimestampStatus.swift; sourceTree = ""; }; + D0BCC3D320404CC7008126C2 /* ChatMessageActionSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageActionSheetController.swift; sourceTree = ""; }; + D0BCC3D520404CD8008126C2 /* ChatMessageActionSheetControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageActionSheetControllerNode.swift; sourceTree = ""; }; D0BDB09A1F79C658002ABF2F /* SaveToCameraRoll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToCameraRoll.swift; sourceTree = ""; }; D0BE383B1E7C3E51000079AF /* StickerPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewController.swift; sourceTree = ""; }; D0BE931A1E92DFBA00DCC1E6 /* StickerPreviewControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewControllerNode.swift; sourceTree = ""; }; @@ -1967,6 +1985,7 @@ isa = PBXGroup; children = ( D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */, + D048B338203C532800038D05 /* ChatMediaInputPane.swift */, D0F02CCB1E96EF350065DEE2 /* ChatMediaInputStickerPane.swift */, D0F02CCD1E96FACE0065DEE2 /* ChatMediaInputGifPane.swift */, D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */, @@ -1985,6 +2004,7 @@ D002A0D81E9BEC8100A81812 /* SoftwareVideoLayerFrameManager.swift */, D002A0DA1E9C190700A81812 /* SoftwareVideoThumbnailLayer.swift */, D0575AEC1E9FF1AD006F2541 /* ChatMediaInputTrendingPane.swift */, + D04203142037162700490EA5 /* MediaInputPaneTrendingItem.swift */, ); name = Media; sourceTree = ""; @@ -2507,6 +2527,7 @@ D073D2DA1FB61DA9009E1DA2 /* CallListSettings.swift */, D09250031FE5363D003F693F /* ExperimentalSettings.swift */, D056CD711FF1569800880D28 /* MusicPlaybackSettings.swift */, + D048B33A203C777500038D05 /* RenderedTotalUnreadCount.swift */, ); name = Settings; sourceTree = ""; @@ -2878,6 +2899,7 @@ D0D7480E1E7B1BD600F4B1F6 /* StickerPackPreviewGridItem.swift */, D0BE383B1E7C3E51000079AF /* StickerPreviewController.swift */, D0BE931A1E92DFBA00DCC1E6 /* StickerPreviewControllerNode.swift */, + D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */, ); name = Stickers; sourceTree = ""; @@ -2922,6 +2944,7 @@ D0DF0CA21D82BCBC008AEB01 /* Mentions */, D0DC35481DE366B4000195EB /* Commands */, D0E35A041DE47FFE00BC6096 /* Context Request Results */, + D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */, ); name = "Input Context Panels"; sourceTree = ""; @@ -2982,6 +3005,7 @@ D021E0A81E3AACA200AF709C /* ItemListEditableItem.swift */, D021E0AA1E3B9E2700AF709C /* ItemListRevealOptionsNode.swift */, D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */, + D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */, D01C06B91FBBB076001561AB /* ItemListSelectableControlNode.swift */, D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */, D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */, @@ -3424,6 +3448,8 @@ D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */, D00ACA57202285090045D427 /* ChatRestrictedNode.swift */, D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */, + D0BCC3D320404CC7008126C2 /* ChatMessageActionSheetController.swift */, + D0BCC3D520404CD8008126C2 /* ChatMessageActionSheetControllerNode.swift */, D0F69E181D6B8AD10046BCD6 /* Items */, D03ADB461D703250005A521C /* Interface State */, D03ADB491D704427005A521C /* Accessory Panels */, @@ -3678,6 +3704,7 @@ D04ECD711FFBF22B00DE9029 /* OpenUrl.swift */, D0FC194C201F82A000FEDBB2 /* OpenResolvedUrl.swift */, D00ACA592022897D0045D427 /* ProcessedPeerRestrictionText.swift */, + D0BCC3D1203F0A6C008126C2 /* StringForMessageTimestampStatus.swift */, ); name = Utils; sourceTree = ""; @@ -4083,6 +4110,7 @@ D0A8BBA11F61EE83000F03FD /* UniversalVideoCalleryItem.swift in Sources */, D0EC6CDE1EB9F58800EBF1C3 /* ComponentsThemes.swift in Sources */, D0642EFC1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift in Sources */, + D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */, D01C06BC1FBBB0D8001561AB /* CheckNode.swift in Sources */, D0EC6CDF1EB9F58800EBF1C3 /* PresentationResourceKey.swift in Sources */, D0EC6CE01EB9F58800EBF1C3 /* PresentationResourcesRootController.swift in Sources */, @@ -4120,6 +4148,7 @@ D0EC6CF51EB9F58800EBF1C3 /* PeerMessageManagedMediaId.swift in Sources */, D0E9BA521F0559DA00F079A4 /* STPImageLibrary.m in Sources */, D0EC6CF61EB9F58800EBF1C3 /* ChatContextResultManagedMediaId.swift in Sources */, + D048B33B203C777500038D05 /* RenderedTotalUnreadCount.swift in Sources */, D04ECD721FFBF22B00DE9029 /* OpenUrl.swift in Sources */, D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */, D056CD7A1FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift in Sources */, @@ -4132,6 +4161,7 @@ D0EC6CFA1EB9F58800EBF1C3 /* ManagedAudioSession.swift in Sources */, D0EB5ADF1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift in Sources */, D0EC6CFB1EB9F58800EBF1C3 /* ManagedAudioRecorder.swift in Sources */, + D048B339203C532800038D05 /* ChatMediaInputPane.swift in Sources */, D0E817502012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift in Sources */, D0EC6CFC1EB9F58800EBF1C3 /* ManagedAudioPlaylistPlayer.swift in Sources */, D0EC6CFD1EB9F58800EBF1C3 /* AudioWaveform.swift in Sources */, @@ -4153,6 +4183,7 @@ D01847801FFBD12E00075256 /* ChatListPresentationData.swift in Sources */, D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */, D0FFF7F61F55B82500BEBC01 /* InstantPageAudioItem.swift in Sources */, + D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */, D0EC6D0C1EB9F58800EBF1C3 /* stream.c in Sources */, D0EC6D0D1EB9F58800EBF1C3 /* MediaFrameSource.swift in Sources */, D0EC6D0E1EB9F58800EBF1C3 /* MediaPlaybackData.swift in Sources */, @@ -4321,6 +4352,7 @@ D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */, D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */, D0EC6D791EB9F58800EBF1C3 /* ChatListTitleLockView.swift in Sources */, + D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */, D0EC6D7A1EB9F58800EBF1C3 /* ChatListSearchContainerNode.swift in Sources */, D0E9BACC1F05738600F079A4 /* STPAPIPostRequest.m in Sources */, D0EC6D7B1EB9F58800EBF1C3 /* ChatListRecentPeersListItem.swift in Sources */, @@ -4436,6 +4468,7 @@ D0EC6DC01EB9F58900EBF1C3 /* ChatMediaInputRecentGifsItem.swift in Sources */, D0477D211F61A47600412B44 /* UniversalVideoContentManager.swift in Sources */, D0EC6DC11EB9F58900EBF1C3 /* ChatMediaInputTrendingItem.swift in Sources */, + D0BCC3D620404CD8008126C2 /* ChatMessageActionSheetControllerNode.swift in Sources */, D0EC6DC21EB9F58900EBF1C3 /* ChatMediaInputStickerPackItem.swift in Sources */, D0EC6DC31EB9F58900EBF1C3 /* ChatMediaInputStickerGridItem.swift in Sources */, D0EC6DC41EB9F58900EBF1C3 /* MultiplexedSoftwareVideoNode.swift in Sources */, @@ -4456,6 +4489,7 @@ D0EC6DCD1EB9F58900EBF1C3 /* ChatInputContextPanelNode.swift in Sources */, D0F8C399201774AF00236FC5 /* FeedGroupingControllerNode.swift in Sources */, D0EC6DCE1EB9F58900EBF1C3 /* HorizontalStickersChatContextPanelNode.swift in Sources */, + D0BCC3D2203F0A6C008126C2 /* StringForMessageTimestampStatus.swift in Sources */, D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */, D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */, D0EC6DD11EB9F58900EBF1C3 /* HashtagChatInputPanelItem.swift in Sources */, @@ -4611,6 +4645,7 @@ D00ACA5A2022897D0045D427 /* ProcessedPeerRestrictionText.swift in Sources */, D0EC6E391EB9F58900EBF1C3 /* ItemListCheckboxItem.swift in Sources */, D0EC6E3A1EB9F58900EBF1C3 /* ItemListSwitchItem.swift in Sources */, + D04203152037162700490EA5 /* MediaInputPaneTrendingItem.swift in Sources */, D0EC6E3B1EB9F58900EBF1C3 /* ItemListPeerItem.swift in Sources */, D0EC6E3C1EB9F58900EBF1C3 /* ItemListPeerActionItem.swift in Sources */, D0EC6E3D1EB9F58900EBF1C3 /* ItemListMultilineInputItem.swift in Sources */, @@ -4728,6 +4763,7 @@ D0EC6E8C1EB9F58900EBF1C3 /* RingByteBuffer.swift in Sources */, D0E9BA181F05574500F079A4 /* STPPaymentCardTextFieldViewModel.m in Sources */, D0EC6E8D1EB9F58900EBF1C3 /* SecretChatKeyVisualization.m in Sources */, + D0BCC3D420404CC7008126C2 /* ChatMessageActionSheetController.swift in Sources */, D0EC6E8E1EB9F58900EBF1C3 /* NumberPluralizationForm.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/TelegramUI/ActivityIndicator.swift b/TelegramUI/ActivityIndicator.swift index 86245f47c5..3858c7b7f4 100644 --- a/TelegramUI/ActivityIndicator.swift +++ b/TelegramUI/ActivityIndicator.swift @@ -3,7 +3,7 @@ import AsyncDisplayKit enum ActivityIndicatorType: Equatable { case navigationAccent(PresentationTheme) - case custom(UIColor, CGFloat) + case custom(UIColor, CGFloat, CGFloat) static func ==(lhs: ActivityIndicatorType, rhs: ActivityIndicatorType) -> Bool { switch lhs { @@ -13,8 +13,8 @@ enum ActivityIndicatorType: Equatable { } else { return false } - case let .custom(lhsColor, lhsDiameter): - if case let .custom(rhsColor, rhsDiameter) = rhs, lhsColor.isEqual(rhsColor), lhsDiameter == rhsDiameter { + case let .custom(lhsColor, lhsDiameter, lhsWidth): + if case let .custom(rhsColor, rhsDiameter, rhsWidth) = rhs, lhsColor.isEqual(rhsColor), lhsDiameter == rhsDiameter, lhsWidth == rhsWidth { return true } else { return false @@ -34,8 +34,8 @@ final class ActivityIndicator: ASDisplayNode { switch type { case let .navigationAccent(theme): self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) - case let .custom(color, diameter): - self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter) + case let .custom(color, diameter, lineWidth): + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter, lineWidth: lineWidth) } } } @@ -56,8 +56,8 @@ final class ActivityIndicator: ASDisplayNode { switch type { case let .navigationAccent(theme): self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) - case let .custom(color, diameter): - self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter) + case let .custom(color, diameter, lineWidth): + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter, lineWidth: lineWidth) } super.init() @@ -96,7 +96,7 @@ final class ActivityIndicator: ASDisplayNode { switch self.type { case .navigationAccent: return CGSize(width: 22.0, height: 22.0) - case let .custom(_, diameter): + case let .custom(_, diameter, _): return CGSize(width: diameter, height: diameter) } } @@ -110,7 +110,7 @@ final class ActivityIndicator: ASDisplayNode { switch self.type { case .navigationAccent: indicatorSize = CGSize(width: 22.0, height: 22.0) - case let .custom(_, diameter): + case let .custom(_, diameter, _): indicatorSize = CGSize(width: diameter, height: diameter) } self.indicatorNode.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize) diff --git a/TelegramUI/ArhivedStickerPacksController.swift b/TelegramUI/ArhivedStickerPacksController.swift index 349da325d2..07beba5ac3 100644 --- a/TelegramUI/ArhivedStickerPacksController.swift +++ b/TelegramUI/ArhivedStickerPacksController.swift @@ -213,7 +213,7 @@ private func archivedStickerPacksControllerEntries(presentationData: Presentatio var index: Int32 = 0 for item in packs { if !installedIds.contains(item.info.id) { - entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, item.topItems.first, stringForStickerCount(item.info.count, strings: presentationData.strings), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, item.topItems.first, stringForStickerCount(item.info.count, strings: presentationData.strings), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id, reorderable: false))) index += 1 } } diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index 37b5e7f235..95ed47218c 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -47,9 +47,11 @@ enum AvatarGalleryEntry: Equatable { } final class AvatarGalleryControllerPresentationArguments { + let animated: Bool let transitionArguments: (AvatarGalleryEntry) -> GalleryTransitionArguments? - init(transitionArguments: @escaping (AvatarGalleryEntry) -> GalleryTransitionArguments?) { + init(animated: Bool = true, transitionArguments: @escaping (AvatarGalleryEntry) -> GalleryTransitionArguments?) { + self.animated = animated self.transitionArguments = transitionArguments } } @@ -104,6 +106,8 @@ class AvatarGalleryController: ViewController { } private var didSetReady = false + private var adjustedForInitialPreviewingLayout = false + private let disposable = MetaDisposable() private var entries: [AvatarGalleryEntry] = [] @@ -122,7 +126,7 @@ class AvatarGalleryController: ViewController { private let replaceRootController: (ViewController, ValuePromise?) -> Void - init(account: Account, peer: Peer, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { + init(account: Account, peer: Peer, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, synchronousLoad: Bool = false) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.replaceRootController = replaceRootController @@ -144,21 +148,60 @@ class AvatarGalleryController: ViewController { let entriesSignal: Signal<[AvatarGalleryEntry], NoError> = .single(initialAvatarGalleryEntries(peer: peer)) |> then(remoteEntriesSignal) let presentationData = self.presentationData - self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in - if let strongSelf = self { - strongSelf.entries = entries - strongSelf.centralEntryIndex = 0 - if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ PeerAvatarImageGalleryItem(account: account, strings: presentationData.strings, entry: $0) }), centralItemIndex: 0, keepFirst: true) - - let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in - strongSelf?.didSetReady = true + + let semaphore: DispatchSemaphore? + if synchronousLoad { + semaphore = DispatchSemaphore(value: 0) + } else { + semaphore = nil + } + + let syncResult = Atomic<(Bool, (() -> Void)?)>(value: (false, nil)) + + self.disposable.set(entriesSignal.start(next: { [weak self] entries in + let f: () -> Void = { + if let strongSelf = self { + strongSelf.entries = entries + strongSelf.centralEntryIndex = 0 + if strongSelf.isViewLoaded { + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ PeerAvatarImageGalleryItem(account: account, strings: presentationData.strings, entry: $0) }), centralItemIndex: 0, keepFirst: true) + + let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in + strongSelf?.didSetReady = true + } + strongSelf._ready.set(ready |> map { true }) } - strongSelf._ready.set(ready |> map { true }) + } + } + + var process = false + let _ = syncResult.modify { processed, _ in + if !processed { + return (processed, f) + } + process = true + return (true, nil) + } + semaphore?.signal() + if process { + Queue.mainQueue().async { + f() } } })) + if let semaphore = semaphore { + let _ = semaphore.wait(timeout: DispatchTime.now() + 1.0) + } + + var syncResultApply: (() -> Void)? + let _ = syncResult.modify { processed, f in + syncResultApply = f + return (true, nil) + } + + syncResultApply?() + self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in if let strongSelf = self { strongSelf.navigationItem.setTitle(title, animated: strongSelf.navigationItem.title?.isEmpty ?? true) @@ -282,6 +325,10 @@ class AvatarGalleryController: ViewController { self._ready.set(ready |> map { true }) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -295,13 +342,22 @@ class AvatarGalleryController: ViewController { if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) { nodeAnimatesItself = true - centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface) + if presentationArguments.animated { + centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface) + } self._hiddenMedia.set(.single(self.entries[centralItemNode.index])) } } - self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) + if !self.isPresentedInPreviewingContext() { + self.galleryNode.setControlsHidden(false, animated: false) + if let presentationArguments = self.presentationArguments as? AvatarGalleryControllerPresentationArguments { + if presentationArguments.animated { + self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) + } + } + } } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -309,5 +365,15 @@ class AvatarGalleryController: ViewController { self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + + if !self.adjustedForInitialPreviewingLayout && self.isPresentedInPreviewingContext() { + self.adjustedForInitialPreviewingLayout = true + self.galleryNode.setControlsHidden(true, animated: false) + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() { + self.preferredContentSize = itemSize.aspectFitted(self.view.bounds.size) + self.containerLayoutUpdated(ContainerViewLayout(size: self.preferredContentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + centralItemNode.activateAsInitial() + } + } } } diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index 3b96f8a975..dba42ef065 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -524,7 +524,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var updatedInsets = layout.intrinsicInsets updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 - super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarHeight, transition: transition) + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarHeight, transition: transition) let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) diff --git a/TelegramUI/BotReceiptControllerNode.swift b/TelegramUI/BotReceiptControllerNode.swift index 467a029530..6f812749b6 100644 --- a/TelegramUI/BotReceiptControllerNode.swift +++ b/TelegramUI/BotReceiptControllerNode.swift @@ -296,7 +296,7 @@ final class BotReceiptControllerNode: ItemListControllerNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var updatedInsets = layout.intrinsicInsets updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 - super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarHeight, transition: transition) + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarHeight, transition: transition) let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) diff --git a/TelegramUI/CalculatingCacheSizeItem.swift b/TelegramUI/CalculatingCacheSizeItem.swift index a8c11152fb..befa8746b4 100644 --- a/TelegramUI/CalculatingCacheSizeItem.swift +++ b/TelegramUI/CalculatingCacheSizeItem.swift @@ -128,7 +128,7 @@ class CalculatingCacheSizeItemNode: ListViewItemNode { if let current = strongSelf.activityIndicator { activityIndicator = current } else { - activityIndicator = ActivityIndicator(type: .custom(item.theme.list.itemAccentColor, 20.0), speed: ActivityIndicatorSpeed.slow) + activityIndicator = ActivityIndicator(type: .custom(item.theme.list.itemAccentColor, 20.0, 2.0), speed: ActivityIndicatorSpeed.slow) strongSelf.addSubnode(activityIndicator) } @@ -136,7 +136,7 @@ class CalculatingCacheSizeItemNode: ListViewItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - activityIndicator.type = .custom(item.theme.list.itemAccentColor, 20.0) + activityIndicator.type = .custom(item.theme.list.itemAccentColor, 20.0, 2.0) } let _ = titleApply() diff --git a/TelegramUI/CallListCallItem.swift b/TelegramUI/CallListCallItem.swift index b1486478fe..01b731e965 100644 --- a/TelegramUI/CallListCallItem.swift +++ b/TelegramUI/CallListCallItem.swift @@ -385,7 +385,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } if hasMissed { - statusAttributedString = NSAttributedString(string: item.strings.Calls_Missed, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.strings.Notification_CallMissedShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } else if hasIncoming && hasOutgoing { statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort + ", " + item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } else if hasIncoming { diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 2260f8c83a..491eff02c0 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -23,10 +23,10 @@ private final class ChannelInfoControllerArguments { let leaveChannel: () -> Void let deleteChannel: () -> Void let displayAddressNameContextMenu: (String) -> Void - let displayAboutContextMenu: (String) -> Void + let displayContextMenu: (ChannelInfoEntryTag, String) -> Void let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayContextMenu: @escaping (ChannelInfoEntryTag, String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.tapAvatarAction = tapAvatarAction @@ -44,7 +44,7 @@ private final class ChannelInfoControllerArguments { self.leaveChannel = leaveChannel self.deleteChannel = deleteChannel self.displayAddressNameContextMenu = displayAddressNameContextMenu - self.displayAboutContextMenu = displayAboutContextMenu + self.displayContextMenu = displayContextMenu self.aboutLinkAction = aboutLinkAction } } @@ -58,6 +58,7 @@ private enum ChannelInfoSection: ItemListSectionId { private enum ChannelInfoEntryTag { case about + case link } private enum ChannelInfoEntry: ItemListNodeEntry { @@ -260,14 +261,16 @@ private enum ChannelInfoEntry: ItemListNodeEntry { }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) case let .about(theme, text, value): return ItemListTextWithLabelItem(theme: theme, label: text, text: value, enabledEntitiyTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: { - arguments.displayAboutContextMenu(value) + arguments.displayContextMenu(ChannelInfoEntryTag.about, value) }, linkItemAction: { action, itemLink in arguments.aboutLinkAction(action, itemLink) }, tag: ChannelInfoEntryTag.about) case let .addressName(theme, text, value): return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { arguments.displayAddressNameContextMenu("https://t.me/\(value)") - }) + }, longTapAction: { + arguments.displayContextMenu(ChannelInfoEntryTag.link, "https://t.me/\(value)") + }, tag: ChannelInfoEntryTag.link) case let .channelPhotoSetup(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.changeProfilePhoto() @@ -537,7 +540,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? - var displayAboutContextMenuImpl: ((String) -> Void)? + var displayContextMenuImpl: ((ChannelInfoEntryTag, String) -> Void)? var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)? let arguments = ChannelInfoControllerArguments(account: account, avatarAndNameInfoContext: avatarAndNameInfoContext, tapAvatarAction: { @@ -741,8 +744,8 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }, displayAddressNameContextMenu: { text in let shareController = ShareController(account: account, subject: .url(text)) presentControllerImpl?(shareController, nil) - }, displayAboutContextMenu: { text in - displayAboutContextMenuImpl?(text) + }, displayContextMenu: { tag, text in + displayContextMenuImpl?(tag, text) }, aboutLinkAction: { action, itemLink in aboutLinkActionImpl?(action, itemLink) }) @@ -865,7 +868,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: (ASDisplayNode, CGRect)? + var result: ((ASDisplayNode, () -> UIView?), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() @@ -887,14 +890,14 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } } } - displayAboutContextMenuImpl = { [weak controller] text in + displayContextMenuImpl = { [weak controller] tag, text in if let strongController = controller { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } var resultItemNode: ListViewItemNode? let _ = strongController.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListTextWithLabelItemNode { - if let tag = itemNode.tag as? ChannelInfoEntryTag { - if tag == .about { + if let itemTag = itemNode.tag as? ChannelInfoEntryTag { + if itemTag == tag { resultItemNode = itemNode return true } diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index 467275a721..d8e5dd9e81 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -5,8 +5,6 @@ import Postbox import TelegramCore import SwiftSignalKit -private let defaultPortraitPanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 271.0 : 258.0 - private final class ChatButtonKeyboardInputButtonNode: ASButtonNode { var button: ReplyMarkupButton? @@ -66,11 +64,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } } - private func heightForWidth(width: CGFloat) -> CGFloat { - return defaultPortraitPanelHeight - } - - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) if self.theme !== interfaceState.theme { @@ -101,7 +95,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { let columnSpacing: CGFloat = 6.0 let rowSpacing: CGFloat = 5.0 - var panelHeight = self.heightForWidth(width: width) + var panelHeight = standardInputHeight let rowsHeight = verticalInset + CGFloat(markup.rows.count) * buttonHeight + CGFloat(max(0, markup.rows.count - 1)) * rowSpacing + verticalInset if !markup.flags.contains(.fit) && rowsHeight < panelHeight { @@ -142,13 +136,13 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } if markup.flags.contains(.fit) { - panelHeight = min(panelHeight, rowsHeight) + panelHeight = min(panelHeight, rowsHeight + bottomInset) } transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) self.scrollNode.view.contentSize = CGSize(width: width, height: rowsHeight) - return panelHeight + bottomInset + return panelHeight } else { return 0.0 } diff --git a/TelegramUI/ChatContextResultPeekContentNode.swift b/TelegramUI/ChatContextResultPeekContentNode.swift new file mode 100644 index 0000000000..ba4b5cf8dd --- /dev/null +++ b/TelegramUI/ChatContextResultPeekContentNode.swift @@ -0,0 +1,262 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +final class ChatContextResultPeekContent: PeekControllerContent { + let account: Account + let contextResult: ChatContextResult + let menu: [PeekControllerMenuItem] + + init(account: Account, contextResult: ChatContextResult, menu: [PeekControllerMenuItem]) { + self.account = account + self.contextResult = contextResult + self.menu = menu + } + + func presentation() -> PeekControllerContentPresentation { + return .contained + } + + func menuActivation() -> PeerkControllerMenuActivation { + return .drag + } + + func menuItems() -> [PeekControllerMenuItem] { + return self.menu + } + + func node() -> PeekControllerContentNode & ASDisplayNode { + return ChatContextResultPeekNode(account: self.account, contextResult: self.contextResult) + } +} + +private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerContentNode { + private let account: Account + private let contextResult: ChatContextResult + + private let imageNodeBackground: ASDisplayNode + private let imageNode: TransformImageNode + private var videoLayer: (SoftwareVideoThumbnailLayer, SoftwareVideoLayerFrameManager, SampleBufferLayer)? + + private var currentImageResource: TelegramMediaResource? + private var currentVideoFile: TelegramMediaFile? + + private let timebase: CMTimebase + + private var displayLink: CADisplayLink? + private var ticking: Bool = false { + didSet { + if self.ticking != oldValue { + if self.ticking { + class DisplayLinkProxy: NSObject { + weak var target: ChatContextResultPeekNode? + init(target: ChatContextResultPeekNode) { + self.target = target + } + + @objc func displayLinkEvent() { + self.target?.displayLinkEvent() + } + } + + let displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) + self.displayLink = displayLink + displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) + if #available(iOS 10.0, *) { + displayLink.preferredFramesPerSecond = 25 + } else { + displayLink.frameInterval = 2 + } + displayLink.isPaused = false + CMTimebaseSetRate(self.timebase, 1.0) + } else if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.isPaused = true + displayLink.invalidate() + CMTimebaseSetRate(self.timebase, 0.0) + } + } + } + } + + private func displayLinkEvent() { + let timestamp = CMTimebaseGetTime(self.timebase).seconds + self.videoLayer?.1.tick(timestamp: timestamp) + } + + init(account: Account, contextResult: ChatContextResult) { + self.account = account + self.contextResult = contextResult + + self.imageNodeBackground = ASDisplayNode() + self.imageNodeBackground.isLayerBacked = true + self.imageNodeBackground.backgroundColor = UIColor(white: 0.9, alpha: 1.0) + + self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + + var timebase: CMTimebase? + CMTimebaseCreateWithMasterClock(nil, CMClockGetHostTimeClock(), &timebase) + CMTimebaseSetRate(timebase!, 0.0) + self.timebase = timebase! + + super.init() + + self.addSubnode(self.imageNodeBackground) + + self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + self.addSubnode(self.imageNode) + } + + deinit { + if let displayLink = self.displayLink { + displayLink.isPaused = true + displayLink.invalidate() + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let imageLayout = self.imageNode.asyncLayout() + let currentImageResource = self.currentImageResource + let currentVideoFile = self.currentVideoFile + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + + var imageResource: TelegramMediaResource? + var videoFile: TelegramMediaFile? + var imageDimensions: CGSize? + switch self.contextResult { + case let .externalReference(_, type, title, _, url, thumbnailUrl, contentUrl, _, dimensions, _, _): + if let contentUrl = contentUrl { + imageResource = HttpReferenceMediaResource(url: contentUrl, size: nil) + } else if let thumbnailUrl = thumbnailUrl { + imageResource = HttpReferenceMediaResource(url: thumbnailUrl, size: nil) + } + imageDimensions = dimensions + if type == "gif", let contentUrl = contentUrl, let thumbnailResource = imageResource, let dimensions = dimensions { + videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), resource: HttpReferenceMediaResource(url: contentUrl, size: nil), previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) + imageResource = nil + } + case let .internalReference(_, _, title, _, image, file, _): + if let image = image { + if let largestRepresentation = largestImageRepresentation(image.representations) { + imageDimensions = largestRepresentation.dimensions + } + imageResource = imageRepresentationLargerThan(image.representations, size: CGSize(width: 200.0, height: 100.0))?.resource + } else if let file = file { + if let dimensions = file.dimensions { + imageDimensions = dimensions + } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { + imageDimensions = largestRepresentation.dimensions + } + imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource + } + + if let file = file { + if file.isVideo && file.isAnimated { + videoFile = file + imageResource = nil + } + } + } + + let fittedImageDimensions: CGSize + let croppedImageDimensions: CGSize + if let imageDimensions = imageDimensions { + fittedImageDimensions = imageDimensions.fitted(CGSize(width: size.width, height: size.height)) + } else { + fittedImageDimensions = CGSize(width: min(size.width, size.height), height: min(size.width, size.height)) + } + croppedImageDimensions = fittedImageDimensions + + var imageApply: (() -> Void)? + if let _ = imageResource { + let imageCorners = ImageCorners() + let arguments = TransformImageArguments(corners: imageCorners, imageSize: fittedImageDimensions, boundingSize: croppedImageDimensions, intrinsicInsets: UIEdgeInsets()) + imageApply = imageLayout(arguments) + } + + var updatedImageResource = false + if let currentImageResource = currentImageResource, let imageResource = imageResource { + if !currentImageResource.isEqual(to: imageResource) { + updatedImageResource = true + } + } else if (currentImageResource != nil) != (imageResource != nil) { + updatedImageResource = true + } + + var updatedVideoFile = false + if let currentVideoFile = currentVideoFile, let videoFile = videoFile { + if !currentVideoFile.isEqual(videoFile) { + updatedVideoFile = true + } + } else if (currentVideoFile != nil) != (videoFile != nil) { + updatedVideoFile = true + } + + if updatedImageResource { + if let imageResource = imageResource { + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) + //updateImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) + updateImageSignal = chatMessagePhoto(postbox: self.account.postbox, photo: tmpImage) + } else { + updateImageSignal = .complete() + } + } + + self.currentImageResource = imageResource + self.currentVideoFile = videoFile + + if let imageApply = imageApply { + if let updateImageSignal = updateImageSignal { + self.imageNode.setSignal(updateImageSignal) + } + + self.imageNode.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions) + self.imageNodeBackground.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions) + imageApply() + } + + if updatedVideoFile { + if let (thumbnailLayer, _, layer) = self.videoLayer { + self.videoLayer = nil + thumbnailLayer.removeFromSuperlayer() + layer.layer.removeFromSuperlayer() + } + + if let videoFile = videoFile { + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, file: videoFile) + self.layer.addSublayer(thumbnailLayer) + let layerHolder = takeSampleBufferLayer() + layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill + self.layer.addSublayer(layerHolder.layer) + let manager = SoftwareVideoLayerFrameManager(account: self.account, resource: videoFile.resource, layerHolder: layerHolder) + self.videoLayer = (thumbnailLayer, manager, layerHolder) + thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in + if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { + if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager { + manager.start() + } + } + } + } + } + + if let (thumbnailLayer, _, layer) = self.videoLayer { + thumbnailLayer.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions) + layer.layer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) + } + + if !self.ticking { + self.ticking = true + } + + return croppedImageDimensions + } +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 0d5f12e342..b190fa3f19 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -49,10 +49,11 @@ public enum NavigateToMessageLocation { } } -public final class ChatController: TelegramController { - private var containerLayout = ContainerViewLayout() +public final class ChatController: TelegramController, UIViewControllerPreviewingDelegate { + private var validLayout: ContainerViewLayout? public var peekActions: ChatControllerPeekActions = .standard + private var didSetup3dTouch: Bool = false private let account: Account public let chatLocation: ChatLocation @@ -195,7 +196,7 @@ public final class ChatController: TelegramController { self.scrollToTop = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.historyNode.scrollToStartOfHistory() + strongSelf.chatDisplayNode.historyNode.scrollScreenToTop() } } @@ -206,7 +207,7 @@ public final class ChatController: TelegramController { }, present: { c, a in self?.present(c, in: .window(.root), with: a) }, transitionNode: { messageId, media in - var selectedNode: ASDisplayNode? + var selectedNode: (ASDisplayNode, () -> UIView?)? if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { @@ -299,29 +300,45 @@ public final class ChatController: TelegramController { } let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, messages: updatedMessages, interfaceInteraction: strongSelf.interfaceInteraction, debugStreamSingleVideo: { id in self?.debugStreamSingleVideo(id) - }).start(next: { contextMenuController in - if let strongSelf = self, let contextMenuController = contextMenuController { - if let controllerInteraction = strongSelf.controllerInteraction { - controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: updatedMessages[0].stableId) - strongSelf.updateItemNodesHighlightedStates(animated: true) + }).start(next: { actions in + if let strongSelf = self, !actions.isEmpty { + var contextMenuController: ContextMenuController? + var contextActions: [ContextMenuAction] = [] + var sheetActions: [ChatMessageContextMenuSheetAction] = [] + for action in actions { + switch action { + case let .context(contextAction): + contextActions.append(contextAction) + case let .sheet(sheetAction): + sheetActions.append(sheetAction) + } } - contextMenuController.dismissed = { - if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - if controllerInteraction.highlightedState?.messageStableId == updatedMessages[0].stableId { - controllerInteraction.highlightedState = nil - strongSelf.updateItemNodesHighlightedStates(animated: true) - } + if !contextActions.isEmpty { + contextMenuController = ContextMenuController(actions: contextActions) + } + + contextMenuController?.dismissed = { + if let strongSelf = self { + //strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: nil, displayContextMenuController: nil) } } + strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: updatedMessages[0].stableId, sheetActions: sheetActions, displayContextMenuController: contextMenuController.flatMap { ($0, node, frame) }) + return + + /*if let controllerInteraction = strongSelf.controllerInteraction { + controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: updatedMessages[0].stableId) + strongSelf.updateItemNodesHighlightedStates(animated: true) + } + strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in if let node = node { return (node, frame) } else { return nil } - })) + }))*/ } }) } @@ -531,6 +548,8 @@ public final class ChatController: TelegramController { } }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) + }, presentGlobalOverlayController: { [weak self] controller, arguments in + self?.presentInGlobalOverlay(controller, with: arguments) }, callPeer: { [weak self] peerId in if let strongSelf = self { let callResult = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) @@ -997,6 +1016,7 @@ public final class ChatController: TelegramController { if let strongSelf = self, strongSelf.isNodeLoaded { if !value { strongSelf.saveInterfaceState() + strongSelf.raiseToListen?.applicationResignedActive() } } }) @@ -1303,10 +1323,10 @@ public final class ChatController: TelegramController { self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f in self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = { [weak self] transition in f() - if let strongSelf = self { + if let strongSelf = self, let validLayout = strongSelf.validLayout { var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? - strongSelf.chatDisplayNode.containerLayoutUpdated(strongSelf.containerLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets, _, _ in + strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets, _, _ in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) @@ -1568,6 +1588,19 @@ public final class ChatController: TelegramController { })) } } + }, deleteMessages: { [weak self] messages in + if let strongSelf = self, !messages.isEmpty { + let messageIds = Set(messages.map { $0.id }) + strongSelf.messageContextDisposable.set((chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { actions in + if let strongSelf = self, !actions.options.isEmpty { + if let banAuthor = actions.banAuthor { + strongSelf.presentBanMessageOptions(author: banAuthor, messageIds: messageIds, options: actions.options) + } else { + strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) + } + } + })) + } }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { @@ -1616,6 +1649,52 @@ public final class ChatController: TelegramController { strongSelf.present(controller, in: .window(.root)) } } + }, forwardMessages: { [weak self] messages in + if let strongSelf = self, !messages.isEmpty { + let forwardMessageIds = messages.map { $0.id }.sorted() + + let controller = PeerSelectionController(account: strongSelf.account) + controller.peerSelected = { [weak controller] peerId in + if let strongSelf = self, let strongController = controller { + if case .peer(peerId) = strongSelf.chatLocation { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) }) + strongController.dismiss() + } else if peerId == strongSelf.account.peerId { + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: forwardMessageIds.map { id -> EnqueueMessage in + return .forward(source: id, grouping: .auto) + }).start() + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + strongController.dismiss() + } else { + let _ = (strongSelf.account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedForwardMessageIds(forwardMessageIds) + } else { + return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds) + } + }) + }) |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + let ready = ValuePromise() + + strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in + if let strongController = controller { + strongController.dismiss() + } + })) + + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) + } + }) + } + } + } + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(controller, in: .window(.root)) + } }, shareSelectedMessages: { [weak self] in if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in @@ -1829,11 +1908,26 @@ public final class ChatController: TelegramController { } }, beginMediaRecording: { [weak self] isVideo in if let strongSelf = self { - if isVideo { - strongSelf.requestVideoRecorder() + let hasOngoingCall: Signal + if let signal = strongSelf.account.telegramApplicationContext.hasOngoingCall { + hasOngoingCall = signal } else { - strongSelf.requestAudioRecorder(beginWithTone: false) + hasOngoingCall = .single(false) } + let _ = (hasOngoingCall |> deliverOnMainQueue).start(next: { hasOngoingCall in + if let strongSelf = self { + if hasOngoingCall { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Call_CallInProgressTitle, text: strongSelf.presentationData.strings.Call_RecordingDisabledMessage, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } else { + if isVideo { + strongSelf.requestVideoRecorder() + } else { + strongSelf.requestAudioRecorder(beginWithTone: false) + } + } + } + }) } }, finishMediaRecording: { [weak self] action in self?.dismissMediaRecorder(action) @@ -2035,6 +2129,8 @@ public final class ChatController: TelegramController { } }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) + }, presentGlobalOverlayController: { [weak self] controller, arguments in + self?.presentInGlobalOverlay(controller, with: arguments) }, navigateFeed: { [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.scrollToNextMessage() @@ -2087,7 +2183,7 @@ public final class ChatController: TelegramController { switch self.chatLocation { case let .peer(peerId): - let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total]) + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total(.filtered)]) let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { @@ -2098,7 +2194,7 @@ public final class ChatController: TelegramController { if let count = view.count(for: .peer(peerId)) { unreadCount = count } - if let count = view.count(for: .total) { + if let count = view.count(for: .total(.filtered)) { totalCount = count } } @@ -2175,7 +2271,7 @@ public final class ChatController: TelegramController { } })) case let .group(groupId): - let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total]) + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(.filtered)]) //let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { @@ -2186,7 +2282,7 @@ public final class ChatController: TelegramController { if let count = view.count(for: .group(groupId)) { unreadCount = count } - if let count = view.count(for: .total) { + if let count = view.count(for: .total(.filtered)) { totalCount = count } } @@ -2264,6 +2360,11 @@ public final class ChatController: TelegramController { self?.deactivateRaiseGesture() }) self.raiseToListen?.enabled = self.canReadHistoryValue + self.tempVoicePlaylistEnded = { [weak self] in + if let strongSelf = self, let raiseToListen = strongSelf.raiseToListen { + raiseToListen.activateBasedOnProximity() + } + } } if let arguments = self.presentationArguments as? ChatControllerOverlayPresentationData { @@ -2272,6 +2373,17 @@ public final class ChatController: TelegramController { arguments.expandData.1() }) } + + if !self.didSetup3dTouch { + self.didSetup3dTouch = true + if #available(iOSApplicationExtension 9.0, *) { + self.registerForPreviewing(with: self, sourceView: self.chatDisplayNode.historyNodeContainer.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme), onlyNative: true) + if case .peer = self.chatLocation, let buttonView = (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view { + self.registerForPreviewing(with: self, sourceView: buttonView, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme), onlyNative: true) + } + self.registerForPreviewing(with: self, sourceView: self.chatDisplayNode.historyNodeContainer.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme), onlyNative: true) + } + } } override public func viewWillDisappear(_ animated: Bool) { @@ -2316,9 +2428,9 @@ public final class ChatController: TelegramController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.containerLayout = layout + self.validLayout = layout - self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in + self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop) }) } @@ -2692,7 +2804,7 @@ public final class ChatController: TelegramController { if let peer = strongSelf.presentationInterfaceState.peer { let _ = legacyAssetPicker(theme: strongSelf.presentationData.theme, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in if let strongSelf = self { - let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme) + let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBar.style.style let controller = generator(legacyController.context) legacyController.bind(controller: controller) @@ -2866,7 +2978,7 @@ public final class ChatController: TelegramController { self.audioRecorderFeedback = HapticFeedback() self.audioRecorderFeedback?.prepareTap() } - self.audioRecorder.set(applicationContext.mediaManager.audioRecorder(beginWithTone: beginWithTone, beganWithTone: { _ in + self.audioRecorder.set(applicationContext.mediaManager.audioRecorder(beginWithTone: beginWithTone, applicationBindings: applicationContext.applicationBindings, beganWithTone: { _ in })) } } @@ -3300,7 +3412,8 @@ public final class ChatController: TelegramController { return ChatInterfaceState().withUpdatedComposeInputState(textInputState) } }) - })).start(completed: { [weak self] in + }) + |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil)) } @@ -3498,6 +3611,94 @@ public final class ChatController: TelegramController { })) } + @available(iOSApplicationExtension 9.0, *) + public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { + if previewingContext.sourceView === (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view { + if let peer = self.presentationInterfaceState.peer { + let galleryController = AvatarGalleryController(account: self.account, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in + }, synchronousLoad: true) + galleryController.setHintWillBePresentedInPreviewingContext(true) + galleryController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + return galleryController + } + } else { + let historyPoint = previewingContext.sourceView.convert(location, to: self.chatDisplayNode.historyNode.view) + var result: (Message, Media)? + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if itemNode.frame.contains(historyPoint) { + if let value = itemNode.peekPreviewContent(at: self.chatDisplayNode.historyNode.view.convert(historyPoint, to: itemNode.view)) { + result = value + } + } + } + } + if let (message, media) = result { + var selectedTransitionNode: (ASDisplayNode, () -> UIView?)? + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: message.id, media: media) { + selectedTransitionNode = result + } + } + } + + if let selectedTransitionNode = selectedTransitionNode { + if let previewData = chatMessagePreviewControllerData(account: self.account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: self.navigationController as? NavigationController) { + switch previewData { + case let .gallery(gallery): + gallery.setHintWillBePresentedInPreviewingContext(true) + let rect = selectedTransitionNode.0.view.convert(selectedTransitionNode.0.bounds, to: previewingContext.sourceView) + previewingContext.sourceRect = rect.insetBy(dx: -2.0, dy: -2.0) + gallery.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + return gallery + case let .instantPage(gallery, centralIndex, galleryMedia): + break + } + } + } + } + } + return nil + } + + public func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { + if let gallery = viewControllerToCommit as? AvatarGalleryController { + self.chatDisplayNode.dismissInput() + gallery.setHintWillBePresentedInPreviewingContext(false) + self.present(gallery, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(animated: false, transitionArguments: { _ in + return nil + })) + } + if let gallery = viewControllerToCommit as? GalleryController { + self.chatDisplayNode.dismissInput() + gallery.setHintWillBePresentedInPreviewingContext(false) + + self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(animated: false, transitionArguments: { [weak self] messageId, media in + if let strongSelf = self { + var selectedTransitionNode: (ASDisplayNode, () -> UIView?)? + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: messageId, media: media) { + selectedTransitionNode = result + } + } + } + if let selectedTransitionNode = selectedTransitionNode { + return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) + } + }) + } + } + return nil + })) + } else if let gallery = viewControllerToCommit as? InstantPageGalleryController { + + } + } + @available(iOSApplicationExtension 9.0, *) override public var previewActionItems: [UIPreviewActionItem] { struct PreviewActionsData { @@ -3583,7 +3784,7 @@ public final class ChatController: TelegramController { self.chatDisplayNode.dismissInput() self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in if let strongSelf = self { - var transitionNode: ASDisplayNode? + var transitionNode: (ASDisplayNode, () -> UIView?)? strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { @@ -3719,7 +3920,11 @@ public final class ChatController: TelegramController { })) } if options.contains(.deleteLocally) { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak self, weak actionSheet] in + var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe + if case .peer(self.account.peerId) = self.chatLocation { + localOptionText = self.presentationData.strings.Conversation_Moderate_Delete + } + items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 73a0bfe987..7b2a48c7c1 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -59,6 +59,7 @@ public final class ChatControllerInteraction { let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void let openMessageShareMenu: (MessageId) -> Void let presentController: (ViewController, Any?) -> Void + let presentGlobalOverlayController: (ViewController, Any?) -> Void let callPeer: (PeerId) -> Void let longTap: (ChatControllerInteractionLongTapAction) -> Void let openCheckoutOrReceipt: (MessageId) -> Void @@ -71,9 +72,10 @@ public final class ChatControllerInteraction { var hiddenMedia: [MessageId: [Media]] = [:] var selectionState: ChatInterfaceSelectionState? var highlightedState: ChatInterfaceHighlightedState? + var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - public init(openMessage: @escaping (Message) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping () -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + public init(openMessage: @escaping (Message) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping () -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview @@ -96,6 +98,7 @@ public final class ChatControllerInteraction { self.updateInputState = updateInputState self.openMessageShareMenu = openMessageShareMenu self.presentController = presentController + self.presentGlobalOverlayController = presentGlobalOverlayController self.callPeer = callPeer self.longTap = longTap self.openCheckoutOrReceipt = openCheckoutOrReceipt diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index b9b124b368..214e5bd767 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -7,10 +7,18 @@ import TelegramCore private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider { var inputAccessoryHeight: (() -> CGFloat)? + var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? func getWindowInputAccessoryHeight() -> CGFloat { return self.inputAccessoryHeight?() ?? 0.0 } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.hitTestImpl?(point, event) { + return result + } + return super.hitTest(point, with: event) + } } class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { @@ -28,10 +36,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let backgroundNode: ASDisplayNode let historyNode: ChatHistoryListNode + let historyNodeContainer: ASDisplayNode let loadingNode: ChatLoadingNode var restrictedNode: ChatRecentActionsEmptyNode? - private var validLayout: ContainerViewLayout? + private var validLayout: (ContainerViewLayout, CGFloat)? private var searchNavigationNode: ChatSearchNavigationContentNode? @@ -88,6 +97,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var interfaceInteraction: ChatPanelInterfaceInteraction? + private var messageActionSheetController: (ChatMessageActionSheetController, UInt32)? + private var messageActionSheetControllerAdditionalInset: CGFloat? + private var messageActionSheetTopDimNode: ASDisplayNode? + private var messageActionSheetBottomDimNode: ASDisplayNode? + private var containerLayoutAndNavigationBarHeight: (ContainerViewLayout, CGFloat)? private var scheduledLayoutTransitionRequestId: Int = 0 @@ -97,7 +111,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { didSet { if self.isLoading != oldValue { if self.isLoading { - self.historyNode.supernode?.insertSubnode(self.loadingNode, aboveSubnode: self.historyNode) + self.historyNodeContainer.supernode?.insertSubnode(self.loadingNode, aboveSubnode: self.historyNodeContainer) } else { self.loadingNode.removeFromSupernode() } @@ -123,6 +137,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.titleAccessoryPanelContainer.clipsToBounds = true self.historyNode = ChatHistoryListNode(account: account, chatLocation: chatLocation, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get()) + self.historyNodeContainer = ASDisplayNode() + self.historyNodeContainer.addSubnode(self.historyNode) self.loadingNode = ChatLoadingNode(theme: chatPresentationInterfaceState.theme) self.inputPanelBackgroundNode = ASDisplayNode() @@ -149,6 +165,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + (self.view as? ChatControllerNodeView)?.hitTestImpl = { [weak self] point, event in + return self?.hitTest(point, with: event) + } + assert(Queue.mainQueue().isCurrent()) self.historyNode.setLoadStateUpdated { [weak self] loadState in @@ -164,7 +184,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.backgroundNode.contents = chatControllerBackgroundImage(wallpaper: chatPresentationInterfaceState.chatWallpaper, postbox: account.postbox)?.cgImage self.addSubnode(self.backgroundNode) - self.addSubnode(self.historyNode) + self.addSubnode(self.historyNodeContainer) self.addSubnode(self.titleAccessoryPanelContainer) self.addSubnode(self.inputPanelBackgroundNode) @@ -301,7 +321,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { containerNode.clipsToBounds = true containerNode.cornerRadius = 15.0 containerNode.addSubnode(self.backgroundNode) - containerNode.addSubnode(self.historyNode) + containerNode.addSubnode(self.historyNodeContainer) if let restrictedNode = self.restrictedNode { containerNode.addSubnode(restrictedNode) } @@ -329,9 +349,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.containerNode = nil containerNode.removeFromSupernode() self.insertSubnode(self.backgroundNode, at: 0) - self.insertSubnode(self.historyNode, aboveSubnode: self.backgroundNode) + self.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode) if let restrictedNode = self.restrictedNode { - self.insertSubnode(restrictedNode, aboveSubnode: self.historyNode) + self.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) } self.navigationBar.isHidden = false } @@ -342,7 +362,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var dismissedInputByDragging = false - if let validLayout = self.validLayout { + if let (validLayout, _) = self.validLayout { var wasDragging = false if validLayout.inputHeight != nil && validLayout.inputHeightIsInteractivellyChanging { wasDragging = true @@ -353,7 +373,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } } - self.validLayout = layout + self.validLayout = (layout, navigationBarHeight) let cleanInsets = layout.intrinsicInsets @@ -421,6 +441,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { dismissedInputNode = self.inputNode self.inputNode = inputNode inputNode.alpha = 1.0 + inputNode.layer.removeAnimation(forKey: "opacity") immediatelyLayoutInputNodeAndAnimateAppearance = true if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { self.insertSubnode(inputNode, aboveSubnode: inputPanelNode) @@ -428,7 +449,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) } } - inputNodeHeight = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + inputNodeHeight = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode self.inputNode = nil @@ -485,7 +506,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { - let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 56.0))) @@ -519,6 +540,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } transition.updateFrame(node: self.backgroundNode, frame: contentBounds) + transition.updateFrame(node: self.historyNodeContainer, frame: contentBounds) transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.midX, y: contentBounds.midY)) @@ -684,7 +706,65 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { listInsets.right += layout.safeInsets.right } - listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, duration: duration, curve: listViewCurve), additionalScrollDistance, scrollToTop) + var ensureTopInsetForOverlayHighlightedItems: CGFloat? + if let (controller, _) = self.messageActionSheetController { + let menuHeight = controller.controllerNode.updateLayout(layout: layout, transition: transition) + ensureTopInsetForOverlayHighlightedItems = menuHeight + + let topInset = listInsets.bottom + UIScreenPixel + let topFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: max(0.0, topInset))) + let bottomInset = containerInsets.bottom + inputPanelsHeight + UIScreenPixel + let bottomFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset), size: CGSize(width: layout.size.width, height: max(0.0, bottomInset - (layout.inputHeight ?? 0.0)))) + + let messageActionSheetTopDimNode: ASDisplayNode + if let current = self.messageActionSheetTopDimNode { + messageActionSheetTopDimNode = current + transition.updateFrame(node: messageActionSheetTopDimNode, frame: topFrame) + } else { + messageActionSheetTopDimNode = ASDisplayNode() + messageActionSheetTopDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + messageActionSheetTopDimNode.alpha = 0.0 + messageActionSheetTopDimNode.isLayerBacked = true + self.messageActionSheetTopDimNode = messageActionSheetTopDimNode + self.addSubnode(messageActionSheetTopDimNode) + transition.updateAlpha(node: messageActionSheetTopDimNode, alpha: 1.0) + messageActionSheetTopDimNode.frame = topFrame + } + + let messageActionSheetBottomDimNode: ASDisplayNode + if let current = self.messageActionSheetBottomDimNode { + messageActionSheetBottomDimNode = current + transition.updateFrame(node: messageActionSheetBottomDimNode, frame: bottomFrame) + } else { + messageActionSheetBottomDimNode = ASDisplayNode() + messageActionSheetBottomDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + messageActionSheetBottomDimNode.alpha = 0.0 + messageActionSheetBottomDimNode.isLayerBacked = true + self.messageActionSheetBottomDimNode = messageActionSheetBottomDimNode + self.addSubnode(messageActionSheetBottomDimNode) + transition.updateAlpha(node: messageActionSheetBottomDimNode, alpha: 1.0) + messageActionSheetBottomDimNode.frame = bottomFrame + } + } else { + if let messageActionSheetTopDimNode = self.messageActionSheetTopDimNode { + self.messageActionSheetTopDimNode = nil + transition.updateAlpha(node: messageActionSheetTopDimNode, alpha: 0.0, completion: { [weak messageActionSheetTopDimNode] _ in + messageActionSheetTopDimNode?.removeFromSupernode() + }) + } + if let messageActionSheetBottomDimNode = self.messageActionSheetBottomDimNode { + self.messageActionSheetBottomDimNode = nil + transition.updateAlpha(node: messageActionSheetBottomDimNode, alpha: 0.0, completion: { [weak messageActionSheetBottomDimNode] _ in + messageActionSheetBottomDimNode?.removeFromSupernode() + }) + } + } + + if let messageActionSheetControllerAdditionalInset = self.messageActionSheetControllerAdditionalInset { + listInsets.top = listInsets.top + messageActionSheetControllerAdditionalInset + } + + listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, duration: duration, curve: listViewCurve, ensureTopInsetForOverlayHighlightedItems: ensureTopInsetForOverlayHighlightedItems), additionalScrollDistance, scrollToTop) let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) @@ -960,16 +1040,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let peer = chatPresentationInterfaceState.peer, let restrictionText = peer.restrictionText { if self.restrictedNode == nil { let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme) - self.historyNode.supernode?.insertSubnode(restrictedNode, aboveSubnode: self.historyNode) + self.historyNodeContainer.supernode?.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) self.restrictedNode = restrictedNode } self.restrictedNode?.setup(title: "", text: processedPeerRestrictionText(restrictionText)) - self.historyNode.isHidden = true + self.historyNodeContainer.isHidden = true self.navigateButtons.isHidden = true } else if let restrictedNode = self.restrictedNode { self.restrictedNode = nil restrictedNode.removeFromSupernode() - self.historyNode.isHidden = false + self.historyNodeContainer.isHidden = false self.navigateButtons.isHidden = false } @@ -1083,8 +1163,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) inputNode.interfaceInteraction = interfaceInteraction self.inputMediaNode = inputNode - if let validLayout = self.validLayout { - let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + if let (validLayout, _) = self.validLayout { + let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } } } @@ -1158,7 +1238,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.animateFrame(node: containerNode, from: fromFrame) transition.animatePositionAdditive(node: self.backgroundNode, offset: -containerNode.bounds.size.height) - transition.animatePositionAdditive(node: self.historyNode, offset: -containerNode.bounds.size.height) + transition.animatePositionAdditive(node: self.historyNodeContainer, offset: -containerNode.bounds.size.height) transition.updateFrame(node: fromNode, frame: CGRect(origin: containerNode.frame.origin, size: fromNode.frame.size)) } @@ -1197,7 +1277,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { func animateDismissAsOverlay(completion: @escaping () -> Void) { if let containerNode = self.containerNode { self.dismissedAsOverlay = true - self.dismissAsOverlayLayout = self.validLayout + self.dismissAsOverlayLayout = self.validLayout?.0 self.backgroundEffectNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.27, removeOnCompletion: false) @@ -1246,4 +1326,85 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } } + + func displayMessageActionSheet(stableId: UInt32?, sheetActions: [ChatMessageContextMenuSheetAction]?, displayContextMenuController: (ContextMenuController, ASDisplayNode, CGRect)?) { + self.controllerInteraction.contextHighlightedState = stableId.flatMap { ChatInterfaceHighlightedState(messageStableId: $0) } + self.updateItemNodesContextHighlightedStates(animated: true, sheetActions: sheetActions, displayContextMenuController: displayContextMenuController) + } + + private func updateItemNodesContextHighlightedStates(animated: Bool, sheetActions: [ChatMessageContextMenuSheetAction]?, displayContextMenuController: (ContextMenuController, ASDisplayNode, CGRect)?) { + self.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHighlightedState(animated: animated) + } + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + var animateIn = false + + self.historyNode.updateNodeHighlightsAnimated(animated) + if self.messageActionSheetController?.1 != self.controllerInteraction.contextHighlightedState?.messageStableId { + if let (controller, _) = self.messageActionSheetController { + controller.controllerNode.animateOut(transition: transition, completion: { [weak controller] in + controller?.dismiss() + }) + self.messageActionSheetController = nil + self.messageActionSheetControllerAdditionalInset = nil + } + if let stableId = self.controllerInteraction.contextHighlightedState?.messageStableId { + let controller = ChatMessageActionSheetController(theme: self.chatPresentationInterfaceState.theme, actions: sheetActions ?? [], dismissed: { [weak self] in + self?.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) + }) + self.messageActionSheetController = (controller, stableId) + self.controllerInteraction.presentGlobalOverlayController(controller, nil) + animateIn = true + } + } + + if let (layout, navigationBarHeight) = self.validLayout { + let menuHeight = self.messageActionSheetController?.0.controllerNode.updateLayout(layout: layout, transition: .immediate) + if let stableId = self.messageActionSheetController?.1 { + var resultItemNode: ListViewItemNode? + self.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if itemNode.item?.message.stableId == stableId { + resultItemNode = itemNode + } + } + } + if let resultItemNode = resultItemNode, let menuHeight = menuHeight { + if resultItemNode.frame.size.height < self.historyNode.bounds.size.height - self.historyNode.insets.top - self.historyNode.insets.bottom { + if resultItemNode.frame.minY < menuHeight { + messageActionSheetControllerAdditionalInset = menuHeight - resultItemNode.frame.minY + } + //self.historyNode.insets.top + } + } + //messageActionSheetControllerAdditionalInset + } + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in + self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop) + }) + if animateIn, let controller = self.messageActionSheetController?.0 { + controller.controllerNode.animateIn(transition: transition) + } + if let _ = menuHeight { + if let _ = self.controllerInteraction.contextHighlightedState?.messageStableId, let (menuController, node, frame) = displayContextMenuController { + + self.controllerInteraction.presentController(menuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { + return (node, frame) + })) + } + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let _ = self.messageActionSheetController { + self.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) + return self.navigationBar.view + } + + return nil + } } diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index c4bc9c0bd8..b14cc4ba13 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -230,9 +230,9 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { return self._title.get() } - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.webView) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.webView.superview) + override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.webView) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.webView.superview) self.webView.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.webView.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) @@ -246,17 +246,17 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.webView) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.webView.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.webView) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.webView.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewInitialFrame = self.webView.convert(self.webView.bounds, to: self.view) var positionCompleted = false var boundsCompleted = false var copyCompleted = false - let copyView = node.view.snapshotContentTree()! + let copyView = node.1()! self.view.insertSubview(copyView, belowSubview: self.webView) copyView.frame = transformedSelfFrame diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 590b11e578..5f81ce1236 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -361,7 +361,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { additionalData.append(.cachedPeerDataMessages(peerId)) additionalData.append(.peerNotificationSettings(peerId)) } - additionalData.append(.totalUnreadCount) + additionalData.append(.totalUnreadState) let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged @@ -523,6 +523,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let visible = displayedRange.visibleRange { let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex) + var readIndexRange = (0, historyView.filteredEntries.count - 1 - visible.firstIndex) + if !visible.firstIndexFullyVisible { + readIndexRange.1 -= 1 + } + var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] for i in (indexRange.0 ... indexRange.1) { @@ -539,7 +544,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } for attribute in message.attributes { if attribute is ViewCountMessageAttribute { - messageIdsWithViewCount.append(message.id) + if message.id.namespace == Namespaces.Message.Cloud { + messageIdsWithViewCount.append(message.id) + } } else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { hasUnsonsumedContent = true } @@ -560,7 +567,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } for attribute in message.attributes { if attribute is ViewCountMessageAttribute { - messageIdsWithViewCount.append(message.id) + if message.id.namespace == Namespaces.Message.Cloud { + messageIdsWithViewCount.append(message.id) + } } else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { hasUnsonsumedContent = true } @@ -582,15 +591,17 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) } - let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView.filteredEntries, indexRange: indexRange) - - if let maxIncomingIndex = maxIncomingIndex { - strongSelf.updateMaxVisibleReadIncomingMessageIndex(maxIncomingIndex) - } - - if let maxOverallIndex = maxOverallIndex, maxOverallIndex != strongSelf.maxVisibleMessageIndexReported { - strongSelf.maxVisibleMessageIndexReported = maxOverallIndex - strongSelf.maxVisibleMessageIndexUpdated?(maxOverallIndex) + if readIndexRange.0 <= readIndexRange.1 { + let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView.filteredEntries, indexRange: readIndexRange) + + if let maxIncomingIndex = maxIncomingIndex { + strongSelf.updateMaxVisibleReadIncomingMessageIndex(maxIncomingIndex) + } + + if let maxOverallIndex = maxOverallIndex, maxOverallIndex != strongSelf.maxVisibleMessageIndexReported { + strongSelf.maxVisibleMessageIndexReported = maxOverallIndex + strongSelf.maxVisibleMessageIndexUpdated?(maxOverallIndex) + } } } @@ -673,6 +684,31 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.loadStateUpdated = f } + public func scrollScreenToTop() { + var currentMessage: Message? + if let historyView = self.historyView { + if let visibleRange = self.displayedItemRange.loadedRange { + var index = historyView.filteredEntries.count - 1 + loop: for entry in historyView.filteredEntries { + if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { + if case let .MessageEntry(message, _, _, _, _) = entry { + currentMessage = message + break loop + } else if case let .MessageGroupEntry(_, messages, _) = entry { + currentMessage = messages.first?.0 + break loop + } + } + index -= 1 + } + } + } + + if let currentMessage = currentMessage { + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .message(MessageIndex(currentMessage)), anchorIndex: .message(MessageIndex(currentMessage)), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true)) + } + } + public func scrollToStartOfHistory() { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true)) } @@ -850,9 +886,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index) if let visible = visibleRange.visibleRange { - let (messageIndex, _) = maxMessageIndexForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) - if let messageIndex = messageIndex { - strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex) + var visibleFirstIndex = visible.firstIndex + if !visible.firstIndexFullyVisible { + visibleFirstIndex += 1 + } + if visibleFirstIndex <= visible.lastIndex { + let (messageIndex, _) = maxMessageIndexForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex)) + if let messageIndex = messageIndex { + strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex) + } } } } diff --git a/TelegramUI/ChatHistorySearchContainerNode.swift b/TelegramUI/ChatHistorySearchContainerNode.swift index b0b1d3648f..b7b3b9bf5a 100644 --- a/TelegramUI/ChatHistorySearchContainerNode.swift +++ b/TelegramUI/ChatHistorySearchContainerNode.swift @@ -281,8 +281,8 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { } } - func transitionNodeForGallery(messageId: MessageId, media: Media) -> ASDisplayNode? { - var transitionNode: ASDisplayNode? + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { + var transitionNode: (ASDisplayNode, () -> UIView?)? self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 00d74293fa..5229b6061b 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -195,12 +195,12 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL if case .peer(peerIdValue) = chatLocation { cachedDataMessages = value } - case let .totalUnreadCount(totalUnreadCount): + case let .totalUnreadState(totalUnreadState): switch chatLocation { case let .peer(peerId): if let combinedReadStates = view.combinedReadStates { if case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId] { - readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) + readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadState.filteredCounters.messageCount, notificationSettings: notificationSettings) } } case .group: diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index dc4366d48b..3644aaa726 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -203,21 +203,21 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { })) } - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) - let surfaceCopyView = node.view.snapshotContentTree()! - let copyView = node.view.snapshotContentTree()! + let surfaceCopyView = node.1()! + let copyView = node.1()! addToTransitionSurface(surfaceCopyView) var transformedSurfaceFrame: CGRect? var transformedSurfaceFinalFrame: CGRect? if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.view.convert(node.view.bounds, to: contentSurface) + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) transformedSurfaceFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: contentSurface) } @@ -232,52 +232,54 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak copyView] _ in + let positionDuration: Double = 0.21 + + copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: positionDuration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak copyView] _ in copyView?.removeFromSuperview() }) let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height) copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame { - surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: positionDuration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in surfaceCopyView?.removeFromSuperview() }) let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFinalFrame.size.height / transformedSurfaceFrame.size.height) surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) } - self.imageNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.imageNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.imageNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.imageNode.layer.position, duration: positionDuration, timingFunction: kCAMediaTimingFunctionSpring) self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) transformedFrame.origin = CGPoint() self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: positionDuration, timingFunction: kCAMediaTimingFunctionSpring) self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { self.fetchDisposable.set(nil) - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) var positionCompleted = false var boundsCompleted = false var copyCompleted = false - let copyView = node.view.snapshotContentTree()! - let surfaceCopyView = node.view.snapshotContentTree()! + let copyView = node.1()! + let surfaceCopyView = node.1()! addToTransitionSurface(surfaceCopyView) var transformedSurfaceFrame: CGRect? var transformedSurfaceCopyViewInitialFrame: CGRect? if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.view.convert(node.view.bounds, to: contentSurface) + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) transformedSurfaceCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: contentSurface) } diff --git a/TelegramUI/ChatInputNode.swift b/TelegramUI/ChatInputNode.swift index ccdd70af9d..f78dd429fd 100644 --- a/TelegramUI/ChatInputNode.swift +++ b/TelegramUI/ChatInputNode.swift @@ -5,7 +5,7 @@ import AsyncDisplayKit class ChatInputNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { return 0.0 } } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 235d70b1dc..1ae6eedf5c 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -12,6 +12,7 @@ private struct MessageContextMenuData { let canPin: Bool let canEdit: Bool let resourceStatus: MediaResourceStatus? + let messageActions: ChatAvailableMessageActions } private let starIconEmpty = UIImage(bundleImageName: "Chat/Context Menu/StarIconEmpty")?.precomposed() @@ -47,9 +48,25 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS return canReply } -func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, messages: [Message], interfaceInteraction: ChatPanelInterfaceInteraction?, debugStreamSingleVideo: @escaping (MessageId) -> Void) -> Signal { +enum ChatMessageContextMenuActionColor { + case accent + case destructive +} + +struct ChatMessageContextMenuSheetAction { + let color: ChatMessageContextMenuActionColor + let title: String + let action: () -> Void +} + +enum ChatMessageContextMenuAction { + case context(ContextMenuAction) + case sheet(ChatMessageContextMenuSheetAction) +} + +func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, messages: [Message], interfaceInteraction: ChatPanelInterfaceInteraction?, debugStreamSingleVideo: @escaping (MessageId) -> Void) -> Signal<[ChatMessageContextMenuAction], NoError> { guard let interfaceInteraction = interfaceInteraction else { - return .single(nil) + return .single([]) } let dataSignal: Signal @@ -142,7 +159,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if !hasUneditableAttributes { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - if message.timestamp >= timestamp - 60 * 60 * 24 * 2 { + if message.timestamp >= timestamp - 60 * 60 * 24 * 2 || message.id.peerId == account.peerId { canEdit = true } } @@ -172,30 +189,30 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: |> map(Optional.init) } - dataSignal = combineLatest(loadStickerSaveStatusSignal, loadResourceStatusSignal) - |> map { stickerSaveStatus, resourceStatus -> MessageContextMenuData in - return MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, resourceStatus: resourceStatus) + dataSignal = combineLatest(loadStickerSaveStatusSignal, loadResourceStatusSignal, chatAvailableMessageActions(postbox: account.postbox, accountPeerId: account.peerId, messageIds: Set(messages.map { $0.id }))) + |> map { stickerSaveStatus, resourceStatus, messageActions -> MessageContextMenuData in + return MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, resourceStatus: resourceStatus, messageActions: messageActions) } - return dataSignal |> deliverOnMainQueue |> map { data -> ContextMenuController? in - var actions: [ContextMenuAction] = [] + return dataSignal |> deliverOnMainQueue |> map { data -> [ChatMessageContextMenuAction] in + var actions: [ChatMessageContextMenuAction] = [] if let starStatus = data.starStatus, let image = starStatus ? starIconFilled : starIconEmpty { - actions.append(ContextMenuAction(content: .icon(image), action: { + actions.append(.context(ContextMenuAction(content: .icon(image), action: { interfaceInteraction.toggleMessageStickerStarred(messages[0].id) - })) + }))) } if data.canReply { - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuReply), action: { + actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuReply), action: { interfaceInteraction.setupReplyMessage(messages[0].id) - })) + }))) } if data.canEdit { - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Edit), action: { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_Edit, action: { interfaceInteraction.setupEditMessage(messages[0].id) - })) + }))) } let resourceAvailable: Bool @@ -207,7 +224,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if !messages[0].text.isEmpty || resourceAvailable { let message = messages[0] - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), action: { + actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), action: { if resourceAvailable { for media in message.media { if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { @@ -237,18 +254,18 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } else { UIPasteboard.general.string = message.text } - })) + }))) } if data.canPin { if chatPresentationInterfaceState.pinnedMessage?.id != messages[0].id { - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Pin), action: { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_Pin, action: { interfaceInteraction.pinMessage(messages[0].id) - })) + }))) } else { - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Unpin), action: { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_Unpin, action: { interfaceInteraction.unpinMessage() - })) + }))) } } @@ -259,29 +276,36 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if let file = media as? TelegramMediaFile { if file.isVideo { if file.isAnimated { - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_LinkDialogSave), action: { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_LinkDialogSave, action: { let _ = addSavedGif(postbox: account.postbox, file: file).start() - })) + }))) } else if !GlobalExperimentalSettings.isAppStoreBuild { - actions.append(ContextMenuAction(content: .text("Stream"), action: { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: "Stream", action: { debugStreamSingleVideo(message.id) - })) + }))) } break } } } } - actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuMore), action: { + actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuMore), action: { interfaceInteraction.beginMessageSelection(messages.map { $0.id }) - })) + }))) - if !actions.isEmpty { - let contextMenuController = ContextMenuController(actions: actions) - return contextMenuController - } else { - return nil + if data.messageActions.options.contains(.forward) { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, action: { + interfaceInteraction.forwardMessages(messages) + }))) } + + if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .destructive, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, action: { + interfaceInteraction.deleteMessages(messages) + }))) + } + + return actions } } diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 7e868cb664..d7237998d0 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -82,12 +82,8 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } }) - self.badgeDisposable = (account.postbox.unreadMessageCountsView(items: [.total]) |> deliverOnMainQueue).start(next: { [weak self] view in + self.badgeDisposable = (renderedTotalUnreadCount(postbox: account.postbox) |> deliverOnMainQueue).start(next: { [weak self] count in if let strongSelf = self { - var count: Int32 = 0 - if let total = view.count(for: .total) { - count = total - } if count == 0 { strongSelf.tabBarItem.badgeValue = "" } else { @@ -332,7 +328,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD if !self.didSetup3dTouch { self.didSetup3dTouch = true if #available(iOSApplicationExtension 9.0, *) { - self.registerForPreviewing(with: self, sourceView: self.view) + self.registerForPreviewing(with: self, sourceView: self.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme), onlyNative: false) } } } @@ -424,7 +420,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } }) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatController } else if let messageId = action as? MessageId { if #available(iOSApplicationExtension 9.0, *) { @@ -435,13 +431,23 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD let chatController = ChatController(account: self.account, chatLocation: .peer(messageId.peerId), messageId: messageId) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatController } } return nil } + var isEditing = false + self.chatListDisplayNode.chatListNode.updateState { state in + isEditing = state.editing + return state + } + + if isEditing { + return nil + } + let listLocation = self.view.convert(location, to: self.chatListDisplayNode.chatListNode.view) var selectedNode: ChatListItemNode? @@ -460,11 +466,11 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD case let .peer(_, peer, _, _, _, _, _): let chatController = ChatController(account: self.account, chatLocation: .peer(peer.peerId)) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatController case let .groupReference(groupId, _, _, _): let chatListController = ChatListController(account: self.account, groupId: groupId, controlsHistoryPreload: false) - chatListController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) + chatListController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatListController } } else { diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index cfc93ff431..45a6efb66c 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -205,6 +205,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let mutedIconNode: ASImageNode var editableControlNode: ItemListEditableControlNode? + var reorderControlNode: ItemListEditableReorderControlNode? var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams)? @@ -367,6 +368,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout() let badgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let currentItem = self.layoutParams?.0 @@ -446,12 +448,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var currentSecretIconImage: UIImage? var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var reorderControlSizeAndApply: (CGSize, () -> ItemListEditableReorderControlNode)? let editingOffset: CGFloat + var reorderInset: CGFloat = 0.0 if item.editing { let sizeAndApply = editableControlLayout(itemHeight, item.presentationData.theme, isPeerGroup) editableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0.width + + if item.index.pinningIndex != nil { + let sizeAndApply = reorderControlLayout(itemHeight, item.presentationData.theme) + reorderControlSizeAndApply = sizeAndApply + reorderInset = sizeAndApply.0.width + } } else { editingOffset = 0.0 } @@ -617,6 +627,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + badgeSize = max(badgeSize, reorderInset) + let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: hideAuthor ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) @@ -713,6 +725,34 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { }) } + if let reorderControlSizeAndApply = reorderControlSizeAndApply { + if strongSelf.reorderControlNode == nil { + let reorderControlNode = reorderControlSizeAndApply.1() + strongSelf.reorderControlNode = reorderControlNode + strongSelf.addSubnode(reorderControlNode) + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0.width, y: 0.0), size: reorderControlSizeAndApply.0) + reorderControlNode.frame = reorderControlFrame + reorderControlNode.alpha = 0.0 + transition.updateAlpha(node: reorderControlNode, alpha: 1.0) + + transition.updateAlpha(node: strongSelf.dateNode, alpha: 0.0) + transition.updateAlpha(node: strongSelf.badgeTextNode, alpha: 0.0) + transition.updateAlpha(node: strongSelf.badgeBackgroundNode, alpha: 0.0) + transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 0.0) + transition.updateAlpha(node: strongSelf.statusNode, alpha: 0.0) + } + } else if let reorderControlNode = strongSelf.reorderControlNode { + strongSelf.reorderControlNode = nil + transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in + reorderControlNode?.removeFromSupernode() + }) + transition.updateAlpha(node: strongSelf.dateNode, alpha: 1.0) + transition.updateAlpha(node: strongSelf.badgeTextNode, alpha: 1.0) + transition.updateAlpha(node: strongSelf.badgeBackgroundNode, alpha: 1.0) + transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 1.0) + transition.updateAlpha(node: strongSelf.statusNode, alpha: 1.0) + } + let avatarFrame = CGRect(origin: CGPoint(x: leftInset - 78.0 + editingOffset + 10.0 + revealOffset, y: 7.0), size: CGSize(width: 60.0, height: 60.0)) transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) @@ -966,6 +1006,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } + if let reorderControlNode = self.reorderControlNode { + var reorderControlFrame = reorderControlNode.frame + reorderControlFrame.origin.x = params.width - params.rightInset - reorderControlFrame.size.width + offset + transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame) + } + let leftInset: CGFloat = params.leftInset + 78.0 let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) @@ -1091,4 +1137,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.revealOptionsInteractivelyClosed() } } + + override func isReorderable(at point: CGPoint) -> Bool { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) { + return true + } + return false + } } diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 2e6ebc3667..444f8e19d5 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -477,6 +477,70 @@ final class ChatListNode: ListView { } } } + self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState in + if let strongSelf = self, let filteredEntries = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.filteredEntries { + if fromIndex >= 0 && fromIndex < filteredEntries.count && toIndex >= 0 && toIndex < filteredEntries.count { + let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] + let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] + + var referenceId: PinnedItemId? + var beforeAll = false + switch toEntry { + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): + referenceId = .peer(index.messageIndex.id.peerId) + case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): + referenceId = .group(groupId) + case .SearchEntry: + beforeAll = true + default: + break + } + + if let _ = fromEntry.index.pinningIndex { + let _ = (strongSelf.account.postbox.modify { modifier -> Void in + var itemIds = modifier.getPinnedItemIds() + + var itemId: PinnedItemId? + switch fromEntry { + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): + itemId = .peer(index.messageIndex.id.peerId) + case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): + itemId = .group(groupId) + default: + break + } + + if let itemId = itemId { + itemIds = itemIds.filter({ $0 != itemId }) + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< itemIds.count { + if itemIds[i] == referenceId { + if fromIndex < toIndex { + itemIds.insert(itemId, at: i + 1) + } else { + itemIds.insert(itemId, at: i) + } + inserted = true + break + } + } + if !inserted { + itemIds.append(itemId) + } + } else if beforeAll { + itemIds.insert(itemId, at: 0) + } else { + itemIds.append(itemId) + } + reorderPinnedItemIds(modifier: modifier, itemIds: itemIds) + //modifier.setPinnedItemIds(itemIds) + } + }).start() + } + } + } + } } deinit { diff --git a/TelegramUI/ChatListTypingNode.swift b/TelegramUI/ChatListTypingNode.swift index 40109da498..e8a7cc816a 100644 --- a/TelegramUI/ChatListTypingNode.swift +++ b/TelegramUI/ChatListTypingNode.swift @@ -149,15 +149,38 @@ final class ChatListInputActivitiesNode: ASDisplayNode { string = NSAttributedString(string: text, font: textFont, textColor: color) } else { let text: String - if let _ = commonKey, case .typingText = activities[0].1 { - text = strings.DialogList_SingleTypingSuffix(activities[0].0.compactDisplayTitle).0 + if let _ = commonKey { + let peerTitle = activities[0].0.compactDisplayTitle + switch activities[0].1 { + case .uploadingVideo: + text = strings.DialogList_SingleUploadingVideoSuffix(peerTitle).0 + case .uploadingInstantVideo: + text = strings.DialogList_SingleUploadingVideoSuffix(peerTitle).0 + case .uploadingPhoto: + text = strings.DialogList_SingleUploadingPhotoSuffix(peerTitle).0 + case .uploadingFile: + text = strings.DialogList_SingleUploadingFileSuffix(peerTitle).0 + case .recordingVoice: + text = strings.DialogList_SingleRecordingAudioSuffix(peerTitle).0 + case .recordingInstantVideo: + text = strings.DialogList_SingleRecordingVideoMessageSuffix(peerTitle).0 + case .playingGame: + text = strings.DialogList_SinglePlayingGameSuffix(peerTitle).0 + case .typingText: + text = strings.DialogList_SingleTypingSuffix(peerTitle).0 + } } else { text = activities[0].0.compactDisplayTitle } string = NSAttributedString(string: text, font: textFont, textColor: color) } } else { - string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).0, font: textFont, textColor: color) + if activities.count > 1 { + let peerTitle = activities[0].0.compactDisplayTitle + string = NSAttributedString(string: strings.DialogList_MultipleTyping(peerTitle, strings.DialogList_MultipleTypingSuffix(activities.count - 1).0).0, font: textFont, textColor: color) + } else { + string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).0, font: textFont, textColor: color) + } } } else { string = nil diff --git a/TelegramUI/ChatListViewTransition.swift b/TelegramUI/ChatListViewTransition.swift index fa13de859c..32fbf0b096 100644 --- a/TelegramUI/ChatListViewTransition.swift +++ b/TelegramUI/ChatListViewTransition.swift @@ -82,7 +82,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV var minTimestamp: Int32? var maxTimestamp: Int32? for (_, item, _) in indicesAndItems { - if case .PeerEntry = item { + if case .PeerEntry = item, item.index.pinningIndex == nil { let timestamp = item.index.messageIndex.timestamp if minTimestamp == nil || timestamp < minTimestamp! { diff --git a/TelegramUI/ChatLoadingNode.swift b/TelegramUI/ChatLoadingNode.swift index cada0d92eb..65fd3ec2f5 100644 --- a/TelegramUI/ChatLoadingNode.swift +++ b/TelegramUI/ChatLoadingNode.swift @@ -13,7 +13,7 @@ final class ChatLoadingNode: ASDisplayNode { self.backgroundNode.displaysAsynchronously = false self.backgroundNode.image = PresentationResourcesChat.chatLoadingIndicatorBackgroundImage(theme) - self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.serviceMessage.serviceMessagePrimaryTextColor, 22.0), speed: .regular) + self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.serviceMessage.serviceMessagePrimaryTextColor, 22.0, 2.0), speed: .regular) super.init() diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index 0d4c8a8126..e4e9ffa2e2 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -5,7 +5,7 @@ import Postbox import TelegramCore import SwiftSignalKit -final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { +final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let account: Account private let controllerInteraction: ChatControllerInteraction @@ -26,10 +26,14 @@ final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { self.disposable.dispose() } - func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = size self.multiplexedNode?.bottomInset = bottomInset - self.multiplexedNode?.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) + self.multiplexedNode?.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height - topInset)) + } + + func fileAt(point: CGPoint) -> TelegramMediaFile? { + return self.multiplexedNode?.fileAt(point: point) } override func willEnterHierarchy() { @@ -65,23 +69,6 @@ final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { multiplexedNode.fileSelected = { [weak self] file in self?.controllerInteraction.sendGif(file) } - multiplexedNode.fileLongPressed = { [weak self] file in - if let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode, let itemFrame = multiplexedNode.frameForItem(file.fileId) { - let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Common_Delete), action: { - if let strongSelf = self { - let _ = removeSavedGif(postbox: strongSelf.account.postbox, mediaId: file.fileId).start() - } - })]) - strongSelf.controllerInteraction.presentController(contextMenuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { - if let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode { - return (strongSelf, multiplexedNode.convert(itemFrame, to: strongSelf.view).insetBy(dx: -2.0, dy: -2.0).offsetBy(dx: multiplexedNode.frame.minX, dy: multiplexedNode.frame.minY)) - } else { - return nil - } - })) - } - } } } } diff --git a/TelegramUI/ChatMediaInputMetaSectionItemNode.swift b/TelegramUI/ChatMediaInputMetaSectionItemNode.swift index 1a63a7c96f..2d61bf9aec 100644 --- a/TelegramUI/ChatMediaInputMetaSectionItemNode.swift +++ b/TelegramUI/ChatMediaInputMetaSectionItemNode.swift @@ -35,6 +35,8 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { node.inputNodeInteraction = self.inputNodeInteraction node.setItem(item: self) node.updateTheme(theme: self.theme) + node.updateIsHighlighted() + node.updateAppearanceTransition(transition: .immediate) completion(node, { return (nil, { @@ -123,6 +125,12 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { } } + func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { + if let inputNodeInteraction = self.inputNodeInteraction { + transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition) + } + } + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index f5cf7535c7..266b7fa082 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -113,6 +113,7 @@ private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers index += 1 } } + entries.append(.trending(false, theme)) entries.append(.settings(theme)) return entries } @@ -206,6 +207,7 @@ final class ChatMediaInputNodeInteraction { var highlightedStickerItemCollectionId: ItemCollectionId? var highlightedItemCollectionId: ItemCollectionId? var previewedStickerPackItem: StickerPackItem? + var appearanceTransition: CGFloat = 1.0 init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void) { self.navigateToCollectionId = navigateToCollectionId @@ -225,13 +227,14 @@ private func clipScrollPosition(_ position: StickerPacksCollectionPosition) -> S return position } -private enum ChatMediaInputPane { +private enum ChatMediaInputPaneType { case gifs case stickers + case trending } private struct ChatMediaInputPaneArrangement { - let panes: [ChatMediaInputPane] + let panes: [ChatMediaInputPaneType] let currentIndex: Int let indexTransition: CGFloat @@ -252,6 +255,7 @@ final class ChatMediaInputNode: ChatInputNode { private var inputNodeInteraction: ChatMediaInputNodeInteraction! private let collectionListPanel: ASDisplayNode + private var collectionListPanelOffset: CGFloat = 0.0 private let collectionListSeparator: ASDisplayNode private let disposable = MetaDisposable() @@ -262,6 +266,8 @@ final class ChatMediaInputNode: ChatInputNode { private var animatingStickerPaneOut = false private let gifPane: ChatMediaInputGifPane private var animatingGifPaneOut = false + private let trendingPane: ChatMediaInputTrendingPane + private var animatingTrendingPaneOut = false private let itemCollectionsViewPosition = Promise() private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? @@ -269,7 +275,7 @@ final class ChatMediaInputNode: ChatInputNode { private var stickerPreviewController: StickerPreviewController? - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? private var paneArrangement: ChatMediaInputPaneArrangement private var theme: PresentationTheme @@ -286,6 +292,7 @@ final class ChatMediaInputNode: ChatInputNode { self.themeAndStringsPromise = Promise((theme, strings)) self.collectionListPanel = ASDisplayNode() + self.collectionListPanel.clipsToBounds = true self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor self.collectionListSeparator = ASDisplayNode() @@ -295,10 +302,18 @@ final class ChatMediaInputNode: ChatInputNode { self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) - self.stickerPane = ChatMediaInputStickerPane() - self.gifPane = ChatMediaInputGifPane(account: account, controllerInteraction: controllerInteraction) + var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)? + var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)? - self.paneArrangement = ChatMediaInputPaneArrangement(panes: [.gifs, .stickers], currentIndex: 1, indexTransition: 0.0) + self.stickerPane = ChatMediaInputStickerPane(paneDidScroll: { pane, state, transition in + paneDidScrollImpl?(pane, state, transition) + }, fixPaneScroll: { pane, state in + fixPaneScrollImpl?(pane, state) + }) + self.gifPane = ChatMediaInputGifPane(account: account, controllerInteraction: controllerInteraction) + self.trendingPane = ChatMediaInputTrendingPane(account: account, controllerInteraction: controllerInteraction) + + self.paneArrangement = ChatMediaInputPaneArrangement(panes: [.gifs, .stickers, .trending], currentIndex: 1, indexTransition: 0.0) super.init() @@ -307,11 +322,15 @@ final class ChatMediaInputNode: ChatInputNode { var index: Int32 = 0 if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue { strongSelf.setCurrentPane(.gifs, transition: .animated(duration: 0.25, curve: .spring)) + } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { + strongSelf.setCurrentPane(.trending, transition: .animated(duration: 0.25, curve: .spring)) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) + strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) + strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) @@ -319,6 +338,7 @@ final class ChatMediaInputNode: ChatInputNode { if id.namespace == collectionId.namespace { if id == collectionId { let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id) + strongSelf.currentStickerPacksCollectionPosition = .navigate(index: itemIndex, collectionId: nil) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: itemIndex, collectionId: nil))) break } @@ -336,9 +356,9 @@ final class ChatMediaInputNode: ChatInputNode { self.clipsToBounds = true self.backgroundColor = theme.chat.inputMediaPanel.gifsBackgroundColor + self.collectionListPanel.addSubnode(self.listView) self.addSubnode(self.collectionListPanel) self.addSubnode(self.collectionListSeparator) - self.addSubnode(self.listView) let itemCollectionsView = self.itemCollectionsViewPosition.get() |> distinctUntilChanged @@ -351,7 +371,7 @@ final class ChatMediaInputNode: ChatInputNode { } case let .scroll(aroundIndex): var firstTime = true - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 140) + return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 200) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in let update: StickerPacksCollectionUpdate if firstTime { @@ -364,7 +384,7 @@ final class ChatMediaInputNode: ChatInputNode { } case let .navigate(index, collectionId): var firstTime = true - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 140) + return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 200) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in let update: StickerPacksCollectionUpdate if firstTime { @@ -442,17 +462,16 @@ final class ChatMediaInputNode: ChatInputNode { } } - let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.previewGesture(_:))) - longTapRecognizer.tapActionAtPoint = { [weak self] location in - if let strongSelf = self, let _ = strongSelf.stickerPane.gridNode.itemNodeAtPoint(location) as? ChatMediaInputStickerGridItemNode { - return .waitForHold(timeout: 0.2, acceptTap: false) - } - return .fail - } - self.stickerPane.gridNode.view.addGestureRecognizer(longTapRecognizer) - self.currentStickerPacksCollectionPosition = .initial self.itemCollectionsViewPosition.set(.single(.initial)) + + paneDidScrollImpl = { [weak self] pane, state, transition in + self?.updatePaneDidScroll(pane: pane, state: state, transition: transition) + } + + fixPaneScrollImpl = { [weak self] pane, state in + self?.fixPaneScroll(pane: pane, state: state) + } } deinit { @@ -476,26 +495,97 @@ final class ChatMediaInputNode: ChatInputNode { super.didLoad() self.view.disablesInteractiveTransitionGestureRecognizer = true + self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in + if let strongSelf = self { + let panes: [ASDisplayNode] = [strongSelf.gifPane, strongSelf.stickerPane, strongSelf.stickerPane] + for pane in panes { + if pane.supernode != nil, pane.frame.contains(point) { + if let pane = pane as? ChatMediaInputGifPane { + if let file = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { + return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.account, contextResult: .internalReference(id: "", type: "gif", title: nil, description: nil, image: nil, file: file, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + if let strongSelf = self { + strongSelf.controllerInteraction.sendGif(file) + } + }), + PeekControllerMenuItem(title: strongSelf.strings.Common_Delete, color: .destructive, action: { + if let strongSelf = self { + let _ = removeSavedGif(postbox: strongSelf.account.postbox, mediaId: file.fileId).start() + } + }) + ]))) + } + } else if let pane = pane as? ChatMediaInputStickerPane { + if let (itemNode, item) = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { + return strongSelf.account.postbox.modify { modifier -> Bool in + return getIsStickerSaved(modifier: modifier, fileId: item.file.fileId) + } + |> deliverOnMainQueue + |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in + if let strongSelf = self { + var menuItems: [PeekControllerMenuItem] = [] + menuItems = [ + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + if let strongSelf = self { + strongSelf.controllerInteraction.sendSticker(item.file) + } + }), + PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { + if let strongSelf = self { + if isStarred { + let _ = removeSavedSticker(postbox: strongSelf.account.postbox, mediaId: item.file.fileId).start() + } else { + let _ = addSavedSticker(postbox: strongSelf.account.postbox, network: strongSelf.account.network, file: item.file).start() + } + } + }), + PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { + if let strongSelf = self { + loop: for attribute in item.file.attributes { + switch attribute { + case let .Sticker(_, packReference, _): + if let packReference = packReference { + strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: packReference), nil) + } + break loop + default: + break + } + } + } + }), + PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {}) + ] + return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: item, menu: menuItems)) + } else { + return nil + } + } + } + } + } + } + } + return nil + }, present: { [weak self] content, sourceNode in + if let strongSelf = self { + let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + return sourceNode + }) + strongSelf.controllerInteraction.presentGlobalOverlayController(controller, nil) + return controller + } + return nil + })) self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } - private func heightForWidth(width: CGFloat) -> CGFloat { - let defaultPortraitPanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 271.0 : 258.0 - let defaultLandscapePanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 194.0 : 194.0 - - if width.isEqual(to: 812.0) { - return defaultLandscapePanelHeight - } - - return defaultPortraitPanelHeight - } - - private func setCurrentPane(_ pane: ChatMediaInputPane, transition: ContainedViewLayoutTransition) { + private func setCurrentPane(_ pane: ChatMediaInputPaneType, transition: ContainedViewLayoutTransition) { if let index = self.paneArrangement.panes.index(of: pane), index != self.paneArrangement.currentIndex { let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) - if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs if updatedGifPanelWasActive != previousGifPanelWasActive { @@ -508,10 +598,12 @@ final class ChatMediaInputNode: ChatInputNode { if let highlightedStickerCollectionId = self.inputNodeInteraction.highlightedStickerItemCollectionId { self.setHighlightedItemCollectionId(highlightedStickerCollectionId) } + case .trending: + self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0)) } } else { - if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } } } @@ -521,6 +613,10 @@ final class ChatMediaInputNode: ChatInputNode { if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs { self.inputNodeInteraction.highlightedItemCollectionId = collectionId } + } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { + if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .trending { + self.inputNodeInteraction.highlightedItemCollectionId = collectionId + } } else { self.inputNodeInteraction.highlightedStickerItemCollectionId = collectionId if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .stickers { @@ -551,6 +647,12 @@ final class ChatMediaInputNode: ChatInputNode { self.listView.ensureItemNodeVisible(itemNode) ensuredNodeVisible = true } + } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { + itemNode.updateIsHighlighted() + if itemNode.currentCollectionId == collectionId { + self.listView.ensureItemNodeVisible(itemNode) + ensuredNodeVisible = true + } } } @@ -564,21 +666,39 @@ final class ChatMediaInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, bottomInset, interfaceState) + private func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { + var value: CGFloat = 1.0 - abs(self.collectionListPanelOffset / 41.0) + value = min(1.0, max(0.0, value)) + self.inputNodeInteraction.appearanceTransition = max(0.1, value) + transition.updateAlpha(node: self.listView, alpha: value) + self.listView.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { + itemNode.updateAppearanceTransition(transition: transition) + } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { + itemNode.updateAppearanceTransition(transition: transition) + } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { + itemNode.updateAppearanceTransition(transition: transition) + } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { + itemNode.updateAppearanceTransition(transition: transition) + } + } + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) } let separatorHeight = UIScreenPixel - let panelHeight = self.heightForWidth(width: width) + bottomInset + let panelHeight = standardInputHeight - transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: 41.0))) - transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: width, height: separatorHeight))) + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) - self.listView.position = CGPoint(x: width / 2.0, y: 41.0 / 2.0) + transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - self.collectionListPanelOffset) / 2.0)) var duration: Double = 0.0 var curve: UInt = 0 @@ -606,7 +726,7 @@ final class ChatMediaInputNode: ChatInputNode { self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - var visiblePanes: [(ChatMediaInputPane, CGFloat)] = [] + var visiblePanes: [(ChatMediaInputPaneType, CGFloat)] = [] var paneIndex = 0 for pane in self.paneArrangement.panes { @@ -618,11 +738,11 @@ final class ChatMediaInputNode: ChatInputNode { } for (pane, paneOrigin) in visiblePanes { - let paneFrame = CGRect(origin: CGPoint(x: paneOrigin + leftInset, y: 41.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight - 41.0)) + let paneFrame = CGRect(origin: CGPoint(x: paneOrigin + leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight)) switch pane { case .gifs: if self.gifPane.supernode == nil { - self.addSubnode(self.gifPane) + self.insertSubnode(self.gifPane, belowSubnode: self.collectionListPanel) self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) } if self.gifPane.frame != paneFrame { @@ -631,18 +751,28 @@ final class ChatMediaInputNode: ChatInputNode { } case .stickers: if self.stickerPane.supernode == nil { - self.addSubnode(self.stickerPane) + self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListPanel) self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) } if self.stickerPane.frame != paneFrame { self.stickerPane.layer.removeAnimation(forKey: "position") transition.updateFrame(node: self.stickerPane, frame: paneFrame) } + case .trending: + if self.trendingPane.supernode == nil { + self.insertSubnode(self.trendingPane, belowSubnode: self.collectionListPanel) + self.trendingPane.frame = CGRect(origin: CGPoint(x: width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) + } + if self.trendingPane.frame != paneFrame { + self.trendingPane.layer.removeAnimation(forKey: "position") + transition.updateFrame(node: self.trendingPane, frame: paneFrame) + } } } - self.gifPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight - 41.0), bottomInset: bottomInset, transition: transition) - self.stickerPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight - 41.0), bottomInset: bottomInset, transition: transition) + self.gifPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, transition: transition) + self.stickerPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, transition: transition) + self.trendingPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, transition: transition) if self.gifPane.supernode != nil { if !visiblePanes.contains(where: { $0.0 == .gifs }) { @@ -694,6 +824,31 @@ final class ChatMediaInputNode: ChatInputNode { self.animatingStickerPaneOut = false } + if self.trendingPane.supernode != nil { + if !visiblePanes.contains(where: { $0.0 == .trending }) { + if case .animated = transition { + if !self.animatingTrendingPaneOut { + self.animatingTrendingPaneOut = true + var toLeft = false + if let index = self.paneArrangement.panes.index(of: .trending), index < self.paneArrangement.currentIndex { + toLeft = true + } + transition.animatePosition(node: self.trendingPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.trendingPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in + if let strongSelf = self, value { + strongSelf.animatingTrendingPaneOut = false + strongSelf.trendingPane.removeFromSupernode() + } + }) + } + } else { + self.animatingTrendingPaneOut = false + self.trendingPane.removeFromSupernode() + } + } + } else { + self.animatingTrendingPaneOut = false + } + return panelHeight } @@ -780,7 +935,7 @@ final class ChatMediaInputNode: ChatInputNode { case .began: break case .changed: - if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x var indexTransition = translationX / width if self.paneArrangement.currentIndex == 0 { @@ -789,10 +944,10 @@ final class ChatMediaInputNode: ChatInputNode { indexTransition = min(0.0, indexTransition) } self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .immediate, interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .immediate, interfaceState: interfaceState) } case .ended: - if let (width, _, _, _, _) = self.validLayout { + if let (width, _, _, _, _, _) = self.validLayout { var updatedIndex = self.paneArrangement.currentIndex if abs(self.paneArrangement.indexTransition * width) > 30.0 { if self.paneArrangement.indexTransition < 0.0 { @@ -805,12 +960,52 @@ final class ChatMediaInputNode: ChatInputNode { self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) } case .cancelled: - if let (width, leftInset, rightInset, bottomInset, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } default: break } } + + private func updatePaneDidScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState, transition: ContainedViewLayoutTransition) { + var computedAbsoluteOffset: CGFloat + if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { + computedAbsoluteOffset = 0.0 + } else { + computedAbsoluteOffset = self.collectionListPanelOffset + state.relativeChange + } + computedAbsoluteOffset = max(-41.0, min(computedAbsoluteOffset, 0.0)) + self.collectionListPanelOffset = computedAbsoluteOffset + if transition.isAnimated { + if self.collectionListPanelOffset < -41.0 / 2.0 { + self.collectionListPanelOffset = -41.0 + } else { + self.collectionListPanelOffset = 0.0 + } + } + + self.updateAppearanceTransition(transition: transition) + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) + transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - self.collectionListPanelOffset) / 2.0)) + } + + private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) { + if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { + self.collectionListPanelOffset = 0.0 + } else { + if self.collectionListPanelOffset < -41.0 / 2.0 { + self.collectionListPanelOffset = -41.0 + } else { + self.collectionListPanelOffset = 0.0 + } + } + let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .spring) + self.updateAppearanceTransition(transition: transition) + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) + transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - self.collectionListPanelOffset) / 2.0)) + } } diff --git a/TelegramUI/ChatMediaInputPane.swift b/TelegramUI/ChatMediaInputPane.swift new file mode 100644 index 0000000000..6d806a4224 --- /dev/null +++ b/TelegramUI/ChatMediaInputPane.swift @@ -0,0 +1,13 @@ +import Foundation +import AsyncDisplayKit +import Display + +struct ChatMediaInputPaneScrollState { + let absoluteOffset: CGFloat? + let relativeChange: CGFloat +} + +class ChatMediaInputPane: ASDisplayNode { + func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + } +} diff --git a/TelegramUI/ChatMediaInputRecentGifsItem.swift b/TelegramUI/ChatMediaInputRecentGifsItem.swift index 4dbedcef75..35584970f1 100644 --- a/TelegramUI/ChatMediaInputRecentGifsItem.swift +++ b/TelegramUI/ChatMediaInputRecentGifsItem.swift @@ -27,6 +27,8 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction node.updateTheme(theme: self.theme) + node.updateIsHighlighted() + node.updateAppearanceTransition(transition: .immediate) completion(node, { return (nil, {}) }) @@ -98,4 +100,10 @@ final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId } } + + func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { + if let inputNodeInteraction = self.inputNodeInteraction { + transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition) + } + } } diff --git a/TelegramUI/ChatMediaInputSettingsItem.swift b/TelegramUI/ChatMediaInputSettingsItem.swift index 99fea7e2ff..922732d3b0 100644 --- a/TelegramUI/ChatMediaInputSettingsItem.swift +++ b/TelegramUI/ChatMediaInputSettingsItem.swift @@ -27,6 +27,7 @@ final class ChatMediaInputSettingsItem: ListViewItem { node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction node.updateTheme(theme: self.theme) + node.updateAppearanceTransition(transition: .immediate) completion(node, { return (nil, {}) }) @@ -87,5 +88,11 @@ final class ChatMediaInputSettingsItemNode: ListViewItemNode { self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSettingsIconImage(theme) } } + + func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { + if let inputNodeInteraction = self.inputNodeInteraction { + transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition) + } + } } diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index 93d5503926..d3f5ba90f0 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -34,9 +34,11 @@ final class ChatMediaInputStickerPackItem: ListViewItem { node.contentSize = CGSize(width: 41.0, height: 41.0) node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction - node.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) completion(node, { - return (nil, {}) + return (nil, { + node.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) + node.updateAppearanceTransition(transition: .immediate) + }) }) } } @@ -117,8 +119,16 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { } func updateIsHighlighted() { + assert(Queue.mainQueue().isCurrent()) if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction { self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId } } + + func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { + assert(Queue.mainQueue().isCurrent()) + if let inputNodeInteraction = self.inputNodeInteraction { + transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition) + } + } } diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index 1c1495cea9..39e37e9567 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -5,20 +5,51 @@ import Postbox import TelegramCore import SwiftSignalKit -final class ChatMediaInputStickerPane: ASDisplayNode { +final class ChatMediaInputStickerPane: ChatMediaInputPane { let gridNode: GridNode + private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void + private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void + private var didScrollPreviousOffset: CGFloat? + private var didScrollPreviousState: ChatMediaInputPaneScrollState? - override init() { + init(paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void) { self.gridNode = GridNode() + self.paneDidScroll = paneDidScroll + self.fixPaneScroll = fixPaneScroll super.init() self.addSubnode(self.gridNode) + self.gridNode.presentationLayoutUpdated = { [weak self] layout, transition in + if let strongSelf = self { + let offset = -(layout.contentOffset.y + 41.0) + var relativeChange: CGFloat = 0.0 + if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset { + relativeChange = offset - didScrollPreviousOffset + } + strongSelf.didScrollPreviousOffset = offset + let state = ChatMediaInputPaneScrollState(absoluteOffset: offset, relativeChange: relativeChange) + strongSelf.didScrollPreviousState = state + strongSelf.paneDidScroll(strongSelf, state, transition) + } + } + self.gridNode.scrollingCompleted = { [weak self] in + if let strongSelf = self, let didScrollPreviousState = strongSelf.didScrollPreviousState { + strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState) + } + } } - func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) } + + func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { + if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) as? ChatMediaInputStickerGridItemNode, let stickerPackItem = itemNode.stickerPackItem { + return (itemNode, stickerPackItem) + } + return nil + } } diff --git a/TelegramUI/ChatMediaInputTrendingItem.swift b/TelegramUI/ChatMediaInputTrendingItem.swift index 9c586f8e71..64591dba38 100644 --- a/TelegramUI/ChatMediaInputTrendingItem.swift +++ b/TelegramUI/ChatMediaInputTrendingItem.swift @@ -27,6 +27,8 @@ final class ChatMediaInputTrendingItem: ListViewItem { node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction node.updateTheme(theme: self.theme) + node.updateIsHighlighted() + node.updateAppearanceTransition(transition: .immediate) completion(node, { return (nil, {}) }) @@ -98,5 +100,11 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId } } + + func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { + if let inputNodeInteraction = self.inputNodeInteraction { + transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition) + } + } } diff --git a/TelegramUI/ChatMediaInputTrendingPane.swift b/TelegramUI/ChatMediaInputTrendingPane.swift index 9ae84f8e62..a0a17b11f8 100644 --- a/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/TelegramUI/ChatMediaInputTrendingPane.swift @@ -5,13 +5,102 @@ import Postbox import TelegramCore import SwiftSignalKit -final class ChatMediaInputTrendingPane: ASDisplayNode { +final class TrendingPaneInteraction { + let installPack: (ItemCollectionInfo) -> Void + let openPack: (ItemCollectionInfo) -> Void + + init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void) { + self.installPack = installPack + self.openPack = openPack + } +} + +private final class TrendingPaneEntry: Identifiable, Comparable { + let index: Int + let info: StickerPackCollectionInfo + let topItems: [StickerPackItem] + let installed: Bool + let unread: Bool + + init(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, unread: Bool) { + self.index = index + self.info = info + self.topItems = topItems + self.installed = installed + self.unread = unread + } + + var stableId: ItemCollectionId { + return self.info.id + } + + static func ==(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.info != rhs.info { + return false + } + if lhs.topItems != rhs.topItems { + return false + } + if lhs.installed != rhs.installed { + return false + } + return true + } + + static func <(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: TrendingPaneInteraction) -> ListViewItem { + return MediaInputPaneTrendingItem(account: account, theme: theme, strings: strings, interaction: interaction, info: self.info, topItems: self.topItems, installed: self.installed, unread: self.unread) + } +} + +private struct TrendingPaneTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let initial: Bool +} + +private func preparedTransition(from fromEntries: [TrendingPaneEntry], to toEntries: [TrendingPaneEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: TrendingPaneInteraction, initial: Bool) -> TrendingPaneTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } + + return TrendingPaneTransition(deletions: deletions, insertions: insertions, updates: updates, initial: initial) +} + +private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], installedPacks: Set) -> [TrendingPaneEntry] { + var result: [TrendingPaneEntry] = [] + var index = 0 + for item in trendingEntries { + result.append(TrendingPaneEntry(index: index, info: item.info, topItems: item.topItems, installed: installedPacks.contains(item.info.id), unread: item.unread)) + index += 1 + } + return result +} + +final class ChatMediaInputTrendingPane: ChatMediaInputPane { private let account: Account + private let controllerInteraction: ChatControllerInteraction private let listNode: ListView - init(account: Account) { + private var enqueuedTransitions: [TrendingPaneTransition] = [] + private var validLayout: (CGSize, CGFloat)? + + private var disposable: Disposable? + private var isActivated = false + + init(account: Account, controllerInteraction: ChatControllerInteraction) { self.account = account + self.controllerInteraction = controllerInteraction self.listNode = ListView() @@ -19,4 +108,95 @@ final class ChatMediaInputTrendingPane: ASDisplayNode { self.addSubnode(self.listNode) } + + deinit { + self.disposable?.dispose() + } + + func activate() { + if self.isActivated { + return + } + self.isActivated = true + + let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } + + let interaction = TrendingPaneInteraction(installPack: { [weak self] info in + if let strongSelf = self, let info = info as? StickerPackCollectionInfo { + strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }, openPack: { [weak self] info in + if let strongSelf = self, let info = info as? StickerPackCollectionInfo { + strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }) + + let previousEntries = Atomic<[TrendingPaneEntry]?>(value: nil) + let account = self.account + self.disposable = (combineLatest(account.viewTracker.featuredStickerPacks(), account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + |> map { trendingEntries, view -> TrendingPaneTransition in + var installedPacks = Set() + if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { + if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { + for entry in packsEntries { + installedPacks.insert(entry.id) + } + } + } + let entries = trendingPaneEntries(trendingEntries: trendingEntries, installedPacks: installedPacks) + let previous = previousEntries.swap(entries) + + return preparedTransition(from: previous ?? [], to: entries, account: account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, initial: previous == nil) + } + |> deliverOnMainQueue).start(next: { [weak self] transition in + self?.enqueueTransition(transition) + }) + } + + override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (size, bottomInset) + self.listNode.frame = CGRect(origin: CGPoint(), size: size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func enqueueTransition(_ transition: TrendingPaneTransition) { + enqueuedTransitions.append(transition) + + if self.validLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + self.activate() + } + + private func dequeueTransition() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + let options = ListViewDeleteAndInsertOptions() + if transition.initial { + //options.insert(.Synchronous) + //options.insert(.LowLatency) + } else { + //options.insert(.AnimateTopItemPosition) + //options.insert(.AnimateCrossfade) + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + }) + } + } } diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index cca5c67a40..bee674bf67 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -422,7 +422,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { self.view.addGestureRecognizer(recognizer) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants diff --git a/TelegramUI/ChatMessageActionSheetController.swift b/TelegramUI/ChatMessageActionSheetController.swift new file mode 100644 index 0000000000..f547ce4652 --- /dev/null +++ b/TelegramUI/ChatMessageActionSheetController.swift @@ -0,0 +1,30 @@ +import Foundation +import Display +import AsyncDisplayKit + +final class ChatMessageActionSheetController: ViewController { + var controllerNode: ChatMessageActionSheetControllerNode { + return self.displayNode as! ChatMessageActionSheetControllerNode + } + + private let theme: PresentationTheme + private let actions: [ChatMessageContextMenuSheetAction] + private let dismissed: () -> Void + + init(theme: PresentationTheme, actions: [ChatMessageContextMenuSheetAction], dismissed: @escaping () -> Void) { + self.theme = theme + self.actions = actions + self.dismissed = dismissed + + super.init(navigationBarTheme: nil) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = ChatMessageActionSheetControllerNode(theme: self.theme, actions: self.actions, dismissed: self.dismissed) + self.displayNodeDidLoad() + } +} diff --git a/TelegramUI/ChatMessageActionSheetControllerNode.swift b/TelegramUI/ChatMessageActionSheetControllerNode.swift new file mode 100644 index 0000000000..668ff3a8ae --- /dev/null +++ b/TelegramUI/ChatMessageActionSheetControllerNode.swift @@ -0,0 +1,153 @@ +import Foundation +import Display +import AsyncDisplayKit + +private final class MessageActionButtonNode: HighlightableButtonNode { + let theme: PresentationTheme + let separatorNode: ASDisplayNode + let backgroundNode: ASDisplayNode + + init(theme: PresentationTheme) { + self.theme = theme + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = theme.actionSheet.opaqueItemHighlightedBackgroundColor + self.backgroundNode.alpha = 0.0 + + super.init() + + self.setAttributedTitle(NSAttributedString(string: " "), for: []) + + self.insertSubnode(self.separatorNode, at: 0) + self.insertSubnode(self.backgroundNode, at: 1) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + if let supernode = strongSelf.titleNode.supernode { + strongSelf.titleNode.removeFromSupernode() + supernode.addSubnode(strongSelf.titleNode) + } + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 1.0 + } else { + strongSelf.backgroundNode.alpha = 0.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + } + + override func layout() { + super.layout() + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height - UIScreenPixel), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) + self.backgroundNode.frame = self.bounds + } +} + +final class ChatMessageActionSheetControllerNode: ViewControllerTracingNode { + private let theme: PresentationTheme + + private let inputDimNode: ASDisplayNode + private let itemsContainerNode: ASDisplayNode + + private let actions: [ChatMessageContextMenuSheetAction] + private let dismissed: () -> Void + private let actionNodes: [MessageActionButtonNode] + + private let feedback = HapticFeedback() + + init(theme: PresentationTheme, actions: [ChatMessageContextMenuSheetAction], dismissed: @escaping () -> Void) { + self.theme = theme + self.actions = actions + self.dismissed = dismissed + + self.inputDimNode = ASDisplayNode() + self.inputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.inputDimNode.isLayerBacked = true + + self.itemsContainerNode = ASDisplayNode() + self.itemsContainerNode.backgroundColor = theme.actionSheet.opaqueItemBackgroundColor + self.itemsContainerNode.cornerRadius = 16.0 + self.itemsContainerNode.clipsToBounds = true + + self.actionNodes = actions.map { action in + let node = MessageActionButtonNode(theme: theme) + node.setAttributedTitle(NSAttributedString(string: action.title, font: Font.regular(20.0), textColor: action.color == .destructive ? theme.actionSheet.destructiveActionTextColor : theme.actionSheet.controlAccentColor), for: []) + return node + } + + super.init() + + self.addSubnode(self.inputDimNode) + self.addSubnode(self.itemsContainerNode) + + for actionNode in actionNodes { + self.itemsContainerNode.addSubnode(actionNode) + actionNode.addTarget(self, action: #selector(actionPressed(_:)), forControlEvents: .touchUpInside) + } + + self.feedback.prepareImpact() + } + + func animateIn(transition: ContainedViewLayoutTransition) { + self.inputDimNode.alpha = 0.0 + transition.updateAlpha(node: self.inputDimNode, alpha: 1.0) + transition.animatePositionAdditive(node: self.itemsContainerNode, offset: self.bounds.size.height) + + self.feedback.impact() + } + + func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + transition.updateAlpha(node: self.inputDimNode, alpha: 0.0) + transition.updatePosition(node: self.itemsContainerNode, position: CGPoint(x: self.itemsContainerNode.position.x, y: self.bounds.size.height + self.itemsContainerNode.bounds.height), completion: { _ in + completion() + }) + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + var height: CGFloat = 14.0 + + let inputHeight = layout.inputHeight ?? 0.0 + transition.updateFrame(node: self.inputDimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - inputHeight), size: CGSize(width: layout.size.width, height: inputHeight))) + + height += layout.safeInsets.bottom + + var itemsHeight: CGFloat = 0.0 + for actionNode in self.actionNodes { + actionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: CGSize(width: layout.size.width - 14.0 * 2.0, height: 57.0)) + actionNode.layout() + itemsHeight += actionNode.bounds.height + } + + transition.updateFrame(node: self.itemsContainerNode, frame: CGRect(origin: CGPoint(x: 14.0, y: layout.size.height - height - itemsHeight), size: CGSize(width: layout.size.width - 14.0 * 2.0, height: itemsHeight))) + + height += itemsHeight + + height += 6.0 + + return height + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.itemsContainerNode.frame.contains(point) { + let subpoint = self.view.convert(point, to: self.itemsContainerNode.view) + return itemsContainerNode.hitTest(subpoint, with: event) + } + return nil + } + + @objc func actionPressed(_ node: ASDisplayNode) { + for i in 0 ..< self.actionNodes.count { + if node == self.actionNodes[i] { + self.actions[i].action() + self.dismissed() + break + } + } + } +} diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index d764065c94..348d29f09e 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -298,18 +298,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { sentViaBot = true } } - - var dateText = stringForMessageTimestamp(timestamp: message.timestamp, timeFormat: presentationData.timeFormat) - - if let author = message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } + if let author = message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true } + let dateText = stringForMessageTimestampStatus(message: message, timeFormat: presentationData.timeFormat, strings: presentationData.strings) + var webpageGalleryMediaCount: Int? for media in message.media { if let media = media as? TelegramMediaWebpage { @@ -824,7 +818,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - func updateHiddenMedia(_ media: [Media]?) { + func updateHiddenMedia(_ media: [Media]?) -> Bool { if let currentMedia = self.media { if let media = media { var found = false @@ -836,18 +830,24 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } if let contentImageNode = self.contentImageNode { contentImageNode.isHidden = found + return found } } else if let contentImageNode = self.contentImageNode { contentImageNode.isHidden = false } } + return false } - func transitionNode(media: Media) -> ASDisplayNode? { - if let image = self.media as? TelegramMediaImage, image.isEqual(media) { - return self.contentImageNode - } else if let file = self.media as? TelegramMediaFile, file.isEqual(media) { - return self.contentImageNode + func transitionNode(media: Media) -> (ASDisplayNode, () -> UIView?)? { + if let contentImageNode = self.contentImageNode, let image = self.media as? TelegramMediaImage, image.isEqual(media) { + return (contentImageNode, { [weak contentImageNode] in + return contentImageNode?.view.snapshotContentTree(unhide: true) + }) + } else if let contentImageNode = self.contentImageNode, let file = self.media as? TelegramMediaFile, file.isEqual(media) { + return (contentImageNode, { [weak contentImageNode] in + return contentImageNode?.view.snapshotContentTree(unhide: true) + }) } return nil } diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index 9684790676..8306dfc8b2 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -38,7 +38,6 @@ struct ChatMessageBubbleContentMosaicPosition { let topRight: ChatMessageBubbleContentMosaicNeighbor let bottomLeft: ChatMessageBubbleContentMosaicNeighbor let bottomRight: ChatMessageBubbleContentMosaicNeighbor - let mosaicStatusHorizontalOffset: CGFloat? } enum ChatMessageBubbleContentPosition { @@ -115,11 +114,16 @@ class ChatMessageBubbleContentNode: ASDisplayNode { }) } - func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { return nil } - func updateHiddenMedia(_ media: [Media]?) { + func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { + return nil + } + + func updateHiddenMedia(_ media: [Media]?) -> Bool { + return false } func updateAutomaticMediaDownloadSettings(_ settings: AutomaticMediaDownloadSettings) { diff --git a/TelegramUI/ChatMessageBubbleImages.swift b/TelegramUI/ChatMessageBubbleImages.swift index 63866abc6f..d7d6d56883 100644 --- a/TelegramUI/ChatMessageBubbleImages.swift +++ b/TelegramUI/ChatMessageBubbleImages.swift @@ -31,9 +31,9 @@ func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor let additionalOffset: CGFloat switch neighbors { - case .none, .side, .bottom: + case .none, .bottom: additionalOffset = 0.0 - case .both, .top: + case .both, .side, .top: additionalOffset = 6.0 } @@ -54,10 +54,12 @@ func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") context.fillPath() case .side: - let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 35.0, height: 35.0))) + //let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") context.strokePath() - let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") - context.fillPath() + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 35.0, height: 35.0))) + /*let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") + context.fillPath()*/ case .top: let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") context.strokePath() diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 22b73bc044..397b7766c8 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -113,6 +113,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var replyInfoNode: ChatMessageReplyInfoNode? private var contentNodes: [ChatMessageBubbleContentNode] = [] + private var mosaicStatusNode: ChatMessageDateAndStatusNode? private var actionButtonsNode: ChatMessageActionButtonsNode? private var shareButtonNode: HighlightableButtonNode? @@ -253,7 +254,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.view.addGestureRecognizer(replyRecognizer) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))))] = [] for contentNode in self.contentNodes { if let message = contentNode.item?.message { @@ -266,6 +267,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) + let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode) + let currentShareButtonNode = self.shareButtonNode let layoutConstants = self.layoutConstants @@ -294,10 +297,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { ignoreForward = true effectiveAuthor = forwardInfo.author } - displayAuthorInfo = !mergedTop && incoming && effectiveAuthor != nil + displayAuthorInfo = !mergedTop.merged && incoming && effectiveAuthor != nil } else { effectiveAuthor = firstMessage.author - displayAuthorInfo = !mergedTop && incoming && peerId.isGroupOrChannel && effectiveAuthor != nil + displayAuthorInfo = !mergedTop.merged && incoming && peerId.isGroupOrChannel && effectiveAuthor != nil } if peerId != item.account.peerId { @@ -424,8 +427,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var contentPropertiesAndLayouts: [(CGSize?, ChatMessageBubbleContentProperties, ChatMessageBubblePreparePosition, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)))] = [] - let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) - let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) + let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) + let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) var canPossiblyHideBackground = false if case .color = item.presentationData.wallpaper { @@ -577,6 +580,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus) var calculatedGroupFramesAndSize: ([(CGRect, MosaicItemPosition)], CGSize)? + var mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)? if let mosaicRange = mosaicRange { let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) @@ -589,6 +593,43 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { calculatedGroupFramesAndSize = (framesAndPositions, size) maximumNodeWidth = size.width + + if mosaicRange.upperBound == contentPropertiesAndLayouts.count { + let message = item.content.firstMessage + + var edited = false + var sentViaBot = false + var viewCount: Int? + for attribute in message.attributes { + if let _ = attribute as? EditedMessageAttribute { + edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let _ = attribute as? InlineBotMessageAttribute { + sentViaBot = true + } + } + if let author = message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true + } + + let dateText = stringForMessageTimestampStatus(message: message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) + + let statusType: ChatMessageDateAndStatusType + if message.effectivelyIncoming(item.account.peerId) { + statusType = .ImageIncoming + } else { + if message.flags.contains(.Failed) { + statusType = .ImageOutgoing(.Failed) + } else if message.flags.isSending { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .ImageOutgoing(.Sent(read: item.read)) + } + } + + mosaicStatusSizeAndApply = mosaicStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude)) + } } var headerSize = CGSize() @@ -811,12 +852,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - var mosaicStatusHorizontalOffset: CGFloat? - if position.contains(.bottom) { - mosaicStatusHorizontalOffset = size.width - framesAndPositions[mosaicIndex].0.maxX - } - - let (_, contentNodeFinalize) = contentNodeLayout(framesAndPositions[mosaicIndex].0.size, .mosaic(position: ChatMessageBubbleContentMosaicPosition(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight, mosaicStatusHorizontalOffset: mosaicStatusHorizontalOffset))) + let (_, contentNodeFinalize) = contentNodeLayout(framesAndPositions[mosaicIndex].0.size, .mosaic(position: ChatMessageBubbleContentMosaicPosition(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight))) contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize)) @@ -846,6 +882,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { contentPosition = .linear(top: .Neighbour, bottom: .Neighbour) } let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), contentPosition) + #if DEBUG + if contentNodeWidth > maximumNodeWidth { + print("\(contentNodeWidth) > \(maximumNodeWidth)") + } + #endif maxContentWidth = max(maxContentWidth, contentNodeWidth) contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize)) @@ -855,6 +896,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var contentSize = CGSize(width: maxContentWidth, height: 0.0) var contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, (ListViewItemUpdateAnimation) -> Void)] = [] var contentNodesHeight: CGFloat = 0.0 + var mosaicStatusOrigin: CGPoint? for i in 0 ..< contentNodePropertiesAndFinalize.count { let (properties, finalize) = contentNodePropertiesAndFinalize[i] @@ -868,10 +910,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } let (_, apply) = finalize(maxContentWidth) - contentNodeFramesPropertiesAndApply.append((framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodesHeight), properties, apply)) + let contentNodeFrame = framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodesHeight) + contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, apply)) if mosaicIndex == mosaicRange.upperBound - 1 { contentNodesHeight += size.height + + mosaicStatusOrigin = contentNodeFrame.bottomRight } } else { if i == 0 && !headerSize.height.isZero { @@ -902,7 +947,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { layoutSize.height += actionButtonsSizeAndApply.0.height } - var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) + var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) if dateHeaderAtBottom { layoutInsets.top += layoutConstants.timestampHeaderHeight } @@ -940,11 +985,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var udpdatedMergedTop = mergedBottom var udpdatedMergedBottom = mergedTop if mosaicRange == nil { - if headerSize.height.isZero, contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { - udpdatedMergedTop = false - } if contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { - udpdatedMergedBottom = false + udpdatedMergedTop = .semanticallyMerged + } + if headerSize.height.isZero && contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { + udpdatedMergedBottom = .none } } @@ -957,7 +1002,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { transition = .animated(duration: duration, curve: .spring) } - let mergeType = ChatMessageBackgroundMergeType(top: udpdatedMergedTop, bottom: udpdatedMergedBottom, side: actionButtonsSizeAndApply != nil) + var forceBackgroundSide = false + if actionButtonsSizeAndApply != nil { + forceBackgroundSide = true + } else if case .semanticallyMerged = udpdatedMergedTop { + forceBackgroundSide = true + } + let mergeType = ChatMessageBackgroundMergeType(top: udpdatedMergedTop == .fullyMerged, bottom: udpdatedMergedBottom == .fullyMerged, side: forceBackgroundSide) let backgroundType: ChatMessageBackgroundType if hideBackground { backgroundType = .none @@ -1085,6 +1136,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { contentNodeIndex += 1 } + if let mosaicStatusOrigin = mosaicStatusOrigin, let (size, apply) = mosaicStatusSizeAndApply { + let mosaicStatusNode = apply(false) + if mosaicStatusNode !== strongSelf.mosaicStatusNode { + strongSelf.mosaicStatusNode?.removeFromSupernode() + strongSelf.mosaicStatusNode = mosaicStatusNode + strongSelf.addSubnode(mosaicStatusNode) + } + let absoluteOrigin = mosaicStatusOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) + mosaicStatusNode.frame = CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size) + } else if let mosaicStatusNode = strongSelf.mosaicStatusNode { + strongSelf.mosaicStatusNode = nil + mosaicStatusNode.removeFromSupernode() + } + if let updatedShareButtonNode = updatedShareButtonNode { if updatedShareButtonNode !== strongSelf.shareButtonNode { if let shareButtonNode = strongSelf.shareButtonNode { @@ -1448,7 +1513,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return super.hitTest(point, with: event) } - override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { for contentNode in self.contentNodes { if let result = contentNode.transitionNode(messageId: id, media: media) { return result @@ -1457,11 +1522,37 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return nil } + override func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { + 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() { + var hasHiddenMosaicStatus = false if let item = self.item { for contentNode in self.contentNodes { if let contentItem = contentNode.item { - contentNode.updateHiddenMedia(item.controllerInteraction.hiddenMedia[contentItem.message.id]) + if contentNode.updateHiddenMedia(item.controllerInteraction.hiddenMedia[contentItem.message.id]) { + if let mosaicStatusNode = self.mosaicStatusNode, mosaicStatusNode.frame.intersects(contentNode.frame) { + hasHiddenMosaicStatus = true + } + } + } + } + } + + if let mosaicStatusNode = self.mosaicStatusNode { + if mosaicStatusNode.alpha.isZero != hasHiddenMosaicStatus { + if hasHiddenMosaicStatus { + mosaicStatusNode.alpha = 0.0 + } else { + mosaicStatusNode.alpha = 1.0 + mosaicStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } @@ -1556,6 +1647,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } override func updateHighlightedState(animated: Bool) { + super.updateHighlightedState(animated: animated) + if let item = self.item { var highlighted = false if let highlightedState = item.controllerInteraction.highlightedState { diff --git a/TelegramUI/ChatMessageCallBubbleContentNode.swift b/TelegramUI/ChatMessageCallBubbleContentNode.swift index 8eb8d97001..2a745d43fd 100644 --- a/TelegramUI/ChatMessageCallBubbleContentNode.swift +++ b/TelegramUI/ChatMessageCallBubbleContentNode.swift @@ -127,15 +127,7 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { buttonImage = PresentationResourcesChat.chatBubbleOutgoingCallButtonImage(item.presentationData.theme) } - var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) - - if let duration = callDuration, duration != 0 { - if duration >= 60 { - dateText += ", " + item.presentationData.strings.Call_Minutes(duration / 60) - } else { - dateText += ", " + item.presentationData.strings.Call_Seconds(duration) - } - } + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) var attributedLabel: NSAttributedString? attributedLabel = NSAttributedString(string: dateText, font: labelFont, textColor: message.effectivelyIncoming(item.account.peerId) ? bubbleTheme.incomingFileDurationColor : bubbleTheme.outgoingFileDurationColor) diff --git a/TelegramUI/ChatMessageContactBubbleContentNode.swift b/TelegramUI/ChatMessageContactBubbleContentNode.swift index ed46896942..6471f995fc 100644 --- a/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -107,18 +107,12 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { sentViaBot = true } } - - var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) - - if let author = item.message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } + if let author = item.message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true } + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) + let statusType: ChatMessageDateAndStatusType? switch position { case .linear(_, .None): diff --git a/TelegramUI/ChatMessageDateAndStatusNode.swift b/TelegramUI/ChatMessageDateAndStatusNode.swift index ed852de783..0349ae8c1a 100644 --- a/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -493,4 +493,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { }) } } + + static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { + let currentLayout = node?.asyncLayout() + return { theme, strings, edited, impressionCount, dateText, type, constrainedSize in + let resultNode: ChatMessageDateAndStatusNode + let resultSizeAndApply: (CGSize, (Bool) -> Void) + if let node = node, let currentLayout = currentLayout { + resultNode = node + resultSizeAndApply = currentLayout(theme, strings, edited, impressionCount, dateText, type, constrainedSize) + } else { + resultNode = ChatMessageDateAndStatusNode() + resultSizeAndApply = resultNode.asyncLayout()(theme, strings, edited, impressionCount, dateText, type, constrainedSize) + } + + return (resultSizeAndApply.0, { animated in + resultSizeAndApply.1(animated) + return resultNode + }) + } + } } diff --git a/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift index 720f22e468..e38e5807ec 100644 --- a/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -94,11 +94,11 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble return .none } - override func updateHiddenMedia(_ media: [Media]?) { - self.contentNode.updateHiddenMedia(media) + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id != messageId { return nil } diff --git a/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift index 9bfe51858f..1f3b93b447 100644 --- a/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift @@ -89,11 +89,11 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent return .none } - override func updateHiddenMedia(_ media: [Media]?) { - self.contentNode.updateHiddenMedia(media) + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id != messageId { return nil } diff --git a/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift index 13fc1e71c2..12198652d0 100644 --- a/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift @@ -96,11 +96,11 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) }) } - override func updateHiddenMedia(_ media: [Media]?) { - self.contentNode.updateHiddenMedia(media) + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id != messageId { return nil } diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index ea3353611b..0d8933300b 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -63,12 +63,12 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getVoice(item.message.id.peerId) } - let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.presentationData, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.account.peerId), statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.presentationData, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.account.peerId), statusType, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) return (contentProperties, nil, initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize, position in - let (refinedWidth, finishLayout) = refineLayout(constrainedSize) + let (refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) return (refinedWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { boundingWidth in let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right) @@ -87,7 +87,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id == messageId { return self.interactiveFileNode.transitionNode(media: media) } else { @@ -95,8 +95,8 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateHiddenMedia(_ media: [Media]?) { - self.interactiveFileNode.updateHiddenMedia(media) + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + return self.interactiveFileNode.updateHiddenMedia(media) } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { diff --git a/TelegramUI/ChatMessageGameBubbleContentNode.swift b/TelegramUI/ChatMessageGameBubbleContentNode.swift index 9a6aef0789..e5e8e89768 100644 --- a/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -117,11 +117,11 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { return .none } - override func updateHiddenMedia(_ media: [Media]?) { - self.contentNode.updateHiddenMedia(media) + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id != messageId { return nil } diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 4665f7c3d0..e4e0b9c166 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -114,7 +114,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { self.view.addGestureRecognizer(replyRecognizer) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let displaySize = CGSize(width: 212.0, height: 212.0) let previousFile = self.telegramFile let layoutConstants = self.layoutConstants @@ -261,7 +261,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { forwardSource = forwardInfo.author forwardAuthorSignature = nil } - let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width + 30.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width + 6.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData.theme, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) if let currentForwardBackgroundNode = currentForwardBackgroundNode { @@ -298,18 +298,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { sentViaBot = true } } - - var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) - - if let author = item.message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } + if let author = item.message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true } + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index a041085278..5c1c7afcb8 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -200,18 +200,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { sentViaBot = true } } - - var dateText = stringForMessageTimestamp(timestamp: message.timestamp, timeFormat: presentationData.timeFormat) - - if let author = message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } + if let author = message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true } + let dateText = stringForMessageTimestampStatus(message: message, timeFormat: presentationData.timeFormat, strings: presentationData.strings) + let (size, apply) = statusLayout(presentationData.theme, presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, constrainedSize) statusSize = size statusApply = apply @@ -304,7 +298,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let maxVoiceLength: CGFloat = 30.0 let minVoiceLength: CGFloat = 2.0 - let minLayoutWidth: CGFloat + var minLayoutWidth: CGFloat if hasThumbnail { minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 86.0 } else if isVoice { @@ -314,6 +308,10 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0 } + if let statusSize = statusSize { + minLayoutWidth = max(minLayoutWidth, statusSize.width) + } + let fileIconImage: UIImage? if hasThumbnail { fileIconImage = nil @@ -368,6 +366,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var statusFrame: CGRect? if let statusSize = statusSize { + fittedLayoutSize.width = max(fittedLayoutSize.width, statusSize.width) statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize) } @@ -597,15 +596,17 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - func transitionNode(media: Media) -> ASDisplayNode? { - if let file = self.file, file.isEqual(media) { - return self.iconNode + func transitionNode(media: Media) -> (ASDisplayNode, () -> UIView?)? { + if let iconNode = self.iconNode, let file = self.file, file.isEqual(media) { + return (iconNode, { [weak iconNode] in + return iconNode?.view.snapshotContentTree(unhide: true) + }) } else { return nil } } - func updateHiddenMedia(_ media: [Media]?) { + func updateHiddenMedia(_ media: [Media]?) -> Bool { var isHidden = false if let file = self.file, let media = media { for m in media { @@ -616,5 +617,6 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } self.iconNode?.isHidden = isHidden + return isHidden } } diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 6a0bed8d80..4ec39ecc99 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -220,7 +220,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { boundingSize = CGSize(width: boundingWidth, height: fittedSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: layoutConstants.image.maxDimensions.height)) boundingSize.height = max(boundingSize.height, layoutConstants.image.minDimensions.height) boundingSize.width = max(boundingSize.width, layoutConstants.image.minDimensions.width) - drawingSize = nativeSize.aspectFitted(boundingSize) + drawingSize = nativeSize.aspectFittedWithOverflow(boundingSize, leeway: 4.0) } case .unconstrained: boundingSize = constrainedSize diff --git a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index 8b4577117b..c4abc3c60b 100644 --- a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -106,11 +106,11 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { return .none } - override func updateHiddenMedia(_ media: [Media]?) { - self.contentNode.updateHiddenMedia(media) + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id != messageId { return nil } diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index fcae81c08e..99e2c3b3f9 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -76,7 +76,7 @@ private func mediaIsNotMergeable(_ media: Media) -> Bool { return false } -private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs: Message) -> Bool { +private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs: Message) -> ChatMessageMerge { var lhsEffectiveAuthor: Peer? = lhs.author var rhsEffectiveAuthor: Peer? = rhs.author if lhs.id.peerId == accountPeerId { @@ -93,27 +93,27 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs if abs(lhs.timestamp - rhs.timestamp) < Int32(5 * 60) && lhsEffectiveAuthor?.id == rhsEffectiveAuthor?.id { for media in lhs.media { if mediaIsNotMergeable(media) { - return false + return .semanticallyMerged } } for media in rhs.media { if mediaIsNotMergeable(media) { - return false + return .semanticallyMerged } } for attribute in lhs.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute { if attribute.flags.contains(.inline) && !attribute.rows.isEmpty { - return false + return .semanticallyMerged } break } } - return true + return .fullyMerged } - return false + return .none } func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) -> Bool{ @@ -156,6 +156,20 @@ public enum ChatMessageItemAdditionalContent { case eventLogPreviousLink(Message) } +enum ChatMessageMerge { + case none + case fullyMerged + case semanticallyMerged + + var merged: Bool { + if case .none = self { + return false + } else { + return true + } + } +} + public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let presentationData: ChatPresentationData let account: Account @@ -303,20 +317,20 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - final func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: Bool, bottom: Bool, dateAtBottom: Bool) { - var mergedTop = false - var mergedBottom = false + final 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 top.header.id != self.header.id { - mergedBottom = false + mergedBottom = .none } else { mergedBottom = messagesShouldBeMerged(accountPeerId: self.account.peerId, message, top.message) } } if let bottom = bottom as? ChatMessageItem { if bottom.header.id != self.header.id { - mergedTop = false + mergedTop = .none dateAtBottom = true } else { mergedTop = messagesShouldBeMerged(accountPeerId: self.account.peerId, bottom.message, message) @@ -325,10 +339,8 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { if bottom.header.id != self.header.id { dateAtBottom = true } - } else if let bottom = bottom as? ChatHoleItem { - //if bottom.header.id != self.header.id { - dateAtBottom = true - //} + } else if let _ = bottom as? ChatHoleItem { + dateAtBottom = true } else { dateAtBottom = true } diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 422660bf5d..fdfe94d7d8 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -133,7 +133,7 @@ public class ChatMessageItemView: ListViewItemNode { } } - func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { return { _, _, _, _, _ in return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _ in @@ -141,7 +141,11 @@ public class ChatMessageItemView: ListViewItemNode { } } - func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { + return nil + } + + func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { return nil } @@ -152,6 +156,13 @@ public class ChatMessageItemView: ListViewItemNode { } func updateHighlightedState(animated: Bool) { + if let item = self.item { + if item.content.firstMessage.stableId == item.controllerInteraction.contextHighlightedState?.messageStableId { + self.isHighligtedInOverlay = true + } else { + self.isHighligtedInOverlay = false + } + } } func updateAutomaticMediaDownloadSettings() { diff --git a/TelegramUI/ChatMessageMapBubbleContentNode.swift b/TelegramUI/ChatMessageMapBubbleContentNode.swift index 27e7b1ddf0..c8cab4b36e 100644 --- a/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -183,6 +183,9 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { sentViaBot = true } } + if let author = item.message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true + } if let selectedMedia = selectedMedia { if selectedMedia.liveBroadcastingTimeout != nil { @@ -190,16 +193,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } - var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) - - if let author = item.message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } - } + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) let statusType: ChatMessageDateAndStatusType? switch position { @@ -435,14 +429,17 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(media) { - return self.imageNode + let imageNode = self.imageNode + return (self.imageNode, { [weak imageNode] in + return imageNode?.view.snapshotContentTree(unhide: true) + }) } return nil } - override func updateHiddenMedia(_ media: [Media]?) { + override func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false if let currentMedia = self.media, let media = media { for item in media { @@ -454,6 +451,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } self.imageNode.isHidden = mediaHidden + return mediaHidden } override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 40e34ccc44..c5dcc9222d 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -73,7 +73,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { bubbleInsets = layoutConstants.image.bubbleInsets } - sizeCalculation = .constrained(CGSize(width: constrainedSize.width, height: constrainedSize.height)) + sizeCalculation = .constrained(CGSize(width: constrainedSize.width - bubbleInsets.left - bubbleInsets.right, height: constrainedSize.height)) case .mosaic: bubbleInsets = UIEdgeInsets() sizeCalculation = .unconstrained @@ -96,7 +96,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: updatedPosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - let (refinedWidth, finishLayout) = refineLayout(constrainedSize, imageCorners) + let (refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - bubbleInsets.left - bubbleInsets.right, height: constrainedSize.height), imageCorners) return (refinedWidth + bubbleInsets.left + bubbleInsets.right, { boundingWidth in let (imageSize, imageApply) = finishLayout(boundingWidth - bubbleInsets.left - bubbleInsets.right) @@ -116,31 +116,11 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { sentViaBot = true } } - - var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) - - var authorTitle: String? - if let author = item.message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - authorTitle = author.displayTitle - } - } else { - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - for attribute in item.message.attributes { - if let attribute = attribute as? AuthorSignatureMessageAttribute { - authorTitle = attribute.signature - break - } - } - } + if let author = item.message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true } - if let authorTitle = authorTitle, !authorTitle.isEmpty { - dateText = "\(authorTitle), \(dateText)" - } + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) let statusType: ChatMessageDateAndStatusType? var statusHorizontalOffset: CGFloat = 0.0 @@ -157,23 +137,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { statusType = .ImageOutgoing(.Sent(read: item.read)) } } - case let .mosaic(position): - if let mosaicStatusHorizontalOffset = position.mosaicStatusHorizontalOffset { - statusHorizontalOffset = mosaicStatusHorizontalOffset - if item.message.effectivelyIncoming(item.account.peerId) { - statusType = .ImageIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .ImageOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .ImageOutgoing(.Sending) - } else { - statusType = .ImageOutgoing(.Sent(read: item.read)) - } - } - } else { - statusType = nil - } + case .mosaic: + statusType = nil default: statusType = nil } @@ -221,18 +186,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let dateAndStatusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width + statusHorizontalOffset, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) - if case .unconstrained = sizeCalculation { - strongSelf.dateAndStatusNode.clipsToBounds = true - - let deltaWidth = dateAndStatusFrame.size.width - layoutSize.width + layoutConstants.image.statusInsets.right - statusHorizontalOffset - let adjustedFrame = CGRect(origin: CGPoint(x: 0.0, y: dateAndStatusFrame.minY), size: CGSize(width: layoutSize.width, height: dateAndStatusFrame.height)) - strongSelf.dateAndStatusNode.frame = adjustedFrame - strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(x: deltaWidth, y: 0.0), size: adjustedFrame.size) - } else { - strongSelf.dateAndStatusNode.clipsToBounds = false - strongSelf.dateAndStatusNode.frame = dateAndStatusFrame - strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) - } + strongSelf.dateAndStatusNode.frame = dateAndStatusFrame + strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() } @@ -272,14 +227,26 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(media) { - return self.interactiveImageNode + let interactiveImageNode = self.interactiveImageNode + return (self.interactiveImageNode, { [weak interactiveImageNode] in + return interactiveImageNode?.view.snapshotContentTree(unhide: true) + }) } return nil } - override func updateHiddenMedia(_ media: [Media]?) { + override func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { + if let message = self.item?.message, let currentMedia = self.media { + if self.interactiveImageNode.frame.contains(point) { + return (message, currentMedia) + } + } + return nil + } + + override func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false if let currentMedia = self.media, let media = media { for item in media { @@ -291,6 +258,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } self.interactiveImageNode.isHidden = mediaHidden + return mediaHidden } override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index c2fc2e44a7..405f002fc5 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -85,7 +85,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let displaySize = CGSize(width: 200.0, height: 200.0) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants @@ -131,7 +131,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { avatarInset = 0.0 } - var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) + var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) if dateHeaderAtBottom { layoutInsets.top += layoutConstants.timestampHeaderHeight } @@ -169,18 +169,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { sentViaBot = true } } - - var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) - - if let author = item.message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } + if let author = item.message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true } + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? @@ -436,6 +430,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } override func updateHighlightedState(animated: Bool) { + super.updateHighlightedState(animated: animated) + if let item = self.item { var highlighted = false if let highlightedState = item.controllerInteraction.highlightedState { diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 900c74f960..ab88b66123 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -83,18 +83,12 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { sentViaBot = true } } - - var dateText = stringForMessageTimestamp(timestamp: item.message.timestamp, timeFormat: item.presentationData.timeFormat) - - if let author = item.message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } + if let author = item.message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true } + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) + let statusType: ChatMessageDateAndStatusType? switch position { case .linear(_, .None): diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 4d922a1fcf..c898b6d159 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -307,7 +307,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return .none } - override func updateHiddenMedia(_ media: [Media]?) { + override func updateHiddenMedia(_ media: [Media]?) -> Bool { if let media = media { var updatedMedia = media for item in media { @@ -324,13 +324,13 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { updatedMedia = mediaList } } - self.contentNode.updateHiddenMedia(updatedMedia) + return self.contentNode.updateHiddenMedia(updatedMedia) } else { - self.contentNode.updateHiddenMedia(nil) + return self.contentNode.updateHiddenMedia(nil) } } - override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.item?.message.id != messageId { return nil } diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 74280beeb4..f9d815781e 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -36,7 +36,9 @@ final class ChatPanelInterfaceInteraction { let setupEditMessage: (MessageId) -> Void let beginMessageSelection: ([MessageId]) -> Void let deleteSelectedMessages: () -> Void + let deleteMessages: ([Message]) -> Void let forwardSelectedMessages: () -> Void + let forwardMessages: ([Message]) -> Void let shareSelectedMessages: () -> Void let updateTextInputState: (@escaping (ChatTextInputState) -> ChatTextInputState) -> Void let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void @@ -72,17 +74,20 @@ final class ChatPanelInterfaceInteraction { let beginCall: () -> Void let toggleMessageStickerStarred: (MessageId) -> Void let presentController: (ViewController, Any?) -> Void + let presentGlobalOverlayController: (ViewController, Any?) -> Void let navigateFeed: () -> Void let openGrouping: () -> Void let toggleSilentPost: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages + self.deleteMessages = deleteMessages self.forwardSelectedMessages = forwardSelectedMessages + self.forwardMessages = forwardMessages self.shareSelectedMessages = shareSelectedMessages self.updateTextInputState = updateTextInputState self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId @@ -118,6 +123,7 @@ final class ChatPanelInterfaceInteraction { self.beginCall = beginCall self.toggleMessageStickerStarred = toggleMessageStickerStarred self.presentController = presentController + self.presentGlobalOverlayController = presentGlobalOverlayController self.navigateFeed = navigateFeed self.openGrouping = openGrouping self.toggleSilentPost = toggleSilentPost diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index 3379ba3a7c..9450008878 100644 --- a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -158,7 +158,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let leftInset: CGFloat = 10.0 var textLineInset: CGFloat = 10.0 let rightInset: CGFloat = 18.0 + rightInset - let textRightInset: CGFloat = 25.0 + let textRightInset: CGFloat = 0.0 var updatedMedia: Media? var imageDimensions: CGSize? @@ -211,9 +211,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: descriptionStringForMessage(message, strings: strings, accountPeerId: accountPeerId), font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: descriptionStringForMessage(message, strings: strings, accountPeerId: accountPeerId), font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) Queue.mainQueue().async { if let strongSelf = self { diff --git a/TelegramUI/ChatRecentActionsController.swift b/TelegramUI/ChatRecentActionsController.swift index 69e209cb60..56ee6db293 100644 --- a/TelegramUI/ChatRecentActionsController.swift +++ b/TelegramUI/ChatRecentActionsController.swift @@ -40,6 +40,7 @@ final class ChatRecentActionsController: ViewController { }, setupEditMessage: { _ in }, beginMessageSelection: { _ in }, deleteSelectedMessages: { + }, deleteMessages: { _ in }, forwardSelectedMessages: { [weak self] in /*if let strongSelf = self { if let forwardMessageIdsSet = strongSelf.interfaceState.selectionState?.selectedIds { @@ -76,6 +77,7 @@ final class ChatRecentActionsController: ViewController { strongSelf.present(controller, in: .window(.root)) } }*/ + }, forwardMessages: { _ in }, shareSelectedMessages: { [weak self] in /*if let strongSelf = self, let selectedIds = strongSelf.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in @@ -133,6 +135,7 @@ final class ChatRecentActionsController: ViewController { }, beginCall: { }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in + }, presentGlobalOverlayController: { _, _ in }, navigateFeed: { }, openGrouping: { }, toggleSilentPost: { diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 5c84f89fab..826fedc29f 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -121,7 +121,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, present: { c, a in self?.presentController(c, a) }, transitionNode: { messageId, media in - var selectedNode: ASDisplayNode? + var selectedNode: (ASDisplayNode, () -> UIView?)? if let strongSelf = self { strongSelf.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { @@ -186,7 +186,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.pushController(searchController) } }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in + }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in if let strongSelf = self { switch action { case let .url(url): @@ -605,7 +605,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { switch result { case let .externalUrl(url): if let navigationController = strongSelf.getNavigationController() { - openExternalUrl(url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: navigationController) + openExternalUrl(account: strongSelf.account, url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: navigationController) } case let .peer(peerId): strongSelf.openPeer(peerId: peerId, peer: nil) @@ -628,6 +628,14 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.openPeer(peerId: peerId, peer: nil) } }), nil) + case .proxy: + openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.getNavigationController(), openPeer: { peerId, _ in + if let strongSelf = self { + strongSelf.openPeer(peerId: peerId, peer: nil) + } + }, present: { c, a in + self?.presentController(c, a) + }) } } })) diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 52d972f2c8..f2b0aed688 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -839,6 +839,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if self.contextPlaceholderNode !== contextPlaceholderNode { contextPlaceholderNode.displaysAsynchronously = false + contextPlaceholderNode.isLayerBacked = true self.contextPlaceholderNode = contextPlaceholderNode self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) } diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 6652b18189..04bb12e6ed 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -36,7 +36,7 @@ private final class ChatTitleNetworkStatusNode: ASDisplayNode { self.titleNode.isOpaque = false self.titleNode.isUserInteractionEnabled = false - self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.secondaryTextColor, 22.0), speed: .slow) + self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5), speed: .slow) let activityIndicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) self.activityIndicator.frame = CGRect(origin: CGPoint(), size: activityIndicatorSize) diff --git a/TelegramUI/ComponentsThemes.swift b/TelegramUI/ComponentsThemes.swift index 563047140c..54a25c2fbc 100644 --- a/TelegramUI/ComponentsThemes.swift +++ b/TelegramUI/ComponentsThemes.swift @@ -38,3 +38,10 @@ public extension AlertControllerTheme { self.init(backgroundColor: authTheme.backgroundColor, separatorColor: authTheme.separatorColor, highlightedItemColor: authTheme.itemHighlightedBackgroundColor, primaryColor: authTheme.primaryColor, secondaryColor: authTheme.textPlaceholderColor, accentColor: authTheme.accentColor, destructiveColor: authTheme.destructiveColor) } } + +extension PeekControllerTheme { + convenience init(presentationTheme: PresentationTheme) { + let actionSheet = presentationTheme.actionSheet + self.init(isDark: actionSheet.backgroundType == .dark, menuBackgroundColor: actionSheet.opaqueItemBackgroundColor, menuItemHighligtedColor: actionSheet.opaqueItemHighlightedBackgroundColor, menuItemSeparatorColor: actionSheet.opaqueItemSeparatorColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor) + } +} diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index 6baccca16d..edff5313d9 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -95,7 +95,7 @@ final class ComposeControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) diff --git a/TelegramUI/ContactMultiselectionControllerNode.swift b/TelegramUI/ContactMultiselectionControllerNode.swift index 508a0e082d..b9bf828fbe 100644 --- a/TelegramUI/ContactMultiselectionControllerNode.swift +++ b/TelegramUI/ContactMultiselectionControllerNode.swift @@ -100,7 +100,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight insets.top += strongSelf.tokenListNode.bounds.size.height - searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: .immediate) + searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: .immediate) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } @@ -149,11 +149,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { insets.top += tokenListHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) if let searchResultsNode = self.searchResultsNode { - searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } } diff --git a/TelegramUI/ContactSelectionControllerNode.swift b/TelegramUI/ContactSelectionControllerNode.swift index ec3b3b3d36..16e552ff44 100644 --- a/TelegramUI/ContactSelectionControllerNode.swift +++ b/TelegramUI/ContactSelectionControllerNode.swift @@ -66,7 +66,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index abeadba7b5..8f3805ba79 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -75,7 +75,7 @@ final class ContactsControllerNode: ASDisplayNode { } } - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) } diff --git a/TelegramUI/DefaultDarkAccentPresentationTheme.swift b/TelegramUI/DefaultDarkAccentPresentationTheme.swift index fb997ea8f8..2e029a6157 100644 --- a/TelegramUI/DefaultDarkAccentPresentationTheme.swift +++ b/TelegramUI/DefaultDarkAccentPresentationTheme.swift @@ -274,7 +274,8 @@ private let actionSheet = PresentationThemeActionSheet( inputBackgroundColor: UIColor(rgb: 0x182330), //!!! inputPlaceholderColor: UIColor(rgb: 0x8B9197), //!!! inputTextColor: .white, - inputClearButtonColor: UIColor(rgb: 0x8B9197) + inputClearButtonColor: UIColor(rgb: 0x8B9197), + checkContentColor: .white ) private let inAppNotification = PresentationThemeInAppNotification( diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index 78107e2859..2ff3a9dddb 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -274,7 +274,8 @@ private let actionSheet = PresentationThemeActionSheet( inputBackgroundColor: UIColor(rgb: 0x545454), //!!! inputPlaceholderColor: UIColor(rgb: 0xaaaaaa), //!!! inputTextColor: .white, - inputClearButtonColor: UIColor(rgb: 0xaaaaaa) + inputClearButtonColor: UIColor(rgb: 0xaaaaaa), + checkContentColor: .black ) private let inAppNotification = PresentationThemeInAppNotification( diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 02d1efa518..60d46ac5c9 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -382,7 +382,8 @@ private let actionSheet = PresentationThemeActionSheet( inputBackgroundColor: UIColor(rgb: 0xe9e9e9), inputPlaceholderColor: UIColor(rgb: 0x818086), inputTextColor: .black, - inputClearButtonColor: UIColor(rgb: 0x7b7b81) + inputClearButtonColor: UIColor(rgb: 0x7b7b81), + checkContentColor: .white ) private let inAppNotification = PresentationThemeInAppNotification( diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index 9058e785bb..ec0a30e191 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -69,7 +69,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isHidden = true - self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlAccentColor, 22.0)) + self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlAccentColor, 22.0, 2.0)) super.init() diff --git a/TelegramUI/EditSettingsController.swift b/TelegramUI/EditSettingsController.swift index d041d6511e..ee6061de97 100644 --- a/TelegramUI/EditSettingsController.swift +++ b/TelegramUI/EditSettingsController.swift @@ -476,7 +476,7 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: (ASDisplayNode, CGRect)? + var result: ((ASDisplayNode, () -> UIView?), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() diff --git a/TelegramUI/FeaturedStickerPacksController.swift b/TelegramUI/FeaturedStickerPacksController.swift index 46f91aa2b3..c4ca922875 100644 --- a/TelegramUI/FeaturedStickerPacksController.swift +++ b/TelegramUI/FeaturedStickerPacksController.swift @@ -108,7 +108,7 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { func item(_ arguments: FeaturedStickerPacksControllerArguments) -> ListViewItem { switch self { case let .pack(_, theme, strings, info, unread, topItem, count, installed): - return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false), enabled: true, sectionId: self.section, action: { + return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, sectionId: self.section, action: { arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { _, _ in }, addPack: { @@ -128,14 +128,6 @@ private struct FeaturedStickerPacksControllerState: Equatable { } } -private func stringForStickerCount(_ count: Int32) -> String { - if count == 1 { - return "1 sticker" - } else { - return "\(count) stickers" - } -} - private func featuredStickerPacksControllerEntries(presentationData: PresentationData, state: FeaturedStickerPacksControllerState, view: CombinedView, featured: [FeaturedStickerPackItem], unreadPacks: [ItemCollectionId: Bool]) -> [FeaturedStickerPacksEntry] { var entries: [FeaturedStickerPacksEntry] = [] @@ -151,7 +143,7 @@ private func featuredStickerPacksControllerEntries(presentationData: Presentatio if let value = unreadPacks[item.info.id] { unread = value } - entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, unread, item.topItems.first, stringForStickerCount(item.info.count), installedPacks.contains(item.info.id))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, unread, item.topItems.first, presentationData.strings.StickerPack_StickerCount(item.info.count), installedPacks.contains(item.info.id))) index += 1 } } diff --git a/TelegramUI/FetchManager.swift b/TelegramUI/FetchManager.swift index f8e9c786d6..2f2de9f9cf 100644 --- a/TelegramUI/FetchManager.swift +++ b/TelegramUI/FetchManager.swift @@ -406,6 +406,8 @@ final class FetchManager { self.withCategoryContext(category, { context in context.cancelEntry(FetchManagerLocationEntryId(location: location, resourceId: resource.id, locationKey: locationKey)) }) + + self.postbox.mediaBox.cancelInteractiveResourceFetch(resource) } } diff --git a/TelegramUI/FetchMediaUtils.swift b/TelegramUI/FetchMediaUtils.swift index b1ab03cc78..7ca96f242c 100644 --- a/TelegramUI/FetchMediaUtils.swift +++ b/TelegramUI/FetchMediaUtils.swift @@ -17,7 +17,6 @@ func messageMediaFileInteractiveFetched(account: Account, messageId: MessageId, func messageMediaFileCancelInteractiveFetch(account: Account, messageId: MessageId, file: TelegramMediaFile) { account.telegramApplicationContext.fetchManager.cancelInteractiveFetches(category: .file, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource) - } func messageMediaFileStatus(account: Account, messageId: MessageId, file: TelegramMediaFile) -> Signal { diff --git a/TelegramUI/FetchVideoMediaResource.swift b/TelegramUI/FetchVideoMediaResource.swift index e3159a8521..27e52cf891 100644 --- a/TelegramUI/FetchVideoMediaResource.swift +++ b/TelegramUI/FetchVideoMediaResource.swift @@ -3,6 +3,24 @@ import Postbox import SwiftSignalKit import LegacyComponents +private final class AVURLAssetCopyItem: MediaResourceDataFetchCopyLocalItem { + private let url: URL + + init(url: URL) { + self.url = url + } + + func copyTo(url: URL) -> Bool { + var success = true + do { + try FileManager.default.copyItem(at: self.url, to: url) + } catch { + success = false + } + return success + } +} + private final class VideoConversionWatcher: TGMediaVideoFileWatcher { private let update: (String, Int) -> Void private var path: String? @@ -45,15 +63,33 @@ public func fetchVideoLibraryMediaResource(resource: VideoLibraryMediaResource) let alreadyReceivedAsset = Atomic(value: false) requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: option, resultHandler: { avAsset, _, _ in + if avAsset == nil { + return + } + if alreadyReceivedAsset.swap(true) { return } var adjustments: TGVideoEditAdjustments? - if let videoAdjustments = resource.adjustments { - if let dict = NSKeyedUnarchiver.unarchiveObject(with: videoAdjustments.data.makeData()) as? [AnyHashable : Any] { - adjustments = TGVideoEditAdjustments(dictionary: dict) - } + switch resource.conversion { + case .passthrough: + if let asset = avAsset as? AVURLAsset { + var value = stat() + if stat(asset.url.path, &value) == 0 { + subscriber.putNext(.copyLocalItem(AVURLAssetCopyItem(url: asset.url))) + subscriber.putCompletion() + } + return + } else { + adjustments = nil + } + case let .compress(adjustmentsValue): + if let adjustmentsValue = adjustmentsValue { + if let dict = NSKeyedUnarchiver.unarchiveObject(with: adjustmentsValue.data.makeData()) as? [AnyHashable : Any] { + adjustments = TGVideoEditAdjustments(dictionary: dict) + } + } } let updatedSize = Atomic(value: 0) let signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, watcher: VideoConversionWatcher(update: { path, size in @@ -175,21 +211,38 @@ public func fetchVideoLibraryMediaResourceHash(resource: VideoLibraryMediaResour option.deliveryMode = .highQualityFormat let alreadyReceivedAsset = Atomic(value: false) - requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: option, resultHandler: { avAsset, _, _ in + requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: option, resultHandler: { avAsset, _, info in + if avAsset == nil { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + if alreadyReceivedAsset.swap(true) { return } var adjustments: TGVideoEditAdjustments? - if let videoAdjustments = resource.adjustments { - if let dict = NSKeyedUnarchiver.unarchiveObject(with: videoAdjustments.data.makeData()) as? [AnyHashable : Any] { - adjustments = TGVideoEditAdjustments(dictionary: dict) - } + var isPassthrough = false + switch resource.conversion { + case .passthrough: + isPassthrough = true + adjustments = nil + case let .compress(adjustmentsValue): + if let adjustmentsValue = adjustmentsValue { + if let dict = NSKeyedUnarchiver.unarchiveObject(with: adjustmentsValue.data.makeData()) as? [AnyHashable : Any] { + adjustments = TGVideoEditAdjustments(dictionary: dict) + } + } } let signal = TGMediaVideoConverter.hash(for: avAsset, adjustments: adjustments)! let signalDisposable = signal.start(next: { next in if let next = next as? String, let data = next.data(using: .utf8) { - subscriber.putNext(data) + var updatedData = data + if isPassthrough { + updatedData.reverse() + } + subscriber.putNext(updatedData) } else { subscriber.putNext(nil) } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 6c21db4b5f..956f49687a 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -108,19 +108,21 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr } final class GalleryTransitionArguments { - let transitionNode: ASDisplayNode + let transitionNode: (ASDisplayNode, () -> UIView?) let addToTransitionSurface: (UIView) -> Void - init(transitionNode: ASDisplayNode, addToTransitionSurface: @escaping (UIView) -> Void) { + init(transitionNode: (ASDisplayNode, () -> UIView?), addToTransitionSurface: @escaping (UIView) -> Void) { self.transitionNode = transitionNode self.addToTransitionSurface = addToTransitionSurface } } final class GalleryControllerPresentationArguments { + let animated: Bool let transitionArguments: (MessageId, Media) -> GalleryTransitionArguments? - init(transitionArguments: @escaping (MessageId, Media) -> GalleryTransitionArguments?) { + init(animated: Bool = true, transitionArguments: @escaping (MessageId, Media) -> GalleryTransitionArguments?) { + self.animated = animated self.transitionArguments = transitionArguments } } @@ -163,6 +165,8 @@ class GalleryController: ViewController { } private var didSetReady = false + private var adjustedForInitialPreviewingLayout = false + var temporaryDoNotWaitForReady = false private let disposable = MetaDisposable() @@ -184,7 +188,7 @@ class GalleryController: ViewController { private var hiddenMediaManagerIndex: Int? - init(account: Account, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { + init(account: Account, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { self.account = account self.replaceRootController = replaceRootController self.baseNavigationController = baseNavigationController @@ -228,75 +232,110 @@ class GalleryController: ViewController { } } |> take(1) - |> deliverOnMainQueue + let semaphore: DispatchSemaphore? + if synchronousLoad { + semaphore = DispatchSemaphore(value: 0) + } else { + semaphore = nil + } + + let syncResult = Atomic<(Bool, (() -> Void)?)>(value: (false, nil)) self.disposable.set(messageView.start(next: { [weak self] view in - if let strongSelf = self { - if let view = view { - let entries = view.entries.filter { entry in - if case .MessageEntry = entry { - return true + let f: () -> Void = { + if let strongSelf = self { + if let view = view { + let entries = view.entries.filter { entry in + if case .MessageEntry = entry { + return true + } else { + return false + } + } + var centralEntryStableId: UInt32? + loop: for i in 0 ..< entries.count { + switch entries[i] { + case let .MessageEntry(message, _, _, _): + switch source { + case let .peerMessagesAtId(messageId): + if message.id == messageId { + centralEntryStableId = message.stableId + break loop + } + case let .standaloneMessage(m): + if message.id == m.id { + centralEntryStableId = message.stableId + break loop + } + } + default: + break + } + } + if invertItemOrder { + strongSelf.entries = entries.reversed() + if let centralEntryStableId = centralEntryStableId { + strongSelf.centralEntryStableId = centralEntryStableId + } } else { - return false - } - } - var centralEntryStableId: UInt32? - loop: for i in 0 ..< entries.count { - switch entries[i] { - case let .MessageEntry(message, _, _, _): - switch source { - case let .peerMessagesAtId(messageId): - if message.id == messageId { - centralEntryStableId = message.stableId - break loop - } - case let .standaloneMessage(m): - if message.id == m.id { - centralEntryStableId = message.stableId - break loop - } - } - default: - break - } - } - if invertItemOrder { - strongSelf.entries = entries.reversed() - if let centralEntryStableId = centralEntryStableId { + strongSelf.entries = entries strongSelf.centralEntryStableId = centralEntryStableId } - } else { - strongSelf.entries = entries - strongSelf.centralEntryStableId = centralEntryStableId - } - if strongSelf.isViewLoaded { - var items: [GalleryItem] = [] - var centralItemIndex: Int? - for entry in strongSelf.entries { - if let item = galleryItemForEntry(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, entry: entry, streamVideos: streamSingleVideo) { - if case let .MessageEntry(message, _, _, _) = entry, message.stableId == strongSelf.centralEntryStableId { - centralItemIndex = items.count + if strongSelf.isViewLoaded { + var items: [GalleryItem] = [] + var centralItemIndex: Int? + for entry in strongSelf.entries { + if let item = galleryItemForEntry(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, entry: entry, streamVideos: streamSingleVideo) { + if case let .MessageEntry(message, _, _, _) = entry, message.stableId == strongSelf.centralEntryStableId { + centralItemIndex = items.count + } + items.append(item) } - items.append(item) } - } - - strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex) - - if strongSelf.temporaryDoNotWaitForReady { - strongSelf.didSetReady = true - strongSelf._ready.set(.single(true)) - } else { - let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in - strongSelf?.didSetReady = true + + strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex) + + if strongSelf.temporaryDoNotWaitForReady { + strongSelf.didSetReady = true + strongSelf._ready.set(.single(true)) + } else { + let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in + strongSelf?.didSetReady = true + } + strongSelf._ready.set(ready |> map { true }) } - strongSelf._ready.set(ready |> map { true }) } } } } + var process = false + let _ = syncResult.modify { processed, _ in + if !processed { + return (processed, f) + } + process = true + return (true, nil) + } + semaphore?.signal() + if process { + Queue.mainQueue().async { + f() + } + } })) + if let semaphore = semaphore { + let _ = semaphore.wait(timeout: DispatchTime.now() + 1.0) + } + + var syncResultApply: (() -> Void)? + let _ = syncResult.modify { processed, f in + syncResultApply = f + return (true, nil) + } + + syncResultApply?() + self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in self?.navigationItem.title = title })) @@ -499,12 +538,16 @@ class GalleryController: ViewController { } } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) var nodeAnimatesItself = false - if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { + if let centralItemNode = self.galleryNode.pager.centralItemNode() { if case let .MessageEntry(message, _, _, _) = self.entries[centralItemNode.index] { self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) @@ -512,17 +555,29 @@ class GalleryController: ViewController { self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) - if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) { - nodeAnimatesItself = true - centralItemNode.activateAsInitial() - centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface) - - self._hiddenMedia.set(.single((message.id, media))) + if let media = mediaForMessage(message: message) { + if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) { + nodeAnimatesItself = true + centralItemNode.activateAsInitial() + + if presentationArguments.animated { + centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface) + } + + self._hiddenMedia.set(.single((message.id, media))) + } } } } - self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) + if !self.isPresentedInPreviewingContext() { + self.galleryNode.setControlsHidden(false, animated: false) + if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { + if presentationArguments.animated { + self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) + } + } + } } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -530,5 +585,15 @@ class GalleryController: ViewController { self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + + if !self.adjustedForInitialPreviewingLayout && self.isPresentedInPreviewingContext() { + self.adjustedForInitialPreviewingLayout = true + self.galleryNode.setControlsHidden(true, animated: false) + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() { + self.preferredContentSize = itemSize.aspectFitted(self.view.bounds.size) + self.containerLayoutUpdated(ContainerViewLayout(size: self.preferredContentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + centralItemNode.activateAsInitial() + } + } } } diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index 5068477775..a1208a622f 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -8,7 +8,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog var navigationBar: NavigationBar? let footerNode: GalleryFooterNode var toolbarNode: ASDisplayNode? - var transitionDataForCentralItem: (() -> (ASDisplayNode?, (UIView) -> Void)?)? + var transitionDataForCentralItem: (() -> ((ASDisplayNode, () -> UIView?)?, (UIView) -> Void)?)? var dismiss: (() -> Void)? var containerLayout: (CGFloat, ContainerViewLayout)? @@ -52,13 +52,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog self.pager.toggleControlsVisibility = { [weak self] in if let strongSelf = self { - strongSelf.areControlsHidden = !strongSelf.areControlsHidden - UIView.animate(withDuration: 0.3, animations: { - let alpha: CGFloat = strongSelf.areControlsHidden ? 0.0 : 1.0 - strongSelf.navigationBar?.alpha = alpha - strongSelf.statusBar?.alpha = alpha - strongSelf.footerNode.alpha = alpha - }) + strongSelf.setControlsHidden(!strongSelf.areControlsHidden, animated: true) } } @@ -125,6 +119,23 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog self.pager.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } + func setControlsHidden(_ hidden: Bool, animated: Bool) { + self.areControlsHidden = hidden + if animated { + UIView.animate(withDuration: 0.3, animations: { + let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 + self.navigationBar?.alpha = alpha + self.statusBar?.alpha = alpha + self.footerNode.alpha = alpha + }) + } else { + let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 + self.navigationBar?.alpha = alpha + self.statusBar?.alpha = alpha + self.footerNode.alpha = alpha + } + } + func animateIn(animateContent: Bool) { self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(0.0) self.statusBar?.alpha = 0.0 diff --git a/TelegramUI/GalleryItemNode.swift b/TelegramUI/GalleryItemNode.swift index 3ec07d955e..17229cdcfc 100644 --- a/TelegramUI/GalleryItemNode.swift +++ b/TelegramUI/GalleryItemNode.swift @@ -68,9 +68,13 @@ open class GalleryItemNode: ASDisplayNode { open func visibilityUpdated(isVisible: Bool) { } - open func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { + open func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { } - open func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + open func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + } + + open func contentSize() -> CGSize? { + return nil } } diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index 2d2361d280..c63e8f01d7 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -306,9 +306,12 @@ final class GridMessageItemNode: GridItemNode { } } - func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if self.messageId == id { - return self.imageNode + let imageNode = self.imageNode + return (self.imageNode, { [weak imageNode] in + return imageNode?.view.snapshotContentTree(unhide: true) + }) } else { return nil } diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 8d8cfc8f8d..63fd85b78d 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -1531,7 +1531,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: (ASDisplayNode, CGRect)? + var result: ((ASDisplayNode, () -> UIView?), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() diff --git a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift index 46cfeca3a3..1317a59833 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -55,6 +55,7 @@ private func preparedTransition(from fromEntries: [HorizontalListContextResultsC final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { private var theme: PresentationTheme + private var strings: PresentationStrings private let listView: ListView private let separatorNode: ASDisplayNode @@ -66,6 +67,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme + self.strings = strings self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -91,6 +93,37 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont super.didLoad() self.listView.view.disablesInteractiveTransitionGestureRecognizer = true + self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in + if let strongSelf = self { + let convertedPoint = strongSelf.listView.view.convert(point, from: strongSelf.view) + + if !strongSelf.listView.bounds.contains(convertedPoint) { + return nil + } + + var selectedItemNodeAndContent: (ASDisplayNode, PeekControllerContent)? + strongSelf.listView.forEachItemNode { itemNode in + if itemNode.frame.contains(convertedPoint), let itemNode = itemNode as? HorizontalListContextResultsChatInputPanelItemNode, let item = itemNode.item { + selectedItemNodeAndContent = (itemNode, ChatContextResultPeekContent(account: item.account, contextResult: item.result, menu: [ + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + item.resultSelected(item.result) + }) + ])) + } + } + return .single(selectedItemNodeAndContent) + } + return nil + }, present: { [weak self] content, sourceNode in + if let strongSelf = self { + let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + return sourceNode + }) + strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) + return controller + } + return nil + })) } func updateResults(_ results: ChatContextResultCollection) { diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index 3e90fb42e7..0308ad1431 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -6,9 +6,9 @@ import SwiftSignalKit import Postbox final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { - fileprivate let account: Account - fileprivate let result: ChatContextResult - private let resultSelected: (ChatContextResult) -> Void + let account: Account + let result: ChatContextResult + let resultSelected: (ChatContextResult) -> Void let selectable: Bool = true @@ -80,6 +80,8 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode private var currentImageResource: TelegramMediaResource? private var currentVideoFile: TelegramMediaFile? + private(set) var item: HorizontalListContextResultsChatInputPanelItem? + override var visibility: ListViewItemNodeVisibility { didSet { switch visibility { @@ -275,6 +277,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode return (nodeLayout, { _ in if let strongSelf = self { + strongSelf.item = item strongSelf.currentImageResource = imageResource strongSelf.currentVideoFile = videoFile diff --git a/TelegramUI/InAppNotificationSettings.swift b/TelegramUI/InAppNotificationSettings.swift index 9497bf2433..7577191554 100644 --- a/TelegramUI/InAppNotificationSettings.swift +++ b/TelegramUI/InAppNotificationSettings.swift @@ -2,43 +2,56 @@ import Foundation import Postbox import SwiftSignalKit +public enum TotalUnreadCountDisplayStyle: Int32 { + case filtered = 0 + case raw = 1 +} + public struct InAppNotificationSettings: PreferencesEntry, Equatable { public let playSounds: Bool public let vibrate: Bool public let displayPreviews: Bool + public let totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle public static var defaultSettings: InAppNotificationSettings { - return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true) + return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered) } - init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool) { + init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle) { self.playSounds = playSounds self.vibrate = vibrate self.displayPreviews = displayPreviews + self.totalUnreadCountDisplayStyle = totalUnreadCountDisplayStyle } public init(decoder: PostboxDecoder) { self.playSounds = decoder.decodeInt32ForKey("s", orElse: 0) != 0 self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0 self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 + self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("tds", orElse: 0)) ?? .filtered } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.playSounds ? 1 : 0, forKey: "s") encoder.encodeInt32(self.vibrate ? 1 : 0, forKey: "v") encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p") + encoder.encodeInt32(self.totalUnreadCountDisplayStyle.rawValue, forKey: "tds") } func withUpdatedPlaySounds(_ playSounds: Bool) -> InAppNotificationSettings { - return InAppNotificationSettings(playSounds: playSounds, vibrate: self.vibrate, displayPreviews: self.displayPreviews) + return InAppNotificationSettings(playSounds: playSounds, vibrate: self.vibrate, displayPreviews: self.displayPreviews, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle) } func withUpdatedVibrate(_ vibrate: Bool) -> InAppNotificationSettings { - return InAppNotificationSettings(playSounds: self.playSounds, vibrate: vibrate, displayPreviews: self.displayPreviews) + return InAppNotificationSettings(playSounds: self.playSounds, vibrate: vibrate, displayPreviews: self.displayPreviews, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle) } func withUpdatedDisplayPreviews(_ displayPreviews: Bool) -> InAppNotificationSettings { - return InAppNotificationSettings(playSounds: self.playSounds, vibrate: self.vibrate, displayPreviews: displayPreviews) + return InAppNotificationSettings(playSounds: self.playSounds, vibrate: self.vibrate, displayPreviews: displayPreviews, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle) + } + + func withUpdatedTotalUnreadCountDisplayStyle(_ totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle) -> InAppNotificationSettings { + return InAppNotificationSettings(playSounds: self.playSounds, vibrate: self.vibrate, displayPreviews: self.displayPreviews, totalUnreadCountDisplayStyle: totalUnreadCountDisplayStyle) } public func isEqual(to: PreferencesEntry) -> Bool { @@ -59,6 +72,9 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { if lhs.displayPreviews != rhs.displayPreviews { return false } + if lhs.totalUnreadCountDisplayStyle != rhs.totalUnreadCountDisplayStyle { + return false + } return true } } diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index 0d89d43d33..cf5a4c2cf1 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -321,7 +321,7 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati var index: Int32 = 0 for entry in packsEntries { if let info = entry.info as? StickerPackCollectionInfo { - entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, stringForStickerCount(info.count == 0 ? entry.count : info.count), true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, stringForStickerCount(info.count == 0 ? entry.count : info.count), true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id, reorderable: true))) index += 1 } } @@ -436,6 +436,9 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti updateState { $0.withUpdatedEditing(false) } + if case .modal = mode { + dismissImpl?() + } }) } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { @@ -458,6 +461,68 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti } let controller = ItemListController(account: account, state: signal) + + controller.reorderEntry = { fromIndex, toIndex, entries in + let fromEntry = entries[fromIndex] + guard case let .pack(_, _, _, fromPackInfo, _, _, _, _) = fromEntry else { + return + } + var referenceId: ItemCollectionId? + var beforeAll = false + var afterAll = false + if toIndex < entries.count { + switch entries[toIndex] { + case let .pack(_, _, _, toPackInfo, _, _, _, _): + referenceId = toPackInfo.id + default: + if entries[toIndex] < fromEntry { + beforeAll = true + } else { + afterAll = true + } + } + } else { + afterAll = true + } + + let _ = (account.postbox.modify { modifier -> Void in + var infos = modifier.getItemCollectionsInfos(namespace: namespaceForMode(mode)) + var reorderInfo: ItemCollectionInfo? + for i in 0 ..< infos.count { + if infos[i].0 == fromPackInfo.id { + reorderInfo = infos[i].1 + infos.remove(at: i) + break + } + } + if let reorderInfo = reorderInfo { + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< infos.count { + if infos[i].0 == referenceId { + if fromIndex < toIndex { + infos.insert((fromPackInfo.id, reorderInfo), at: i + 1) + } else { + infos.insert((fromPackInfo.id, reorderInfo), at: i) + } + inserted = true + break + } + } + if !inserted { + infos.append((fromPackInfo.id, reorderInfo)) + } + } else if beforeAll { + infos.insert((fromPackInfo.id, reorderInfo), at: 0) + } else if afterAll { + infos.append((fromPackInfo.id, reorderInfo)) + } + modifier.replaceItemCollectionInfos(namespace: namespaceForMode(mode), itemCollectionInfos: infos) + addSynchronizeInstalledStickerPacksOperation(modifier: modifier, namespace: namespaceForMode(mode)) + } + }).start() + } + presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift index 94c9f894db..3c00955816 100644 --- a/TelegramUI/InstantImageGalleryItem.swift +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -116,13 +116,13 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { self.accountAndMedia = (account, file) } - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) - let copyView = node.view.snapshotContentTree()! + let copyView = node.1()! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame @@ -142,17 +142,17 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) var positionCompleted = false var boundsCompleted = false var copyCompleted = false - let copyView = node.view.snapshotContentTree()! + let copyView = node.1()! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame diff --git a/TelegramUI/InstantPageAudioNode.swift b/TelegramUI/InstantPageAudioNode.swift index da8ea80def..9fc87061be 100644 --- a/TelegramUI/InstantPageAudioNode.swift +++ b/TelegramUI/InstantPageAudioNode.swift @@ -214,7 +214,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { return nil } diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 2df8dfda0e..c527560e23 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -706,7 +706,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } })) } else { - openExternalUrl(url: externalUrl, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.getNavigationController()) + openExternalUrl(account: strongSelf.account, url: externalUrl, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.getNavigationController()) } default: openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.getNavigationController(), openPeer: { peerId, navigation in diff --git a/TelegramUI/InstantPageImageNode.swift b/TelegramUI/InstantPageImageNode.swift index 3168f429e7..7047d6d5d7 100644 --- a/TelegramUI/InstantPageImageNode.swift +++ b/TelegramUI/InstantPageImageNode.swift @@ -82,9 +82,12 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { if media == self.media { - return self.imageNode + let imageNode = self.imageNode + return (self.imageNode, { [weak imageNode] in + return imageNode?.view.snapshotContentTree(unhide: true) + }) } else { return nil } diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index 65b01afc5a..ad781a490d 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -126,11 +126,11 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo let backgroundInset: CGFloat = 14.0 let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0) item.frame = item.frame.offsetBy(dx: horizontalInset, dy: backgroundInset) - let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shape: .rect, color: UIColor(rgb: 0xF5F8FC)) + let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shape: .rect, color: theme.codeBlockBackgroundColor) return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0), items: [backgroundItem, item]) case let .footer(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) @@ -206,7 +206,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo } contentSize.height += verticalInset - let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: UIColor.black) + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: theme.textCategories.paragraph.color) items.append(shapeItem) @@ -443,7 +443,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo contentSize.height += verticalInset - items.append(InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: .black)) + items.append(InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: theme.textCategories.paragraph.color)) if case .empty = caption { } else { diff --git a/TelegramUI/InstantPageNode.swift b/TelegramUI/InstantPageNode.swift index 5f87117859..fd2c5c8549 100644 --- a/TelegramUI/InstantPageNode.swift +++ b/TelegramUI/InstantPageNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit protocol InstantPageNode { func updateIsVisible(_ isVisible: Bool) - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? func updateHiddenMedia(media: InstantPageMedia?) func update(strings: PresentationStrings, theme: InstantPageTheme) } diff --git a/TelegramUI/InstantPagePeerReferenceNode.swift b/TelegramUI/InstantPagePeerReferenceNode.swift index 0d727ce7ce..1361583462 100644 --- a/TelegramUI/InstantPagePeerReferenceNode.swift +++ b/TelegramUI/InstantPagePeerReferenceNode.swift @@ -85,7 +85,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { self.joinNode = HighlightableButtonNode() self.joinNode.hitTestSlop = UIEdgeInsets(top: -17.0, left: -17.0, bottom: -17.0, right: -17.0) - self.activityIndicator = ActivityIndicator(type: .custom(theme.panelAccentColor, 22.0)) + self.activityIndicator = ActivityIndicator(type: .custom(theme.panelAccentColor, 22.0, 2.0)) self.checkNode = ASImageNode() self.checkNode.isLayerBacked = true @@ -177,7 +177,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { if themeUpdated { self.checkNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/PanelCheck"), color: self.theme.panelSecondaryColor) - self.activityIndicator.type = .custom(self.theme.panelAccentColor, 22.0) + self.activityIndicator.type = .custom(self.theme.panelAccentColor, 22.0, 2.0) } self.setNeedsLayout() } @@ -242,7 +242,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { return nil } diff --git a/TelegramUI/InstantPagePlayableVideoNode.swift b/TelegramUI/InstantPagePlayableVideoNode.swift index b882b0e746..e09383af67 100644 --- a/TelegramUI/InstantPagePlayableVideoNode.swift +++ b/TelegramUI/InstantPagePlayableVideoNode.swift @@ -95,9 +95,12 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { if media == self.media { - return self.videoNode + let videoNode = self.videoNode + return (self.videoNode, { [weak videoNode] in + return videoNode?.view.snapshotContentTree(unhide: true) + }) } else { return nil } diff --git a/TelegramUI/InstantPageSlideshowItemNode.swift b/TelegramUI/InstantPageSlideshowItemNode.swift index 6ccf5180cc..cff2a03569 100644 --- a/TelegramUI/InstantPageSlideshowItemNode.swift +++ b/TelegramUI/InstantPageSlideshowItemNode.swift @@ -50,7 +50,7 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode { } } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { if let node = self.contentNode as? InstantPageNode { return node.transitionNode(media: media) } @@ -352,7 +352,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe } } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { for node in self.itemNodes { if let transitionNode = node.transitionNode(media: media) { return transitionNode @@ -395,12 +395,12 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { super.layout() self.pagerNode.frame = self.bounds - self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), transition: .immediate) + self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) self.pageControlNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height - 20.0), size: CGSize(width: self.bounds.size.width, height: 20.0)) } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { return self.pagerNode.transitionNode(media: media) } diff --git a/TelegramUI/InstantPageTheme.swift b/TelegramUI/InstantPageTheme.swift index f8dd8a713e..4215bd7c7c 100644 --- a/TelegramUI/InstantPageTheme.swift +++ b/TelegramUI/InstantPageTheme.swift @@ -68,6 +68,8 @@ final class InstantPageTheme { let textCategories: InstantPageTextCategories + let codeBlockBackgroundColor: UIColor + let textHighlightColor: UIColor let linkHighlightColor: UIColor @@ -77,9 +79,10 @@ final class InstantPageTheme { let panelSecondaryColor: UIColor let panelAccentColor: UIColor - init(pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, textHighlightColor: UIColor, linkHighlightColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor) { + init(pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, codeBlockBackgroundColor: UIColor, textHighlightColor: UIColor, linkHighlightColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor) { self.pageBackgroundColor = pageBackgroundColor self.textCategories = textCategories + self.codeBlockBackgroundColor = codeBlockBackgroundColor self.textHighlightColor = textHighlightColor self.linkHighlightColor = linkHighlightColor self.panelBackgroundColor = panelBackgroundColor @@ -90,7 +93,7 @@ final class InstantPageTheme { } func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme { - return InstantPageTheme(pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor) + return InstantPageTheme(pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), codeBlockBackgroundColor: codeBlockBackgroundColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor) } } @@ -102,6 +105,7 @@ private let lightTheme = InstantPageTheme( paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: .black), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x79828b)) ), + codeBlockBackgroundColor: UIColor(rgb: 0xf5f8fc), textHighlightColor: UIColor(rgb: 0, alpha: 0.12), linkHighlightColor: UIColor(rgb: 0, alpha: 0.12), panelBackgroundColor: UIColor(rgb: 0xf3f4f5), @@ -119,6 +123,7 @@ private let sepiaTheme = InstantPageTheme( paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x927e6b)) ), + codeBlockBackgroundColor: UIColor(rgb: 0xefe7d6), textHighlightColor: UIColor(rgb: 0, alpha: 0.1), linkHighlightColor: UIColor(rgb: 0, alpha: 0.1), panelBackgroundColor: UIColor(rgb: 0xefe7d6), @@ -136,6 +141,7 @@ private let grayTheme = InstantPageTheme( paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xa0a0a0)) ), + codeBlockBackgroundColor: UIColor(rgb: 0x555556), textHighlightColor: UIColor(rgb: 0, alpha: 0.16), linkHighlightColor: UIColor(rgb: 0, alpha: 0.16), panelBackgroundColor: UIColor(rgb: 0x555556), @@ -153,6 +159,7 @@ private let darkTheme = InstantPageTheme( paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x6a6a6a)) ), + codeBlockBackgroundColor: UIColor(rgb: 0x131313), textHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), panelBackgroundColor: UIColor(rgb: 0x131313), diff --git a/TelegramUI/InstantPageTileNode.swift b/TelegramUI/InstantPageTileNode.swift index 3d35a943be..4993f0e5ed 100644 --- a/TelegramUI/InstantPageTileNode.swift +++ b/TelegramUI/InstantPageTileNode.swift @@ -22,7 +22,7 @@ final class InstantPageTileNode: ASDisplayNode { super.init() self.isLayerBacked = true - self.isOpaque = true + self.isOpaque = false self.backgroundColor = backgroundColor } diff --git a/TelegramUI/InstantPageWebEmbedNode.swift b/TelegramUI/InstantPageWebEmbedNode.swift index 8060e3ee8a..288f8acaf3 100644 --- a/TelegramUI/InstantPageWebEmbedNode.swift +++ b/TelegramUI/InstantPageWebEmbedNode.swift @@ -41,7 +41,7 @@ final class instantPageWebEmbedNode: ASDisplayNode, InstantPageNode { self.webView.frame = self.bounds } - func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { return nil } diff --git a/TelegramUI/ItemListActivityTextItem.swift b/TelegramUI/ItemListActivityTextItem.swift index 39fe56f760..9f8f1cf91d 100644 --- a/TelegramUI/ItemListActivityTextItem.swift +++ b/TelegramUI/ItemListActivityTextItem.swift @@ -67,7 +67,7 @@ class ItemListActivityTextItemNode: ListViewItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale - self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0)) + self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0)) super.init(layerBacked: false, dynamicBounce: false) @@ -110,7 +110,7 @@ class ItemListActivityTextItemNode: ListViewItemNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: CGSize(width: 16.0, height: 16.0)) - strongSelf.activityIndicator.type = .custom(item.theme.list.itemAccentColor, 16.0) + strongSelf.activityIndicator.type = .custom(item.theme.list.itemAccentColor, 16.0, 2.0) if item.displayActivity { strongSelf.activityIndicator.isHidden = false diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index dc86ff760e..82eb4df390 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -838,8 +838,11 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite } } - func avatarTransitionNode() -> (ASDisplayNode, CGRect) { - return (self.avatarNode, self.avatarNode.bounds) + func avatarTransitionNode() -> ((ASDisplayNode, () -> UIView?), CGRect) { + let avatarNode = self.avatarNode + return ((self.avatarNode, { [weak avatarNode] in + return avatarNode?.view.snapshotContentTree(unhide: true) + }), self.avatarNode.bounds) } func updateAvatarHidden() { diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 8391831a61..f2e9af9d7e 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -159,7 +159,17 @@ final class ItemListController: ViewController { var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? { didSet { - (self.displayNode as! ItemListControllerNode).visibleEntriesUpdated = self.visibleEntriesUpdated + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).visibleEntriesUpdated = self.visibleEntriesUpdated + } + } + } + + var reorderEntry: ((Int, Int, [Entry]) -> Void)? { + didSet { + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).reorderEntry = self.reorderEntry + } } } @@ -352,6 +362,8 @@ final class ItemListController: ViewController { self?.presentingViewController?.dismiss(animated: true, completion: nil) } displayNode.enableInteractiveDismiss = self.enableInteractiveDismiss + displayNode.visibleEntriesUpdated = self.visibleEntriesUpdated + displayNode.reorderEntry = self.reorderEntry self.displayNode = displayNode super.displayNodeDidLoad() self._ready.set((self.displayNode as! ItemListControllerNode).ready) diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index dbaad39e65..e3f5fa787a 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -115,6 +115,7 @@ class ItemListControllerNode: ViewControllerTracingNod var dismiss: (() -> Void)? var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? + var reorderEntry: ((Int, Int, [Entry]) -> Void)? var enableInteractiveDismiss = false { didSet { @@ -153,6 +154,14 @@ class ItemListControllerNode: ViewControllerTracingNod } } + self.listNode.reorderItem = { [weak self] fromIndex, toIndex, opaqueTransactionState in + if let strongSelf = self, let reorderEntry = strongSelf.reorderEntry, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries { + if fromIndex >= 0 && fromIndex < mergedEntries.count && toIndex >= 0 && toIndex < mergedEntries.count { + reorderEntry(fromIndex, toIndex, mergedEntries) + } + } + } + let previousState = Atomic?>(value: nil) self.transitionDisposable.set(((state |> map { theme, stateAndArguments -> ItemListNodeTransition in let (state, arguments) = stateAndArguments diff --git a/TelegramUI/ItemListEditableReorderControlNode.swift b/TelegramUI/ItemListEditableReorderControlNode.swift new file mode 100644 index 0000000000..9e54bcf782 --- /dev/null +++ b/TelegramUI/ItemListEditableReorderControlNode.swift @@ -0,0 +1,39 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ItemListEditableReorderControlNode: ASDisplayNode { + var tapped: (() -> Void)? + private let iconNode: ASImageNode + + override init() { + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.iconNode) + } + + static func asyncLayout(_ node: ItemListEditableReorderControlNode?) -> (_ height: CGFloat, _ theme: PresentationTheme) -> (CGSize, () -> ItemListEditableReorderControlNode) { + return { height, theme in + let image = PresentationResourcesItemList.itemListReorderIndicatorIcon(theme) + + let resultNode: ItemListEditableReorderControlNode + if let node = node { + resultNode = node + } else { + resultNode = ItemListEditableReorderControlNode() + resultNode.iconNode.image = image + } + + return (CGSize(width: 44.0, height: height), { + if let image = image { + resultNode.iconNode.frame = CGRect(origin: CGPoint(x: 7.0, y: floor((height - image.size.height) / 2.0)), size: image.size) + } + return resultNode + }) + } + } +} + diff --git a/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift b/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift index f4e5963220..2fd38b638c 100644 --- a/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift +++ b/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift @@ -26,7 +26,7 @@ final class ItemListLoadingIndicatorEmptyStateItem: ItemListControllerEmptyState final class ItemListLoadingIndicatorEmptyStateItemNode: ItemListControllerEmptyStateItemNode { var theme: PresentationTheme { didSet { - self.indicator.type = .custom(self.theme.list.itemAccentColor, 40.0) + self.indicator.type = .custom(self.theme.list.itemAccentColor, 40.0, 2.0) } } private let indicator: ActivityIndicator @@ -35,7 +35,7 @@ final class ItemListLoadingIndicatorEmptyStateItemNode: ItemListControllerEmptyS init(theme: PresentationTheme) { self.theme = theme - self.indicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 40.0)) + self.indicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 40.0, 2.0)) super.init() diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index 3f63e3e701..41165b1109 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -9,6 +9,7 @@ struct ItemListStickerPackItemEditing: Equatable { let editable: Bool let editing: Bool let revealed: Bool + let reorderable: Bool static func ==(lhs: ItemListStickerPackItemEditing, rhs: ItemListStickerPackItemEditing) -> Bool { if lhs.editable != rhs.editable { @@ -20,6 +21,9 @@ struct ItemListStickerPackItemEditing: Equatable { if lhs.revealed != rhs.revealed { return false } + if lhs.reorderable != rhs.reorderable { + return false + } return true } } @@ -145,6 +149,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { private var layoutParams: (ItemListStickerPackItem, ListViewItemLayoutParams, ItemListNeighbors)? private var editableControlNode: ItemListEditableControlNode? + private var reorderControlNode: ItemListEditableReorderControlNode? private let fetchDisposable = MetaDisposable() @@ -217,6 +222,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let previousFile = self.layoutParams?.0.topItem?.file var currentDisabledOverlayNode = self.disabledOverlayNode @@ -265,20 +271,6 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let leftInset: CGFloat = 65.0 + params.leftInset - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? - - let editingOffset: CGFloat - if item.editing.editing { - let sizeAndApply = editableControlLayout(59.0, item.theme, false) - editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width - } else { - editingOffset = 0.0 - } - - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0, 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, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let insets = itemListNeighborsGroupedInsets(neighbors) let contentSize = CGSize(width: params.width, height: 59.0) let separatorHeight = UIScreenPixel @@ -286,6 +278,27 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size + var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var reorderControlSizeAndApply: (CGSize, () -> ItemListEditableReorderControlNode)? + + var editingOffset: CGFloat = 0.0 + var reorderInset: CGFloat = 0.0 + + if item.editing.editing { + let sizeAndApply = editableControlLayout(59.0, item.theme, false) + editableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0.width + + if item.editing.reorderable { + let sizeAndApply = reorderControlLayout(contentSize.height, item.theme) + reorderControlSizeAndApply = sizeAndApply + reorderInset = sizeAndApply.0.width + } + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0 - reorderInset, 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 - reorderInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() @@ -387,6 +400,23 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { }) } + if let reorderControlSizeAndApply = reorderControlSizeAndApply { + if strongSelf.reorderControlNode == nil { + let reorderControlNode = reorderControlSizeAndApply.1() + strongSelf.reorderControlNode = reorderControlNode + strongSelf.addSubnode(reorderControlNode) + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0.width, y: 0.0), size: reorderControlSizeAndApply.0) + reorderControlNode.frame = reorderControlFrame + reorderControlNode.alpha = 0.0 + transition.updateAlpha(node: reorderControlNode, alpha: 1.0) + } + } else if let reorderControlNode = strongSelf.reorderControlNode { + strongSelf.reorderControlNode = nil + transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in + reorderControlNode?.removeFromSupernode() + }) + } + imageApply?() let _ = titleApply() @@ -568,4 +598,11 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { item.addPack() } } + + override func isReorderable(at point: CGPoint) -> Bool { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) { + return true + } + return false + } } diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index 7818985c94..af5e99bd75 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -7,15 +7,17 @@ class ItemListSwitchItem: ListViewItem, ItemListItem { let theme: PresentationTheme let title: String let value: Bool + let enableInteractiveChanges: Bool let enabled: Bool let sectionId: ItemListSectionId let style: ItemListStyle let updated: (Bool) -> Void - init(theme: PresentationTheme, title: String, value: Bool, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { + init(theme: PresentationTheme, title: String, value: Bool, enableInteractiveChanges: Bool = true, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { self.theme = theme self.title = title self.value = value + self.enableInteractiveChanges = enableInteractiveChanges self.enabled = enabled self.sectionId = sectionId self.style = style @@ -67,6 +69,7 @@ class ItemListSwitchItemNode: ListViewItemNode { private let titleNode: TextNode private var switchNode: SwitchNode + private let switchGestureNode: ASDisplayNode private var disabledOverlayNode: ASDisplayNode? private var item: ItemListSwitchItem? @@ -86,16 +89,20 @@ class ItemListSwitchItemNode: ListViewItemNode { self.titleNode.isLayerBacked = true self.switchNode = SwitchNode() + self.switchGestureNode = ASDisplayNode() + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) self.addSubnode(self.switchNode) + self.addSubnode(self.switchGestureNode) } override func didLoad() { super.didLoad() (self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged) + self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { @@ -239,11 +246,14 @@ class ItemListSwitchItemNode: ListViewItemNode { } let switchSize = switchView.bounds.size - strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y:6.0), size: switchSize) + strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: 6.0), size: switchSize) + strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame if switchView.isOn != item.value { switchView.setOn(item.value, animated: animated) } + switchView.isUserInteractionEnabled = item.enableInteractiveChanges } + strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges } }) } @@ -259,7 +269,15 @@ class ItemListSwitchItemNode: ListViewItemNode { @objc func switchValueChanged(_ switchView: UISwitch) { if let item = self.item { - item.updated(switchView.isOn) + let value = switchView.isOn + item.updated(value) + } + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state { + let value = switchView.isOn + item.updated(!value) } } } diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 8b6fa730d2..81cb38dbcf 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -120,6 +120,7 @@ final class LegacyControllerContext: NSObject, LegacyComponentsContext { public func setStatusBarHidden(_ hidden: Bool, with animation: UIStatusBarAnimation) { if let controller = self.controller { controller.statusBar.isHidden = hidden + self.updateDeferScreenEdgeGestures() } } @@ -213,6 +214,17 @@ final class LegacyControllerContext: NSObject, LegacyComponentsContext { func setApplicationStatusBarAlpha(_ alpha: CGFloat) { if let controller = self.controller { controller.statusBar.alpha = alpha + self.updateDeferScreenEdgeGestures() + } + } + + private func updateDeferScreenEdgeGestures() { + if let controller = self.controller { + if controller.statusBar.isHidden || controller.statusBar.alpha.isZero { + controller.deferScreenEdgeGestures = [.top] + } else { + controller.deferScreenEdgeGestures = [] + } } } @@ -229,7 +241,11 @@ final class LegacyControllerContext: NSObject, LegacyComponentsContext { func safeAreaInset() -> UIEdgeInsets { if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout { - return validLayout.safeInsets + var safeInsets = validLayout.safeInsets + if safeInsets.top.isEqual(to: 44.0) { + safeInsets.bottom = 34.0 + } + return safeInsets } return UIEdgeInsets() } @@ -268,8 +284,9 @@ public class LegacyController: ViewController { var controllerLoaded: (() -> Void)? public var presentationCompleted: (() -> Void)? - public init(presentation: LegacyControllerPresentation, theme: PresentationTheme?) { + public init(presentation: LegacyControllerPresentation, theme: PresentationTheme?, initialLayout: ContainerViewLayout? = nil) { self.presentation = presentation + self.validLayout = initialLayout super.init(navigationBarTheme: nil) @@ -376,6 +393,7 @@ public class LegacyController: ViewController { } override open func dismiss(completion: (() -> Void)? = nil) { + self.view.endEditing(true) switch self.presentation { case .modal: self.controllerNode.animateModalOut { [weak self] in diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index 271da23426..6b5a056ef6 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -25,10 +25,10 @@ final class InstantVideoController: LegacyController { private var dismissedVideo = false - override init(presentation: LegacyControllerPresentation, theme: PresentationTheme?) { + override init(presentation: LegacyControllerPresentation, theme: PresentationTheme?, initialLayout: ContainerViewLayout? = nil) { self.audioStatus = InstantVideoControllerRecordingStatus(micLevel: self.micLevelValue.get()) - super.init(presentation: presentation, theme: theme) + super.init(presentation: presentation, theme: theme, initialLayout: initialLayout) } required public init(coder aDecoder: NSCoder) { diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index ae8e611b01..b221b49aed 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -74,7 +74,7 @@ private enum LegacyAssetVideoData { private enum LegacyAssetItem { case image(data: LegacyAssetImageData, caption: String?) case file(data: LegacyAssetImageData, mimeType: String, name: String, caption: String?) - case video(data: LegacyAssetVideoData, previewImage: UIImage?, adjustments: TGVideoEditAdjustments?, caption: String?) + case video(data: LegacyAssetVideoData, previewImage: UIImage?, adjustments: TGVideoEditAdjustments?, caption: String?, asFile: Bool) } private final class LegacyAssetItemWrapper: NSObject { @@ -129,15 +129,20 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab return result } } else if (dict["type"] as! NSString) == "video" { + var asFile = false + if let document = dict["document"] as? NSNumber, document.boolValue { + asFile = true + } + if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } else if let url = dict["url"] as? String { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } } @@ -150,7 +155,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: let disposable = SSignal.combineSignals(signals).start(next: { anyValues in var messages: [EnqueueMessage] = [] - for item in (anyValues as! NSArray) { + outer: for item in (anyValues as! NSArray) { if let item = (item as? NSDictionary)?.object(forKey: "item") as? LegacyAssetItemWrapper { switch item.item { case let .image(data, caption): @@ -201,7 +206,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: default: break } - case let .video(data, previewImage, adjustments, caption): + case let .video(data, previewImage, adjustments, caption, asFile): var finalDimensions: CGSize var finalDuration: Double switch data { @@ -240,14 +245,38 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: } let resource: TelegramMediaResource + var fileName: String = "video.mp4" switch data { case let .asset(asset): - resource = VideoLibraryMediaResource(localIdentifier: asset.backingAsset.localIdentifier, adjustments: resourceAdjustments) + if let assetFileName = asset.fileName, !assetFileName.isEmpty { + fileName = (assetFileName as NSString).lastPathComponent + } + resource = VideoLibraryMediaResource(localIdentifier: asset.backingAsset.localIdentifier, conversion: asFile ? .passthrough : .compress(resourceAdjustments)) case let .tempFile(path, _, _): - resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: path, adjustments: resourceAdjustments) + if asFile { + if let size = fileSize(path) { + resource = LocalFileMediaResource(fileId: arc4random64(), size: size) + account.postbox.mediaBox.moveResourceData(resource.id, fromTempPath: path) + } else { + continue outer + } + } else { + resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: path, adjustments: resourceAdjustments) + } } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [])]) + var fileAttributes: [TelegramMediaFileAttribute] = [] + fileAttributes.append(.FileName(fileName: fileName)) + if !asFile { + fileAttributes.append(.Video(duration: Int(finalDuration), size: finalDimensions, flags: [])) + if let adjustments = adjustments { + if adjustments.sendAsGif { + fileAttributes.append(.Animated) + } + } + } + + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: fileAttributes) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 491cc7a65f..290653ea71 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -670,9 +670,12 @@ final class ListMessageFileItemNode: ListMessageNode { } } - override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil { - return self.iconImageNode + let iconImageNode = self.iconImageNode + return (self.iconImageNode, { [weak iconImageNode] in + return iconImageNode?.view.snapshotContentTree(unhide: true) + }) } return nil } diff --git a/TelegramUI/ListMessageNode.swift b/TelegramUI/ListMessageNode.swift index e56678e1bd..2eece59092 100644 --- a/TelegramUI/ListMessageNode.swift +++ b/TelegramUI/ListMessageNode.swift @@ -26,7 +26,7 @@ class ListMessageNode: ListViewItemNode { } } - func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { return nil } diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 0d2b24a3fe..3b02f2b172 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -221,9 +221,12 @@ final class ListMessageSnippetItemNode: ListMessageNode { if item.message.text != urlString { mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemPrimaryTextColor)) } - + let urlAttributedString = NSMutableAttributedString() urlAttributedString.append(NSAttributedString(string: urlString, font: descriptionFont, textColor: item.theme.list.itemAccentColor)) + if item.theme.list.itemAccentColor.isEqual(item.theme.list.itemPrimaryTextColor) { + urlAttributedString.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length)) + } urlAttributedString.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: urlString, range: NSMakeRange(0, urlAttributedString.length)) mutableDescriptionText.append(urlAttributedString) @@ -388,9 +391,12 @@ final class ListMessageSnippetItemNode: ListMessageNode { } } - override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { + override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil { - return self.iconImageNode + let iconImageNode = self.iconImageNode + return (self.iconImageNode, { [weak iconImageNode] in + return iconImageNode?.view.snapshotContentTree(unhide: true) + }) } return nil } @@ -409,7 +415,13 @@ final class ListMessageSnippetItemNode: ListMessageNode { func activateMedia() { if let item = self.item, let currentPrimaryUrl = self.currentPrimaryUrl { if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { - item.controllerInteraction.openInstantPage(item.message) + if websiteType(of: content) == .instagram { + if !item.controllerInteraction.openMessage(item.message) { + item.controllerInteraction.openInstantPage(item.message) + } + } else { + item.controllerInteraction.openInstantPage(item.message) + } } else { if !item.controllerInteraction.openMessage(item.message) { item.controllerInteraction.openUrl(currentPrimaryUrl) diff --git a/TelegramUI/ManagedAudioRecorder.swift b/TelegramUI/ManagedAudioRecorder.swift index 1f50d0c795..f91e9bdf68 100644 --- a/TelegramUI/ManagedAudioRecorder.swift +++ b/TelegramUI/ManagedAudioRecorder.swift @@ -175,8 +175,9 @@ final class ManagedAudioRecorderContext { private var processSamples = false private var toneTimer: SwiftSignalKit.Timer? + private var idleTimerExtensionDisposable: Disposable? - init(queue: Queue, mediaManager: MediaManager, micLevel: ValuePromise, recordingState: ValuePromise, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) { + init(queue: Queue, mediaManager: MediaManager, pushIdleTimerExtension: @escaping () -> Disposable, micLevel: ValuePromise, recordingState: ValuePromise, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) { assert(queue.isCurrent()) self.id = getNextRecorderContextId() @@ -283,11 +284,17 @@ final class ManagedAudioRecorderContext { addAudioUnitHolder(self.id, queue, self.audioUnit) self.oggWriter.begin(with: self.dataItem) + + self.idleTimerExtensionDisposable = (Signal { subscriber in + return pushIdleTimerExtension() + } |> delay(5.0, queue: queue)).start() } deinit { assert(self.queue.isCurrent()) + self.idleTimerExtensionDisposable?.dispose() + removeAudioRecorderContext(self.id) removeAudioUnitHolder(self.id) @@ -650,14 +657,21 @@ final class ManagedAudioRecorder { return self.recordingStateValue.get() } - init(mediaManager: MediaManager, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) { + init(mediaManager: MediaManager, pushIdleTimerExtension: @escaping () -> Disposable, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) { self.beginWithTone = beginWithTone self.queue.async { - let context = ManagedAudioRecorderContext(queue: self.queue, mediaManager: mediaManager, micLevel: self.micLevelValue, recordingState: self.recordingStateValue, beginWithTone: beginWithTone, beganWithTone: beganWithTone) + let context = ManagedAudioRecorderContext(queue: self.queue, mediaManager: mediaManager, pushIdleTimerExtension: pushIdleTimerExtension, micLevel: self.micLevelValue, recordingState: self.recordingStateValue, beginWithTone: beginWithTone, beganWithTone: beganWithTone) self.contextRef = Unmanaged.passRetained(context) } } + deinit { + let contextRef = self.contextRef + self.queue.async { + contextRef?.release() + } + } + func start() { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { diff --git a/TelegramUI/MediaInputPaneTrendingItem.swift b/TelegramUI/MediaInputPaneTrendingItem.swift new file mode 100644 index 0000000000..326265fae2 --- /dev/null +++ b/TelegramUI/MediaInputPaneTrendingItem.swift @@ -0,0 +1,287 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +class MediaInputPaneTrendingItem: ListViewItem { + let account: Account + let theme: PresentationTheme + let strings: PresentationStrings + let interaction: TrendingPaneInteraction + let info: StickerPackCollectionInfo + let topItems: [StickerPackItem] + let installed: Bool + let unread: Bool + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: TrendingPaneInteraction, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, unread: Bool) { + self.account = account + self.theme = theme + self.strings = strings + self.interaction = interaction + self.info = info + self.topItems = topItems + self.installed = installed + self.unread = unread + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = MediaInputPaneTrendingItemNode() + let (layout, apply) = node.asyncLayout()(self, params) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? MediaInputPaneTrendingItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } +} + +private let titleFont = Font.bold(16.0) +private let statusFont = Font.regular(15.0) +private let buttonFont = Font.medium(13.0) + +private final class TrendingTopItemNode: TransformImageNode { + var file: TelegramMediaFile? = nil + let loadDisposable = MetaDisposable() +} + +class MediaInputPaneTrendingItemNode: ListViewItemNode { + private let titleNode: TextNode + private let descriptionNode: TextNode + private let unreadNode: ASImageNode + private let installTextNode: TextNode + private let installBackgroundNode: ASImageNode + private let installButtonNode: HighlightTrackingButtonNode + private var itemNodes: [TrendingTopItemNode] + + private var item: MediaInputPaneTrendingItem? + private let preloadDisposable = MetaDisposable() + + init() { + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.descriptionNode = TextNode() + self.descriptionNode.isLayerBacked = true + self.descriptionNode.contentMode = .left + self.descriptionNode.contentsScale = UIScreen.main.scale + + self.unreadNode = ASImageNode() + self.unreadNode.isLayerBacked = true + self.unreadNode.displayWithoutProcessing = true + self.unreadNode.displaysAsynchronously = false + + self.installTextNode = TextNode() + self.installTextNode.isLayerBacked = true + self.installTextNode.contentMode = .left + self.installTextNode.contentsScale = UIScreen.main.scale + + self.installBackgroundNode = ASImageNode() + self.installBackgroundNode.isLayerBacked = true + self.installBackgroundNode.displayWithoutProcessing = true + self.installBackgroundNode.displaysAsynchronously = false + + self.installButtonNode = HighlightTrackingButtonNode() + + self.itemNodes = [] + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.descriptionNode) + self.addSubnode(self.unreadNode) + self.addSubnode(self.installBackgroundNode) + self.addSubnode(self.installTextNode) + self.addSubnode(self.installButtonNode) + + self.installButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.installBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.installBackgroundNode.alpha = 0.4 + strongSelf.installTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.installTextNode.alpha = 0.4 + } else { + strongSelf.installBackgroundNode.alpha = 1.0 + strongSelf.installBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.installTextNode.alpha = 1.0 + strongSelf.installTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.installButtonNode.addTarget(self, action: #selector(self.installPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.preloadDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func asyncLayout() -> (_ item: MediaInputPaneTrendingItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) { + let makeInstallLayout = TextNode.asyncLayout(self.installTextNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) + + let currentItem = self.item + + return { item, params in + var updateButtonBackgroundImage: UIImage? + if currentItem?.theme !== item.theme { + updateButtonBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.theme) + } + let unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.theme) + + let leftInset: CGFloat = 14.0 + let rightInset: CGFloat = 16.0 + + let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Install, font: buttonFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.info.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - installLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.StickerPack_StickerCount(item.info.count), font: statusFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize: CGSize = CGSize(width: params.width, height: 116.0) + let insets: UIEdgeInsets = UIEdgeInsets(top: 8.0, left: 0.0, bottom: 0.0, right: 0.0) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + var topItems = item.topItems + if topItems.count > 5 { + topItems.removeSubrange(5 ..< topItems.count) + } + + return (layout, { [weak self] in + if let strongSelf = self { + if item.topItems.count < Int(item.info.count) && item.topItems.count < 5 && strongSelf.item?.info.id != item.info.id { + strongSelf.preloadDisposable.set(preloadedFeaturedStickerSet(network: item.account.network, postbox: item.account.postbox, id: item.info.id).start()) + } + strongSelf.item = item + + let _ = installApply() + let _ = titleApply() + let _ = descriptionApply() + + if let updateButtonBackgroundImage = updateButtonBackgroundImage { + strongSelf.installBackgroundNode.image = updateButtonBackgroundImage + } + + let installWidth: CGFloat = installLayout.size.width + 20.0 + let buttonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - installWidth, y: 4.0), size: CGSize(width: installWidth, height: 26.0)) + strongSelf.installBackgroundNode.frame = buttonFrame + strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) + strongSelf.installButtonNode.frame = buttonFrame + + if item.installed { + strongSelf.installButtonNode.isHidden = true + strongSelf.installBackgroundNode.isHidden = true + strongSelf.installTextNode.isHidden = true + } else { + strongSelf.installButtonNode.isHidden = false + strongSelf.installBackgroundNode.isHidden = false + strongSelf.installTextNode.isHidden = false + } + + let titleFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 2.0), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 23.0), size: descriptionLayout.size) + + if false && item.unread { + strongSelf.unreadNode.isHidden = false + } else { + strongSelf.unreadNode.isHidden = true + } + if let image = unreadImage { + strongSelf.unreadNode.image = image + strongSelf.unreadNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: titleFrame.minY + 7.0), size: image.size) + } + + var offset: CGFloat = params.leftInset + leftInset + let itemSize = CGSize(width: 68.0, height: 68.0) + + for i in 0 ..< topItems.count { + let file = topItems[i].file + let node: TrendingTopItemNode + if i < strongSelf.itemNodes.count { + node = strongSelf.itemNodes[i] + } else { + node = TrendingTopItemNode() + node.contentAnimations = [.subsequentUpdates] + strongSelf.itemNodes.append(node) + strongSelf.addSubnode(node) + } + if file.fileId != node.file?.fileId { + node.file = file + node.setSignal(chatMessageSticker(account: item.account, file: file, small: true)) + node.loadDisposable.set(freeMediaFileInteractiveFetched(account: item.account, file: file).start()) + } + if let dimensions = file.dimensions { + let imageSize = dimensions.aspectFitted(itemSize) + node.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + node.frame = CGRect(origin: CGPoint(x: offset, y: 48.0), size: imageSize) + offset += imageSize.width + 4.0 + } + } + + if topItems.count < strongSelf.itemNodes.count { + for i in (topItems.count ..< strongSelf.itemNodes.count).reversed() { + strongSelf.itemNodes[i].removeFromSupernode() + strongSelf.itemNodes.remove(at: i) + } + } + } + }) + } + } + + 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 installPressed() { + if let item = self.item { + item.interaction.installPack(item.info) + } + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let item = self.item { + item.interaction.openPack(item.info) + } + } + } +} diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index a896c1ddc3..1b0f4dc3d6 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -512,12 +512,14 @@ public final class MediaManager: NSObject { return (activeContext.mediaPlayer, activeContext.addContextSubscriber(priority: priority, activate: activate, deactivate: deactivate)) } - func audioRecorder(beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) -> Signal { + func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { - let audioRecorder = ManagedAudioRecorder(mediaManager: self, beginWithTone: beginWithTone, beganWithTone: beganWithTone) + let audioRecorder = ManagedAudioRecorder(mediaManager: self, pushIdleTimerExtension: { [weak applicationBindings] in + return applicationBindings?.pushIdleTimerExtension() ?? EmptyDisposable + }, beginWithTone: beginWithTone, beganWithTone: beganWithTone) subscriber.putNext(audioRecorder) disposable.set(ActionDisposable { diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index 496da6a496..c210234a5b 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -66,7 +66,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) + }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) listNode.preloadPages = true diff --git a/TelegramUI/MediaPlayerTimeTextNode.swift b/TelegramUI/MediaPlayerTimeTextNode.swift index 2e01aea385..202338c942 100644 --- a/TelegramUI/MediaPlayerTimeTextNode.swift +++ b/TelegramUI/MediaPlayerTimeTextNode.swift @@ -57,9 +57,16 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { private let textColor: UIColor var defaultDuration: Double? + private var updateTimer: SwiftSignalKit.Timer? + private var statusValue: MediaPlayerStatus? { didSet { if self.statusValue != oldValue { + if let statusValue = statusValue, case .playing = statusValue.status { + self.ensureHasTimer() + } else { + self.stopTimer() + } self.updateTimestamp() } } @@ -103,16 +110,38 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { deinit { self.statusDisposable?.dispose() + self.updateTimer?.invalidate() + } + + private func ensureHasTimer() { + if self.updateTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updateTimestamp() + }, queue: Queue.mainQueue()) + self.updateTimer = timer + timer.start() + } + } + + private func stopTimer() { + self.updateTimer?.invalidate() + self.updateTimer = nil } func updateTimestamp() { if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { + let timestampSeconds: Double + if !statusValue.generationTimestamp.isZero { + timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp) + } else { + timestampSeconds = statusValue.timestamp + } switch self.mode { case .normal: - let timestamp = Int32(statusValue.timestamp) + let timestamp = Int32(timestampSeconds) self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) case .reversed: - let timestamp = abs(Int32(statusValue.timestamp - statusValue.duration)) + let timestamp = abs(Int32(timestampSeconds - statusValue.duration)) self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) } } else if let defaultDuration = self.defaultDuration { @@ -138,8 +167,12 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { if let parameters = parameters as? MediaPlayerTimeTextNodeParameters { let text: String - if let minutes = parameters.state.minutes, let seconds = parameters.state.seconds { - text = String(format: "%d:%02d", minutes, seconds) + if let hours = parameters.state.hours, let minutes = parameters.state.minutes, let seconds = parameters.state.seconds { + if hours != 0 { + text = String(format: "%d:%02d:%02d", hours, minutes, seconds) + } else { + text = String(format: "%d:%02d", minutes, seconds) + } } else { text = "-:--" } diff --git a/TelegramUI/MediaResources.swift b/TelegramUI/MediaResources.swift index 54f45b7e3b..3cfb7d02cd 100644 --- a/TelegramUI/MediaResources.swift +++ b/TelegramUI/MediaResources.swift @@ -51,40 +51,90 @@ public struct VideoLibraryMediaResourceId: MediaResourceId { } } +public enum VideoLibraryMediaResourceConversion: PostboxCoding, Equatable { + case passthrough + case compress(VideoMediaResourceAdjustments?) + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("v", orElse: 0) { + case 0: + self = .passthrough + case 1: + self = .compress(decoder.decodeObjectForKey("adj", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments) + default: + self = .compress(nil) + } + } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case .passthrough: + encoder.encodeInt32(0, forKey: "v") + case let .compress(adjustments): + encoder.encodeInt32(1, forKey: "v") + if let adjustments = adjustments { + encoder.encodeObject(adjustments, forKey: "adj") + } else { + encoder.encodeNil(forKey: "adj") + } + } + } + + public static func ==(lhs: VideoLibraryMediaResourceConversion, rhs: VideoLibraryMediaResourceConversion) -> Bool { + switch lhs { + case .passthrough: + if case .passthrough = rhs { + return true + } else { + return false + } + case let .compress(lhsAdjustments): + if case let .compress(rhsAdjustments) = rhs, lhsAdjustments == rhsAdjustments { + return true + } else { + return false + } + } + } +} + public final class VideoLibraryMediaResource: TelegramMediaResource { public let localIdentifier: String - public let adjustments: VideoMediaResourceAdjustments? + public let conversion: VideoLibraryMediaResourceConversion public var headerSize: Int32 { return 32 * 1024 } - public init(localIdentifier: String, adjustments: VideoMediaResourceAdjustments?) { + public init(localIdentifier: String, conversion: VideoLibraryMediaResourceConversion) { self.localIdentifier = localIdentifier - self.adjustments = adjustments + self.conversion = conversion } public required init(decoder: PostboxDecoder) { self.localIdentifier = decoder.decodeStringForKey("i", orElse: "") - self.adjustments = decoder.decodeObjectForKey("a", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments + self.conversion = (decoder.decodeObjectForKey("conv", decoder: { VideoLibraryMediaResourceConversion(decoder: $0) }) as? VideoLibraryMediaResourceConversion) ?? .compress(nil) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.localIdentifier, forKey: "i") - if let adjustments = self.adjustments { - encoder.encodeObject(adjustments, forKey: "a") - } else { - encoder.encodeNil(forKey: "a") - } + encoder.encodeObject(self.conversion, forKey: "conv") } public var id: MediaResourceId { - return VideoLibraryMediaResourceId(localIdentifier: self.localIdentifier, adjustmentsDigest: self.adjustments?.digest) + var adjustmentsDigest: MemoryBuffer? + switch self.conversion { + case .passthrough: + break + case let .compress(adjustments): + adjustmentsDigest = adjustments?.digest + } + return VideoLibraryMediaResourceId(localIdentifier: self.localIdentifier, adjustmentsDigest: adjustmentsDigest) } public func isEqual(to: TelegramMediaResource) -> Bool { if let to = to as? VideoLibraryMediaResource { - return self.localIdentifier == to.localIdentifier && self.adjustments == to.adjustments + return self.localIdentifier == to.localIdentifier && self.conversion == to.conversion } else { return false } diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 4ff3e301a6..4e65aa7d0a 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -60,7 +60,6 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { private let timebase: CMTimebase var fileSelected: ((TelegramMediaFile) -> Void)? - var fileLongPressed: ((TelegramMediaFile) -> Void)? var enableVideoNodes = false init(account: Account) { @@ -122,10 +121,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { self.delegate = self - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - recognizer.tapActionAtPoint = { _ in - return .waitForSingleTap - } + let recognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.addGestureRecognizer(recognizer) } @@ -368,20 +364,9 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { @objc func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if case .ended = recognizer.state { - if let (gesture, point) = recognizer.lastRecognizedGestureAndLocation { - for item in self.displayItems { - if item.frame.contains(point) { - switch gesture { - case .tap: - self.fileSelected?(item.file) - case .longTap: - self.fileLongPressed?(item.file) - default: - break - } - break - } - } + let point = recognizer.location(in: self) + if let file = self.offsetFileAt(point: point) { + self.fileSelected?(file) } } } @@ -394,6 +379,20 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { } return nil } + + func fileAt(point: CGPoint) -> TelegramMediaFile? { + let offsetPoint = point.offsetBy(dx: 0.0, dy: self.bounds.minY) + return self.offsetFileAt(point: offsetPoint) + } + + private func offsetFileAt(point: CGPoint) -> TelegramMediaFile? { + for item in self.displayItems { + if item.frame.contains(point) { + return item.file + } + } + return nil + } } private func NH_LP_TABLE_LOOKUP(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int) -> Int { diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index bb260a5049..5c6276097b 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -27,12 +27,10 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { if self.activityIndicator.layer.superlayer == nil { self.addSubnode(self.activityIndicator) } - //self.activityIndicator.startAnimating() } else { if self.activityIndicator.layer.superlayer != nil { self.activityIndicator.removeFromSupernode() } - //self.activityIndicator.stopAnimating() } } self.setNeedsLayout() @@ -67,7 +65,7 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { self.titleNode.isOpaque = false self.titleNode.isUserInteractionEnabled = false - self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.secondaryTextColor, 22.0), speed: .slow) + self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5), speed: .slow) let activityIndicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) self.activityIndicator.frame = CGRect(origin: CGPoint(), size: activityIndicatorSize) @@ -85,7 +83,7 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { self.buttonView.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { - if highlighted && strongSelf.activityIndicator.isHidden { + if highlighted && (strongSelf.activityIndicator.isHidden || strongSelf.activityIndicator.layer.superlayer == nil) { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.lockView.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 @@ -111,8 +109,6 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { let size = self.bounds.size - self.buttonView.frame = CGRect(origin: CGPoint(), size: size) - var indicatorPadding: CGFloat = 0.0 let indicatorSize = self.activityIndicator.bounds.size @@ -126,6 +122,9 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { let titleFrame = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - titleSize.width - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) self.titleNode.frame = titleFrame + let buttonX = max(0.0, titleFrame.minX - 10.0) + self.buttonView.frame = CGRect(origin: CGPoint(x: buttonX, y: 0.0), size: CGSize(width: min(titleFrame.maxX + 28.0, size.width) - buttonX, height: size.height)) + self.lockView.frame = CGRect(x: titleFrame.maxX + 6.0, y: titleFrame.minY + 4.0, width: 2.0, height: 2.0) if self.activityIndicator.layer.superlayer != nil { diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index d884322442..56010bb8ce 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -21,9 +21,11 @@ private final class NotificationsAndSoundsArguments { let updateInAppVibration: (Bool) -> Void let updateInAppPreviews: (Bool) -> Void + let updateTotalUnreadCountStyle: (Bool) -> Void + let resetNotifications: () -> Void - init(account: Account, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, soundSelectionDisposable: MetaDisposable, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void) { + init(account: Account, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, soundSelectionDisposable: MetaDisposable, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateTotalUnreadCountStyle: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void) { self.account = account self.presentController = presentController self.soundSelectionDisposable = soundSelectionDisposable @@ -36,6 +38,7 @@ private final class NotificationsAndSoundsArguments { self.updateInAppSounds = updateInAppSounds self.updateInAppVibration = updateInAppVibration self.updateInAppPreviews = updateInAppPreviews + self.updateTotalUnreadCountStyle = updateTotalUnreadCountStyle self.resetNotifications = resetNotifications } } @@ -44,6 +47,7 @@ private enum NotificationsAndSoundsSection: Int32 { case messages case groups case inApp + case unreadCountStyle case reset } @@ -65,6 +69,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { case inAppVibrate(PresentationTheme, String, Bool) case inAppPreviews(PresentationTheme, String, Bool) + case unreadCountStyle(PresentationTheme, String, Bool) + case reset(PresentationTheme, String) case resetNotice(PresentationTheme, String) @@ -76,6 +82,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return NotificationsAndSoundsSection.groups.rawValue case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews: return NotificationsAndSoundsSection.inApp.rawValue + case .unreadCountStyle: + return NotificationsAndSoundsSection.unreadCountStyle.rawValue case .reset, .resetNotice: return NotificationsAndSoundsSection.reset.rawValue } @@ -111,10 +119,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return 12 case .inAppPreviews: return 13 - case .reset: + case .unreadCountStyle: return 14 - case .resetNotice: + case .reset: return 15 + case .resetNotice: + return 16 } } @@ -204,6 +214,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } else { return false } + case let .unreadCountStyle(lhsTheme, lhsText, lhsValue): + if case let .unreadCountStyle(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .reset(lhsTheme, lhsText): if case let .reset(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -277,6 +293,10 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateInAppPreviews(updatedValue) }) + case let .unreadCountStyle(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateTotalUnreadCountStyle(updatedValue) + }) case let .reset(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.resetNotifications() @@ -315,6 +335,8 @@ private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSet entries.append(.inAppVibrate(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsVibrate, inAppSettings.vibrate)) entries.append(.inAppPreviews(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsPreview, inAppSettings.displayPreviews)) + entries.append(.unreadCountStyle(presentationData.theme, "Include muted chats", inAppSettings.totalUnreadCountDisplayStyle == .raw)) + entries.append(.reset(presentationData.theme, presentationData.strings.Notifications_ResetAllNotifications)) entries.append(.resetNotice(presentationData.theme, presentationData.strings.Notifications_ResetAllNotificationsHelp)) @@ -374,6 +396,10 @@ public func notificationsAndSoundsController(account: Account) -> ViewController let _ = updateInAppNotificationSettingsInteractively(postbox: account.postbox, { settings in return settings.withUpdatedDisplayPreviews(value) }).start() + }, updateTotalUnreadCountStyle: { value in + let _ = updateInAppNotificationSettingsInteractively(postbox: account.postbox, { settings in + return settings.withUpdatedTotalUnreadCountDisplayStyle(value ? .raw : .filtered) + }).start() }, resetNotifications: { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm index c0f7209025..062061a74e 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.mm +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -95,16 +95,16 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat _controller = new tgvoip::VoIPController(); _controller->implData = (__bridge void *)self; - _controller->SetStateCallback(&controllerStateCallback); + /*releasable*/ + //_controller->SetStateCallback(&controllerStateCallback); - /*master*/ - /*auto callbacks = tgvoip::VoIPController::Callbacks(); + auto callbacks = tgvoip::VoIPController::Callbacks(); callbacks.connectionStateChanged = &controllerStateCallback; callbacks.groupCallKeyReceived = NULL; callbacks.groupCallKeySent = NULL; callbacks.signalBarCountChanged = NULL; callbacks.upgradeToGroupCallRequested = NULL; - _controller->SetCallbacks(callbacks);*/ + _controller->SetCallbacks(callbacks); tgvoip::VoIPController::crypto.sha1 = &TGCallSha1; tgvoip::VoIPController::crypto.sha256 = &TGCallSha256; @@ -136,9 +136,9 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat tgvoip::IPv6Address addressv6(std::string(connection.ipv6.UTF8String)); unsigned char peerTag[16]; [connection.peerTag getBytes:peerTag length:16]; - /*master*/ - //endpoints.push_back(tgvoip::Endpoint(connection.connectionId, (uint16_t)connection.port, address, addressv6, tgvoip::Endpoint::TYPE_UDP_RELAY, peerTag)); - endpoints.push_back(tgvoip::Endpoint(connection.connectionId, (uint16_t)connection.port, address, addressv6, EP_TYPE_UDP_RELAY, peerTag)); + endpoints.push_back(tgvoip::Endpoint(connection.connectionId, (uint16_t)connection.port, address, addressv6, tgvoip::Endpoint::TYPE_UDP_RELAY, peerTag)); + /*releasable*/ + //endpoints.push_back(tgvoip::Endpoint(connection.connectionId, (uint16_t)connection.port, address, addressv6, EP_TYPE_UDP_RELAY, peerTag)); } voip_config_t config; @@ -154,8 +154,8 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat _controller->SetConfig(&config); _controller->SetEncryptionKey((char *)key.bytes, isOutgoing); - /*master*/ - _controller->SetRemoteEndpoints(endpoints, _allowP2P/*, 65*/); + /*releasable*/ + _controller->SetRemoteEndpoints(endpoints, _allowP2P, 65); _controller->Start(); _controller->Connect(); @@ -164,8 +164,8 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat - (void)stop { if (_controller) { char *buffer = (char *)malloc(_controller->GetDebugLogLength()); - /*master*/ - //_controller->Stop(); + /*releasable*/ + _controller->Stop(); _controller->GetDebugLog(buffer); NSString *debugLog = [[NSString alloc] initWithUTF8String:buffer]; @@ -188,8 +188,8 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat - (void)controllerStateChanged:(int)state { OngoingCallState callState = OngoingCallStateInitializing; - /*master*/ - /*switch (state) { + /*releasable*/ + switch (state) { case tgvoip::STATE_ESTABLISHED: callState = OngoingCallStateConnected; break; @@ -198,8 +198,8 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat break; default: break; - }*/ - switch (state) { + } + /*switch (state) { case STATE_ESTABLISHED: callState = OngoingCallStateConnected; break; @@ -208,7 +208,7 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat break; default: break; - } + }*/ if (callState != _state) { _state = callState; diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index 6a3fefd1d4..b67ae9e883 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -6,7 +6,18 @@ import TelegramCore import SwiftSignalKit import PassKit -func openChatMessage(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> ASDisplayNode?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, sendSticker: ((TelegramMediaFile) -> Void)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void) -> Bool { +private enum ChatMessageGalleryControllerData { + case url(String) + case pass(TelegramMediaFile) + case instantPage(InstantPageGalleryController, Int, Media) + case map(TelegramMediaMap) + case stickerPack(StickerPackReference) + case audio(TelegramMediaFile) + case gallery(GalleryController) + case other(Media) +} + +private func chatMessageGalleryControllerData(account: Account, message: Message, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, synchronousLoad: Bool) -> ChatMessageGalleryControllerData? { var galleryMedia: Media? var otherMedia: Media? var instantPageMedia: [InstantPageGalleryEntry]? @@ -17,8 +28,7 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever galleryMedia = image } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if content.embedUrl != nil && !webEmbedVideoContentSupportsWebpage(content) { - openUrl(content.url) - return true + return .url(content.url) } else { if let file = content.file { galleryMedia = file @@ -58,138 +68,189 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever navigationController.replaceTopController(controller, animated: false, ready: ready) } }) - setupTemporaryHiddenMedia(gallery.hiddenMedia, centralIndex, galleryMedia) - - dismissInput() - present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry in - var selectedTransitionNode: ASDisplayNode? - if entry.index == centralIndex { - selectedTransitionNode = transitionNode(message.id, galleryMedia) - } - if let selectedTransitionNode = selectedTransitionNode { - return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: addToTransitionSurface) - } - return nil - })) - return true + return .instantPage(gallery, centralIndex, galleryMedia) } else if let galleryMedia = galleryMedia { if let mapMedia = galleryMedia as? TelegramMediaMap { - dismissInput() - present(legacyLocationController(message: message, mapMedia: mapMedia, account: account, openPeer: { peer in - openPeer(peer, .info) - }, sendLiveLocation: { coordinate, period in - let outMessage: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period), replyToMessageId: nil, localGroupingKey: nil) - enqueueMessage(outMessage) - }, stopLiveLocation: { - account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: message.id.peerId) - }), nil) + return .map(mapMedia) } else if let file = galleryMedia as? TelegramMediaFile, file.isSticker { for attribute in file.attributes { if case let .Sticker(_, reference, _) = attribute { if let reference = reference { - let controller = StickerPackPreviewController(account: account, stickerPack: reference) - controller.sendSticker = sendSticker - dismissInput() - present(controller, nil) + return .stickerPack(reference) } break } } } else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice || file.isInstantVideo { - let location: PeerMessagesPlaylistLocation - let playerType: MediaManagerPlayerType - if (file.isVoice || file.isInstantVideo) && message.tags.contains(.voiceOrInstantVideo) { - location = .messages(peerId: message.id.peerId, tagMask: .voiceOrInstantVideo, at: message.id) - playerType = .voice - } else if file.isMusic && message.tags.contains(.music) { - location = .messages(peerId: message.id.peerId, tagMask: .music, at: message.id) - playerType = .music - } else { - location = .singleMessage(message.id) - playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music - } - account.telegramApplicationContext.mediaManager.setPlaylist(PeerMessagesMediaPlaylist(postbox: account.postbox, network: account.network, location: location), type: playerType) + return .audio(file) } else if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "application/vnd.apple.pkpass" || (file.fileName != nil && file.fileName!.lowercased().hasSuffix(".pkpass")) { - let _ = (account.postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)) - |> take(1) - |> deliverOnMainQueue).start(next: { data in - guard let navigationController = navigationController else { - return - } - if data.complete, let content = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { - var error: NSError? - let pass = PKPass(data: content, error: &error) - if error == nil { - let controller = PKAddPassesViewController(pass: pass) - if let window = navigationController.view.window { - window.rootViewController?.present(controller, animated: true) - } - } - } - }) + return .pass(file) } else { - let gallery = GalleryController(account: account, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, replaceRootController: { [weak navigationController] controller, ready in + let gallery = GalleryController(account: account, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in navigationController?.replaceTopController(controller, animated: false, ready: ready) }, baseNavigationController: navigationController) - - dismissInput() - present(gallery, GalleryControllerPresentationArguments(transitionArguments: { messageId, media in - let selectedTransitionNode = transitionNode(messageId, media) - if let selectedTransitionNode = selectedTransitionNode { - return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: addToTransitionSurface) - } - return nil - })) + return .gallery(gallery) } - return true - } else if let contact = otherMedia as? TelegramMediaContact { - let _ = (account.postbox.modify { modifier -> (Peer?, Bool?) in - if let peerId = contact.peerId { - return (modifier.getPeer(peerId), modifier.isPeerContact(peerId: peerId)) - } else { - return (nil, nil) - } - } |> deliverOnMainQueue).start(next: { peer, isContact in - guard let peer = peer else { - return - } + } + if let otherMedia = otherMedia { + return .other(otherMedia) + } else { + return nil + } +} + +enum ChatMessagePreviewControllerData { + case instantPage(InstantPageGalleryController, Int, Media) + case gallery(GalleryController) +} + +func chatMessagePreviewControllerData(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?) -> ChatMessagePreviewControllerData? { + if let mediaData = chatMessageGalleryControllerData(account: account, message: message, navigationController: navigationController, standalone: standalone, reverseMessageGalleryOrder: reverseMessageGalleryOrder, synchronousLoad: true) { + switch mediaData { + case let .gallery(gallery): + return .gallery(gallery) + case let .instantPage(gallery, centralIndex, galleryMedia): + return .instantPage(gallery, centralIndex, galleryMedia) + default: + break + } + } + return nil +} + +func openChatMessage(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, () -> UIView?)?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, sendSticker: ((TelegramMediaFile) -> Void)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void) -> Bool { + if let mediaData = chatMessageGalleryControllerData(account: account, message: message, navigationController: navigationController, standalone: standalone, reverseMessageGalleryOrder: reverseMessageGalleryOrder, synchronousLoad: false) { + switch mediaData { + case let .url(url): + openUrl(url) + return true + case let .pass(file): + let _ = (account.postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)) + |> take(1) + |> deliverOnMainQueue).start(next: { data in + guard let navigationController = navigationController else { + return + } + if data.complete, let content = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + var error: NSError? + let pass = PKPass(data: content, error: &error) + if error == nil { + let controller = PKAddPassesViewController(pass: pass) + if let window = navigationController.view.window { + window.rootViewController?.present(controller, animated: true) + } + } + } + }) + return true + case let .instantPage(gallery, centralIndex, galleryMedia): + setupTemporaryHiddenMedia(gallery.hiddenMedia, centralIndex, galleryMedia) - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - var items: [ActionSheetItem] = [] - - if let peerId = contact.peerId { - items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_SendMessage, action: { - dismissAction() - - openPeer(peer, .chat(textInputState: nil, messageId: nil)) - })) - if let isContact = isContact, !isContact { - items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_AddContact, action: { - dismissAction() - let _ = addContactPeerInteractively(account: account, peerId: peerId, phone: contact.phoneNumber).start() - })) - } - items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: { - dismissAction() - callPeer(peerId) - })) - } - items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: { - dismissAction() - account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(contact.phoneNumber).replacingOccurrences(of: " ", with: ""))") - })) - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) dismissInput() - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }) - return true + present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry in + var selectedTransitionNode: (ASDisplayNode, () -> UIView?)? + if entry.index == centralIndex { + selectedTransitionNode = transitionNode(message.id, galleryMedia) + } + if let selectedTransitionNode = selectedTransitionNode { + return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: addToTransitionSurface) + } + return nil + })) + return true + case let .map(mapMedia): + dismissInput() + present(legacyLocationController(message: message, mapMedia: mapMedia, account: account, openPeer: { peer in + openPeer(peer, .info) + }, sendLiveLocation: { coordinate, period in + let outMessage: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period), replyToMessageId: nil, localGroupingKey: nil) + enqueueMessage(outMessage) + }, stopLiveLocation: { + account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: message.id.peerId) + }), nil) + return true + case let .stickerPack(reference): + let controller = StickerPackPreviewController(account: account, stickerPack: reference) + controller.sendSticker = sendSticker + dismissInput() + present(controller, nil) + return true + case let .audio(file): + let location: PeerMessagesPlaylistLocation + let playerType: MediaManagerPlayerType + if (file.isVoice || file.isInstantVideo) && message.tags.contains(.voiceOrInstantVideo) { + location = .messages(peerId: message.id.peerId, tagMask: .voiceOrInstantVideo, at: message.id) + playerType = .voice + } else if file.isMusic && message.tags.contains(.music) { + location = .messages(peerId: message.id.peerId, tagMask: .music, at: message.id) + playerType = .music + } else { + location = .singleMessage(message.id) + playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music + } + account.telegramApplicationContext.mediaManager.setPlaylist(PeerMessagesMediaPlaylist(postbox: account.postbox, network: account.network, location: location), type: playerType) + return true + case let .gallery(gallery): + dismissInput() + present(gallery, GalleryControllerPresentationArguments(transitionArguments: { messageId, media in + let selectedTransitionNode = transitionNode(messageId, media) + if let selectedTransitionNode = selectedTransitionNode { + return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: addToTransitionSurface) + } + return nil + })) + return true + case let .other(otherMedia): + if let contact = otherMedia as? TelegramMediaContact { + let _ = (account.postbox.modify { modifier -> (Peer?, Bool?) in + if let peerId = contact.peerId { + return (modifier.getPeer(peerId), modifier.isPeerContact(peerId: peerId)) + } else { + return (nil, nil) + } + } |> deliverOnMainQueue).start(next: { peer, isContact in + guard let peer = peer else { + return + } + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var items: [ActionSheetItem] = [] + + if let peerId = contact.peerId { + items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_SendMessage, action: { + dismissAction() + + openPeer(peer, .chat(textInputState: nil, messageId: nil)) + })) + if let isContact = isContact, !isContact { + items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_AddContact, action: { + dismissAction() + let _ = addContactPeerInteractively(account: account, peerId: peerId, phone: contact.phoneNumber).start() + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: { + dismissAction() + callPeer(peerId) + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: { + dismissAction() + account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(contact.phoneNumber).replacingOccurrences(of: " ", with: ""))") + })) + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + dismissInput() + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) + return true + } + } } return false } diff --git a/TelegramUI/OpenResolvedUrl.swift b/TelegramUI/OpenResolvedUrl.swift index dacdb57485..23e279180a 100644 --- a/TelegramUI/OpenResolvedUrl.swift +++ b/TelegramUI/OpenResolvedUrl.swift @@ -6,7 +6,7 @@ import Display func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, present: (ViewController, Any?) -> Void) { switch resolvedUrl { case let .externalUrl(url): - openExternalUrl(url: url, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: account.telegramApplicationContext, navigationController: navigationController) + openExternalUrl(account: account, url: url, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: account.telegramApplicationContext, navigationController: navigationController) case let .peer(peerId): openPeer(peerId, .chat(textInputState: nil, messageId: nil)) case let .botStart(peerId, payload): @@ -23,5 +23,16 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationCon present(JoinLinkPreviewController(account: account, link: link, navigateToPeer: { peerId in openPeer(peerId, .chat(textInputState: nil, messageId: nil)) }), nil) + case let .proxy(host, port, username, password): + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let alertText: String + if let username = username, let password = password { + alertText = presentationData.strings.Settings_ApplyProxyAlertCredentials(host, "\(port)", username, password).0 + } else { + alertText = presentationData.strings.Settings_ApplyProxyAlert(host, "\(port)").0 + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + let _ = applyProxySettings(postbox: account.postbox, network: account.network, settings: ProxySettings(host: host, port: port, username: username, password: password, useForCalls: false)).start() + })]), nil) } } diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index e132304ded..9c325f2696 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -1,8 +1,10 @@ import Foundation import Display import SafariServices +import TelegramCore +import SwiftSignalKit -func openExternalUrl(url: String, presentationData: PresentationData, applicationContext: TelegramApplicationContext, navigationController: NavigationController?) { +public func openExternalUrl(account: Account, url: String, presentationData: PresentationData, applicationContext: TelegramApplicationContext, navigationController: NavigationController?) { if url.lowercased().hasPrefix("tel:") { applicationContext.applicationBindings.openUrl(url) return @@ -11,6 +13,8 @@ func openExternalUrl(url: String, presentationData: PresentationData, applicatio var parsedUrlValue: URL? if let parsed = URL(string: url) { parsedUrlValue = parsed + } else if let encoded = (url as NSString).addingPercentEscapes(using: String.Encoding.utf8.rawValue), let parsed = URL(string: encoded) { + parsedUrlValue = parsed } if let parsed = parsedUrlValue, parsed.scheme == nil { parsedUrlValue = URL(string: "https://" + parsed.absoluteString) @@ -45,6 +49,201 @@ func openExternalUrl(url: String, presentationData: PresentationData, applicatio } } + if parsedUrl.scheme == "tg", let query = parsedUrl.query { + var convertedUrl: String? + if parsedUrl.host == "resolve" { + if let components = URLComponents(string: "/?" + query) { + var domain: String? + var start: String? + var startGroup: String? + var game: String? + var post: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "domain" { + domain = value + } else if queryItem.name == "start" { + start = value + } else if queryItem.name == "startgroup" { + startGroup = value + } else if queryItem.name == "game" { + game = value + } else if queryItem.name == "post" { + post = value + } + } + } + } + + if let domain = domain { + var result = "https://t.me/\(domain)" + if let post = post, let postValue = Int(post) { + result += "/\(postValue)" + } + if let start = start { + result += "?start=\(start)" + } else if let startGroup = startGroup { + result += "?startgroup=\(startGroup)" + } else if let game = game { + result += "?game=\(game)" + } + convertedUrl = result + } + } + } else if parsedUrl.host == "join" { + if let components = URLComponents(string: "/?" + query) { + var invite: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "invite" { + invite = value + } + } + } + } + if let invite = invite { + convertedUrl = "https://t.me/joinchat/\(invite)" + } + } + } else if parsedUrl.host == "addstickers" { + if let components = URLComponents(string: "/?" + query) { + var set: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "set" { + set = value + } + } + } + } + if let set = set { + convertedUrl = "https://t.me/addstickers/\(set)" + } + } + } else if parsedUrl.host == "msg_url" { + if let components = URLComponents(string: "/?" + query) { + var shareUrl: String? + var shareText: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "url" { + shareUrl = value + } else if queryItem.name == "text" { + shareText = value + } + } + } + } + if let shareUrl = shareUrl { + let controller = PeerSelectionController(account: account) + controller.peerSelected = { [weak controller] peerId in + if let strongController = controller { + strongController.dismiss() + + let textInputState: ChatTextInputState + if let shareText = shareText, !shareText.isEmpty { + let urlString = NSMutableAttributedString(string: "\(shareUrl)\n") + let textString = NSAttributedString(string: "\(shareText)") + let selectionRange: Range = urlString.length ..< (urlString.length + textString.length) + urlString.append(textString) + textInputState = ChatTextInputState(inputText: urlString, selectionRange: selectionRange) + } else { + textInputState = ChatTextInputState(inputText: NSAttributedString(string: "\(shareUrl)")) + } + + let _ = (account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedComposeInputState(textInputState) + } else { + return ChatInterfaceState().withUpdatedComposeInputState(textInputState) + } + }) + }) + |> deliverOnMainQueue).start(completed: { + navigationController?.pushViewController(ChatController(account: account, chatLocation: .peer(peerId), messageId: nil)) + }) + } + } + if let navigationController = navigationController { + (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) + } + } + } + } else if parsedUrl.host == "socks" { + if let components = URLComponents(string: "/?" + query) { + var server: String? + var port: String? + var user: String? + var pass: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "server" || queryItem.name == "proxy" { + server = value + } else if queryItem.name == "port" { + port = value + } else if queryItem.name == "user" { + user = value + } else if queryItem.name == "pass" { + pass = value + } + } + } + } + + if let server = server, !server.isEmpty, let port = port, let _ = Int32(port) { + var result = "https://t.me/proxy?proxy=\(server)&port=\(port)" + if let user = user { + result += "&user=\(user)" + if let pass = pass { + result += "&pass=\(pass)" + } + } + convertedUrl = result + } + } + } + + if let convertedUrl = convertedUrl { + let _ = (resolveUrl(account: account, url: convertedUrl) + |> deliverOnMainQueue).start(next: { resolved in + if case let .externalUrl(value) = resolved { + applicationContext.applicationBindings.openUrl(value) + } else { + openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in + switch navigation { + case .info: + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + if let infoController = peerInfoController(account: account, peer: peer) { + navigationController?.pushViewController(infoController) + } + }) + case .chat: + if let navigationController = navigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + case .withBotStartPayload: + if let navigationController = navigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + } + }, present: { c, a in + if let navigationController = navigationController { + (navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a) + } + }) + } + }) + } + return + } + if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { if #available(iOSApplicationExtension 9.0, *) { if let window = navigationController?.view.window { diff --git a/TelegramUI/OverlayMediaController.swift b/TelegramUI/OverlayMediaController.swift index a0612d4e2b..91966309bd 100644 --- a/TelegramUI/OverlayMediaController.swift +++ b/TelegramUI/OverlayMediaController.swift @@ -35,7 +35,7 @@ public final class OverlayMediaController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - let updatedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: (layout.statusBarHeight ?? 0.0) + 44.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging) + let updatedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: (layout.statusBarHeight ?? 0.0) + 44.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging) self.controllerNode.containerLayoutUpdated(updatedLayout, transition: transition) } } diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 526b9e3f45..39a6fbd2db 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -55,7 +55,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec return false } }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, requestMessageUpdate: { _ in diff --git a/TelegramUI/OverlayPlayerControlsNode.swift b/TelegramUI/OverlayPlayerControlsNode.swift index d69a5fadd2..4c82e4022f 100644 --- a/TelegramUI/OverlayPlayerControlsNode.swift +++ b/TelegramUI/OverlayPlayerControlsNode.swift @@ -237,7 +237,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode { switch value.looping { case .none: - strongSelf.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: strongSelf.theme.list.itemPrimaryTextColor) + let baseColor: UIColor + if strongSelf.theme.list.itemPrimaryTextColor.isEqual(strongSelf.theme.list.itemAccentColor) { + baseColor = strongSelf.theme.list.controlSecondaryColor + } else { + baseColor = strongSelf.theme.list.itemPrimaryTextColor + } + strongSelf.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: baseColor) case .item: strongSelf.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/RepeatOne"), color: strongSelf.theme.list.itemPrimaryTextColor) case .all: diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index 0ce3a8c266..6ef7aadf0b 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -12,13 +12,15 @@ private final class PasscodeOptionsControllerArguments { let changePasscode: () -> Void let changePasscodeTimeout: () -> Void let changeTouchId: (Bool) -> Void + let toggleSimplePasscode: (Bool) -> Void - init(turnPasscodeOn: @escaping () -> Void, turnPasscodeOff: @escaping () -> Void, changePasscode: @escaping () -> Void, changePasscodeTimeout: @escaping () -> Void, changeTouchId: @escaping (Bool) -> Void) { + init(turnPasscodeOn: @escaping () -> Void, turnPasscodeOff: @escaping () -> Void, changePasscode: @escaping () -> Void, changePasscodeTimeout: @escaping () -> Void, changeTouchId: @escaping (Bool) -> Void, toggleSimplePasscode: @escaping (Bool) -> Void) { self.turnPasscodeOn = turnPasscodeOn self.turnPasscodeOff = turnPasscodeOff self.changePasscode = changePasscode self.changePasscodeTimeout = changePasscodeTimeout self.changeTouchId = changeTouchId + self.toggleSimplePasscode = toggleSimplePasscode } } @@ -34,12 +36,13 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { case autoLock(PresentationTheme, String, String) case touchId(PresentationTheme, String, Bool) + case simplePasscode(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { case .togglePasscode, .changePasscode, .settingInfo: return PasscodeOptionsSection.setting.rawValue - case .autoLock, .touchId: + case .autoLock, .touchId, .simplePasscode: return PasscodeOptionsSection.options.rawValue } } @@ -56,6 +59,8 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { return 3 case .touchId: return 4 + case .simplePasscode: + return 4 } } @@ -91,6 +96,12 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { } else { return false } + case let .simplePasscode(lhsTheme, lhsText, lhsValue): + if case let .simplePasscode(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -122,6 +133,10 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.changeTouchId(value) }) + case let .simplePasscode(theme, title, value): + return ItemListSwitchItem(theme: theme, title: title, value: value, enableInteractiveChanges: false, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleSimplePasscode(value) + }) } } } @@ -194,6 +209,11 @@ private func passcodeOptionsControllerEntries(presentationData: PresentationData entries.append(.touchId(presentationData.theme, presentationData.strings.PasscodeSettings_UnlockWithFaceId, passcodeOptionsData.presentationSettings.enableBiometrics)) } } + var simplePasscode = false + if case .numericalPassword = passcodeOptionsData.accessChallenge { + simplePasscode = true + } + entries.append(.simplePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_SimplePasscode, simplePasscode)) } return entries @@ -349,6 +369,38 @@ func passcodeOptionsController(account: Account) -> ViewController { return current.withUpdatedEnableBiometrics(value) }).start() }) + }, toggleSimplePasscode: { value in + var dismissImpl: (() -> Void)? + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme) + let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: value ? TGPasscodeEntryControllerModeSetupSimple : TGPasscodeEntryControllerModeSetupComplex, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in + if let result = result { + let challenge = value ? PostboxAccessChallengeData.numericalPassword(value: result, timeout: nil, attempts: nil) : PostboxAccessChallengeData.plaintextPassword(value: result, timeout: nil, attempts: nil) + let _ = account.postbox.modify({ modifier -> Void in + modifier.setAccessChallengeData(challenge) + updatePresentationPasscodeSettingsInternal(modifier: modifier, { current in + return current.withUpdatedAutolockTimeout(1 * 60 * 60) + }) + }).start() + + let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in + passcodeOptionsDataPromise?.set(.single(data.withUpdatedAccessChallenge(challenge).withUpdatedPresentationSettings(data.presentationSettings.withUpdatedAutolockTimeout(1 * 60 * 60)))) + }) + + dismissImpl?() + } else { + dismissImpl?() + } + })! + legacyController.bind(controller: controller) + legacyController.supportedOrientations = .portrait + legacyController.statusBar.statusBarStyle = .White + presentControllerImpl?(legacyController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + dismissImpl = { [weak legacyController] in + legacyController?.dismiss() + } }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), passcodeOptionsDataPromise.get()) |> deliverOnMainQueue @@ -390,7 +442,14 @@ public func passcodeOptionsAccessController(account: Account, animateIn: Bool = let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme) - let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeVerifySimple, cancelEnabled: true, allowTouchId: false, attemptData: attemptData, completion: { value in + let mode: TGPasscodeEntryControllerMode + switch challenge { + case .none, .numericalPassword: + mode = TGPasscodeEntryControllerModeVerifySimple + case .plaintextPassword: + mode = TGPasscodeEntryControllerModeVerifyComplex + } + let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: mode, cancelEnabled: true, allowTouchId: false, attemptData: attemptData, completion: { value in if value != nil { completion(false) } diff --git a/TelegramUI/PeerAvatarImageGalleryItem.swift b/TelegramUI/PeerAvatarImageGalleryItem.swift index 9f38622d81..84f60588a5 100644 --- a/TelegramUI/PeerAvatarImageGalleryItem.swift +++ b/TelegramUI/PeerAvatarImageGalleryItem.swift @@ -158,13 +158,13 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) - let copyView = node.view.snapshotContentTree()! + let copyView = node.1()! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame @@ -198,17 +198,17 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) var positionCompleted = false var boundsCompleted = false var copyCompleted = false - let copyView = node.view.snapshotContentTree()! + let copyView = node.1()! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index b111aba558..f1f6608fa2 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -8,7 +8,7 @@ import TelegramCore import SafariServices public class PeerMediaCollectionController: TelegramController { - private var containerLayout = ContainerViewLayout() + private var validLayout: ContainerViewLayout? private let account: Account private let peerId: PeerId @@ -172,7 +172,7 @@ public class PeerMediaCollectionController: TelegramController { }, updateInputState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in - }, callPeer: { _ in + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] content in if let strongSelf = self { switch content { @@ -273,6 +273,7 @@ public class PeerMediaCollectionController: TelegramController { })) } } + }, deleteMessages: { _ in }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { if let forwardMessageIdsSet = strongSelf.interfaceState.selectionState?.selectedIds { @@ -309,6 +310,7 @@ public class PeerMediaCollectionController: TelegramController { strongSelf.present(controller, in: .window(.root)) } } + }, forwardMessages: { _ in }, shareSelectedMessages: { [weak self] in if let strongSelf = self, let selectedIds = strongSelf.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in @@ -366,6 +368,7 @@ public class PeerMediaCollectionController: TelegramController { }, beginCall: { }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in + }, presentGlobalOverlayController: { _, _ in }, navigateFeed: { }, openGrouping: { }, toggleSilentPost: { @@ -459,7 +462,7 @@ public class PeerMediaCollectionController: TelegramController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.containerLayout = layout + self.validLayout = layout self.mediaCollectionDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets in self.mediaCollectionDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index f4f3ab5146..eacd95e646 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -479,14 +479,14 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { return nil } - func transitionNodeForGallery(messageId: MessageId, media: Media) -> ASDisplayNode? { + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? { if let searchContentNode = self.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { if let transitionNode = searchContentNode.transitionNodeForGallery(messageId: messageId, media: media) { return transitionNode } } - var transitionNode: ASDisplayNode? + var transitionNode: (ASDisplayNode, () -> UIView?)? self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { diff --git a/TelegramUI/PeerMediaCollectionEmptyNode.swift b/TelegramUI/PeerMediaCollectionEmptyNode.swift index 9515df28df..ba091bdb20 100644 --- a/TelegramUI/PeerMediaCollectionEmptyNode.swift +++ b/TelegramUI/PeerMediaCollectionEmptyNode.swift @@ -43,7 +43,7 @@ final class PeerMediaCollectionEmptyNode: ASDisplayNode { self.textNode.isLayerBacked = true self.textNode.displaysAsynchronously = false - self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemSecondaryTextColor, 22.0), speed: .regular) + self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemSecondaryTextColor, 22.0, 2.0), speed: .regular) let icon: UIImage? let text: NSAttributedString diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index fe1b3c05e1..535b84f8d1 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -157,7 +157,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { let contactsInsets = insets - contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: contactsInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: contactsInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) } if let searchDisplayController = self.searchDisplayController { diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index a8f934a64b..fed9a66174 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -36,6 +36,7 @@ enum PresentationResourceKey: Int32 { case itemListSecondaryCheckIcon case itemListPlusIcon case itemListDeleteIndicatorIcon + case itemListReorderIndicatorIcon case itemListAddPersonIcon case itemListStickerItemUnreadDot @@ -113,6 +114,7 @@ enum PresentationResourceKey: Int32 { case chatInputMediaPanelRecentGifsIconImage case chatInputMediaPanelTrendingIconImage case chatInputMediaPanelSettingsIconImage + case chatInputMediaPanelAddPackButtonImage case chatInputButtonPanelButtonImage case chatInputButtonPanelButtonHighlightedImage @@ -126,6 +128,7 @@ enum PresentationResourceKey: Int32 { case chatInputPanelVoiceActiveButtonImage case chatInputPanelVideoActiveButtonImage case chatInputPanelAttachmentButtonImage + case chatInputPanelExpandButtonImage case chatInputPanelMediaRecordingDotImage case chatInputPanelMediaRecordingCancelArrowImage case chatInputTextFieldStickersImage diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index bf501f943f..77b43bd017 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -353,6 +353,12 @@ struct PresentationResourcesChat { }) } + static func chatInputMediaPanelAddPackButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelAddPackButtonImage.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 8.0, color: nil, strokeColor: theme.chat.inputPanel.panelControlAccentColor, strokeWidth: 1.0, backgroundColor: nil) + }) + } + static func chatInputButtonPanelButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputButtonPanelButtonImage.rawValue, { theme in return generateInputPanelButtonBackgroundImage(fillColor: theme.chat.inputButtonPanel.buttonFillColor, strokeColor: theme.chat.inputButtonPanel.buttonStrokeColor) @@ -480,6 +486,12 @@ struct PresentationResourcesChat { }) } + static func chatInputPanelExpandButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelExpandButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconExpandInput"), color: theme.chat.inputPanel.panelControlColor) + }) + } + static func chatInputPanelMediaRecordingDotImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelMediaRecordingDotImage.rawValue, { theme in return generateFilledCircleImage(diameter: 9.0, color: theme.chat.inputPanel.mediaRecordingDotColor) diff --git a/TelegramUI/PresentationResourcesItemList.swift b/TelegramUI/PresentationResourcesItemList.swift index e76f075d81..8bc8924ffc 100644 --- a/TelegramUI/PresentationResourcesItemList.swift +++ b/TelegramUI/PresentationResourcesItemList.swift @@ -84,6 +84,19 @@ struct PresentationResourcesItemList { }) } + static func itemListReorderIndicatorIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListReorderIndicatorIcon.rawValue, { theme in + generateImage(CGSize(width: 22.0, height: 9.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.list.controlSecondaryColor.cgColor) + + context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 1.5))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: 3.5), size: CGSize(width: size.width, height: 1.5))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: 7), size: CGSize(width: size.width, height: 1.5))) + }) + }) + } + static func addPersonIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListAddPersonIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Contact List/AddMemberIcon"), color: theme.list.itemAccentColor) diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index 1af9d7464a..165c149234 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -5,11 +5,10 @@ private func generateShareButtonImage(theme: PresentationTheme) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.rootController.navigationBar.accentTextColor) } -func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0) -> UIImage? { +func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0, lineWidth: CGFloat = 2.0) -> UIImage? { return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setStrokeColor(color.cgColor) - let lineWidth: CGFloat = 2.0 context.setLineWidth(lineWidth) context.setLineCap(.round) let cutoutAngle: CGFloat = CGFloat.pi * 30.0 / 180.0 diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index 8717846c55..55836758e4 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -1520,6 +1520,11 @@ public final class PresentationStrings { public let Notifications_MessageNotifications: String public let ChannelMembers_WhoCanAddMembersAdminsHelp: String public let DialogList_DeleteBotConversationConfirmation: String + private let _DialogList_MultipleTyping: String + private let _DialogList_MultipleTyping_r: [(Int, NSRange)] + public func DialogList_MultipleTyping(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_MultipleTyping, self._DialogList_MultipleTyping_r, [_0, _1]) + } public let Conversation_ClousStorageInfo_Description2: String private let _Time_MonthOfYear_m5: String private let _Time_MonthOfYear_m5_r: [(Int, NSRange)] @@ -2456,6 +2461,7 @@ public final class PresentationStrings { public let Channel_AdminLog_EmptyFilterText: String public let Channel_AdminLog_EmptyText: String public let PrivacySettings_DeleteAccountTitle: String + public let Peer_DeletedUser: String public let PrivacyLastSeenSettings_CustomShareSettings_Delete: String private let _ENCRYPTED_MESSAGE: String private let _ENCRYPTED_MESSAGE_r: [(Int, NSRange)] @@ -5771,6 +5777,8 @@ public final class PresentationStrings { self.Notifications_MessageNotifications = getValue(dict, "Notifications.MessageNotifications") self.ChannelMembers_WhoCanAddMembersAdminsHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAdminsHelp") self.DialogList_DeleteBotConversationConfirmation = getValue(dict, "DialogList.DeleteBotConversationConfirmation") + self._DialogList_MultipleTyping = getValue(dict, "DialogList.MultipleTyping") + self._DialogList_MultipleTyping_r = extractArgumentRanges(self._DialogList_MultipleTyping) self.Conversation_ClousStorageInfo_Description2 = getValue(dict, "Conversation.ClousStorageInfo.Description2") self._Time_MonthOfYear_m5 = getValue(dict, "Time.MonthOfYear_m5") self._Time_MonthOfYear_m5_r = extractArgumentRanges(self._Time_MonthOfYear_m5) @@ -6389,6 +6397,7 @@ public final class PresentationStrings { self.Channel_AdminLog_EmptyFilterText = getValue(dict, "Channel.AdminLog.EmptyFilterText") self.Channel_AdminLog_EmptyText = getValue(dict, "Channel.AdminLog.EmptyText") self.PrivacySettings_DeleteAccountTitle = getValue(dict, "PrivacySettings.DeleteAccountTitle") + self.Peer_DeletedUser = getValue(dict, "Peer.DeletedUser") self.PrivacyLastSeenSettings_CustomShareSettings_Delete = getValue(dict, "PrivacyLastSeenSettings.CustomShareSettings.Delete") self._ENCRYPTED_MESSAGE = getValue(dict, "ENCRYPTED_MESSAGE") self._ENCRYPTED_MESSAGE_r = extractArgumentRanges(self._ENCRYPTED_MESSAGE) diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 4589fc5b2a..a08248b416 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -170,8 +170,9 @@ public final class PresentationThemeActionSheet { public let inputPlaceholderColor: UIColor public let inputTextColor: UIColor public let inputClearButtonColor: UIColor + public let checkContentColor: UIColor - init(dimColor: UIColor, backgroundType: PresentationThemeActionSheetBackgroundType, opaqueItemBackgroundColor: UIColor, itemBackgroundColor: UIColor, opaqueItemHighlightedBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, opaqueItemSeparatorColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, inputBackgroundColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputClearButtonColor: UIColor) { + init(dimColor: UIColor, backgroundType: PresentationThemeActionSheetBackgroundType, opaqueItemBackgroundColor: UIColor, itemBackgroundColor: UIColor, opaqueItemHighlightedBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, opaqueItemSeparatorColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, inputBackgroundColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputClearButtonColor: UIColor, checkContentColor: UIColor) { self.dimColor = dimColor self.backgroundType = backgroundType self.opaqueItemBackgroundColor = opaqueItemBackgroundColor @@ -189,6 +190,7 @@ public final class PresentationThemeActionSheet { self.inputPlaceholderColor = inputPlaceholderColor self.inputTextColor = inputTextColor self.inputClearButtonColor = inputClearButtonColor + self.checkContentColor = checkContentColor } } diff --git a/TelegramUI/RaiseToListen.swift b/TelegramUI/RaiseToListen.swift index 7233257392..dd12cfc2c6 100644 --- a/TelegramUI/RaiseToListen.swift +++ b/TelegramUI/RaiseToListen.swift @@ -20,4 +20,12 @@ final class RaiseToListenManager { return deactivate() }) } + + func activateBasedOnProximity() { + self.activator.activateBasedOnProximity() + } + + func applicationResignedActive() { + self.activator.applicationResignedActive() + } } diff --git a/TelegramUI/RaiseToListenActivator.h b/TelegramUI/RaiseToListenActivator.h index 0c5edfaab5..f5bc9d2446 100644 --- a/TelegramUI/RaiseToListenActivator.h +++ b/TelegramUI/RaiseToListenActivator.h @@ -7,5 +7,8 @@ - (instancetype)initWithShouldActivate:(bool (^)(void))shouldActivate activate:(void (^)(void))activate deactivate:(void (^)(void))deactivate; +- (void)activateBasedOnProximity; +- (void)applicationResignedActive; + @end diff --git a/TelegramUI/RaiseToListenActivator.m b/TelegramUI/RaiseToListenActivator.m index d851a39336..af52ebd876 100644 --- a/TelegramUI/RaiseToListenActivator.m +++ b/TelegramUI/RaiseToListenActivator.m @@ -1,5 +1,6 @@ #import "RaiseToListenActivator.h" +#import #import #import "DeviceProximityManager.h" @@ -44,6 +45,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) STimer *_timer; id _manager; + CFTimeInterval _activationTimestamp; } @end @@ -109,10 +111,6 @@ static void TGDispatchOnMainThread(dispatch_block_t block) } - (bool)shouldActivate { - /*if ([TGMusicPlayer isHeadsetPluggedIn]) { - return false; - }*/ - if (_shouldActivate) { return _shouldActivate(); } @@ -136,6 +134,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) if (_proximityState) { _activated = true; + _activationTimestamp = CACurrentMediaTime(); if (_activate) { _activate(); } @@ -165,6 +164,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) [_timer invalidate]; _timer = nil; _activated = true; + _activationTimestamp = CACurrentMediaTime(); if (_activate) { _activate(); @@ -183,5 +183,27 @@ static void TGDispatchOnMainThread(dispatch_block_t block) }); } +- (void)activateBasedOnProximity { + if ([[DeviceProximityManager shared] currentValue]) { + [self startCheckingProximity]; + } +} + +- (void)applicationResignedActive { + if (_activated) { + CFTimeInterval duration = CACurrentMediaTime() - _activationTimestamp; + if (duration > 0.0 && duration < 1.0) { + [_timer invalidate]; + _timer = nil; + [self stopCheckingProximity]; + + _activated = false; + if (_deactivate) { + _deactivate(); + } + } + } +} + @end diff --git a/TelegramUI/RenderedTotalUnreadCount.swift b/TelegramUI/RenderedTotalUnreadCount.swift new file mode 100644 index 0000000000..69cda6bc61 --- /dev/null +++ b/TelegramUI/RenderedTotalUnreadCount.swift @@ -0,0 +1,26 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public func renderedTotalUnreadCount(postbox: Postbox) -> Signal { + let unreadCountsKey = PostboxViewKey.unreadCounts(items: [UnreadMessageCountsItem.total(.raw)]) + let inAppSettingsKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.inAppNotificationSettings])) + return postbox.combinedView(keys: [unreadCountsKey, inAppSettingsKey]) + |> map { view -> Int32 in + var value: Int32 = 0 + var style: TotalUnreadCountDisplayStyle = .filtered + if let preferences = view.views[inAppSettingsKey] as? PreferencesView, let inAppSettings = preferences.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings { + style = inAppSettings.totalUnreadCountDisplayStyle + } + if let unreadCounts = view.views[unreadCountsKey] as? UnreadMessageCountsView { + switch style { + case .raw: + value = unreadCounts.count(for: .total(.raw)) ?? 0 + case .filtered: + value = unreadCounts.count(for: .total(.filtered)) ?? 0 + } + } + return value + } + |> distinctUntilChanged +} diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index 7168852a80..9cec07cfa0 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -158,6 +158,12 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.messageDisposable.dispose() } + override 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.theme = theme @@ -214,4 +220,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { dismiss() } } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.interfaceInteraction?.navigateToMessage(self.messageId) + } + } } diff --git a/TelegramUI/Resources/currencies.json b/TelegramUI/Resources/currencies.json index 40a1ead30a..5e332a88fa 100644 --- a/TelegramUI/Resources/currencies.json +++ b/TelegramUI/Resources/currencies.json @@ -1448,4 +1448,4 @@ "spaceBetweenAmountAndSymbol": false, "decimalDigits": 2 } -} \ No newline at end of file +} diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index 22afd041cc..2fadcf3518 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -55,7 +55,7 @@ final class SearchDisplayController { self.containerLayout = (layout, navigationBarFrame.maxY) transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarFrame.maxY, transition: transition) + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarFrame.maxY, transition: transition) } func activate(insertSubnode: (ASDisplayNode) -> Void, placeholder: SearchBarPlaceholderNode) { @@ -66,7 +66,7 @@ final class SearchDisplayController { insertSubnode(self.contentNode) self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false), navigationBarHeight: navigationBarHeight, transition: .immediate) + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: false), navigationBarHeight: navigationBarHeight, transition: .immediate) let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: self.contentNode.supernode) diff --git a/TelegramUI/SelectablePeerNode.swift b/TelegramUI/SelectablePeerNode.swift index 0c7bf3ffd1..d40ff21105 100644 --- a/TelegramUI/SelectablePeerNode.swift +++ b/TelegramUI/SelectablePeerNode.swift @@ -167,7 +167,7 @@ final class SelectablePeerNode: ASDisplayNode { if selected { if self.checkView == nil { - let checkView = TGCheckButtonView(style: TGCheckButtonStyleShare, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.theme.checkBackgroundColor, accentBackgroundColor: self.theme.checkBackgroundColor, defaultBorderColor: .clear, mediaBorderColor: .clear, chatBorderColor: .clear, check: self.theme.checkFillColor, blueColor: self.theme.checkFillColor, barBackgroundColor: self.theme.checkBackgroundColor))! + let checkView = TGCheckButtonView(style: TGCheckButtonStyleShare, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.theme.checkBackgroundColor, accentBackgroundColor: self.theme.checkFillColor, defaultBorderColor: .clear, mediaBorderColor: .clear, chatBorderColor: .clear, check: self.theme.checkColor, blueColor: self.theme.checkFillColor, barBackgroundColor: self.theme.checkBackgroundColor))! self.checkView = checkView checkView.isUserInteractionEnabled = false diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index a04af4c980..85a387d5ca 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -47,7 +47,6 @@ private enum SettingsSection: Int32 { case media case generalSettings case help - case debug } private enum SettingsEntry: ItemListNodeEntry { @@ -67,7 +66,6 @@ private enum SettingsEntry: ItemListNodeEntry { case askAQuestion(PresentationTheme, UIImage?, String) case faq(PresentationTheme, UIImage?, String) - case debug(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -79,8 +77,6 @@ private enum SettingsEntry: ItemListNodeEntry { return SettingsSection.generalSettings.rawValue case .askAQuestion, .faq: return SettingsSection.help.rawValue - case .debug: - return SettingsSection.debug.rawValue } } @@ -112,8 +108,6 @@ private enum SettingsEntry: ItemListNodeEntry { return 11 case .faq: return 12 - case .debug: - return 13 } } @@ -223,12 +217,6 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } - case let .debug(lhsTheme, lhsText): - if case let .debug(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } } } @@ -293,10 +281,6 @@ private enum SettingsEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openFaq() }) - case let .debug(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.pushController(debugController(account: arguments.account, accountManager: arguments.accountManager)) - }) } } } @@ -345,10 +329,6 @@ private func settingsEntries(presentationData: PresentationData, state: Settings entries.append(.askAQuestion(presentationData.theme, SettingsItemIcons.support, presentationData.strings.Settings_Support)) entries.append(.faq(presentationData.theme, SettingsItemIcons.faq, presentationData.strings.Settings_FAQ)) - - if !GlobalExperimentalSettings.isAppStoreBuild { - entries.append(.debug(presentationData.theme, "Debug")) - } } return entries @@ -573,7 +553,7 @@ public func settingsController(account: Account, accountManager: AccountManager) } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: (ASDisplayNode, CGRect)? + var result: ((ASDisplayNode, () -> UIView?), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() @@ -600,6 +580,9 @@ public func settingsController(account: Account, accountManager: AccountManager) navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(account.peerId)) } } + controller.tabBarItemDebugTapAction = { + pushControllerImpl?(debugController(account: account, accountManager: accountManager)) + } return controller } diff --git a/TelegramUI/ShareActionButtonNode.swift b/TelegramUI/ShareActionButtonNode.swift index 2620a510e9..8b7d40c409 100644 --- a/TelegramUI/ShareActionButtonNode.swift +++ b/TelegramUI/ShareActionButtonNode.swift @@ -5,18 +5,19 @@ import Display final class ShareActionButtonNode: HighlightTrackingButtonNode { private let badgeTextColor: UIColor - private let badgeLabel: ASTextNode + private let badgeLabel: TextNode + private var badgeText: NSAttributedString? private let badgeBackground: ASImageNode var badge: String? { didSet { if self.badge != oldValue { if let badge = self.badge { - self.badgeLabel.attributedText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: self.badgeTextColor, paragraphAlignment: .center) + self.badgeText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: self.badgeTextColor, paragraphAlignment: .center) self.badgeLabel.isHidden = false self.badgeBackground.isHidden = false } else { - self.badgeLabel.attributedText = nil + self.badgeText = nil self.badgeLabel.isHidden = true self.badgeBackground.isHidden = true } @@ -29,7 +30,7 @@ final class ShareActionButtonNode: HighlightTrackingButtonNode { init(badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { self.badgeTextColor = badgeTextColor - self.badgeLabel = ASTextNode() + self.badgeLabel = TextNode() self.badgeLabel.isHidden = true self.badgeLabel.isLayerBacked = true self.badgeLabel.displaysAsynchronously = false @@ -52,13 +53,14 @@ final class ShareActionButtonNode: HighlightTrackingButtonNode { super.layout() if !self.badgeLabel.isHidden { - let badgeSize = self.badgeLabel.measure(CGSize(width: 100.0, height: 100.0)) + let (badgeLayout, badgeApply) = TextNode.asyncLayout(self.badgeLabel)(TextNodeLayoutArguments(attributedString: self.badgeText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let _ = badgeApply() - let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 10.0 + 1.0), height: 22.0) + let backgroundSize = CGSize(width: max(22.0, badgeLayout.size.width + 10.0 + 1.0), height: 22.0) let backgroundFrame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 6.0, y: self.bounds.size.height - 38.0), size: backgroundSize) self.badgeBackground.frame = backgroundFrame - self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: backgroundFrame.minY + 2.0), size: badgeSize) + self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeLayout.size.width / 2.0), y: backgroundFrame.minY + 3.0), size: badgeLayout.size) } } } diff --git a/TelegramUI/ShareControllerPeerGridItem.swift b/TelegramUI/ShareControllerPeerGridItem.swift index d3536111f8..43dd4701c6 100644 --- a/TelegramUI/ShareControllerPeerGridItem.swift +++ b/TelegramUI/ShareControllerPeerGridItem.swift @@ -144,7 +144,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { func setup(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: Peer, chatPeer: Peer?) { if self.currentState == nil || self.currentState!.0 !== account || !arePeersEqual(self.currentState!.1, peer) { - let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor) + let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: theme.chatList.secretTitleColor, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.checkContentColor) self.peerNode.theme = itemTheme self.peerNode.setup(account: account, strings: strings, peer: peer, chatPeer: chatPeer) self.currentState = (account, peer, chatPeer) diff --git a/TelegramUI/ShareLoadingContainerNode.swift b/TelegramUI/ShareLoadingContainerNode.swift index e00e10443a..827cedc567 100644 --- a/TelegramUI/ShareLoadingContainerNode.swift +++ b/TelegramUI/ShareLoadingContainerNode.swift @@ -9,7 +9,7 @@ final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContainerNode private let activityIndicator: ActivityIndicator init(theme: PresentationTheme) { - self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(theme.actionSheet.controlAccentColor, 50.0)) + self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(theme.actionSheet.controlAccentColor, 50.0, 2.0)) super.init() diff --git a/TelegramUI/SoftwareVideoLayerFrameManager.swift b/TelegramUI/SoftwareVideoLayerFrameManager.swift index 763615769d..bb35d73c81 100644 --- a/TelegramUI/SoftwareVideoLayerFrameManager.swift +++ b/TelegramUI/SoftwareVideoLayerFrameManager.swift @@ -100,7 +100,7 @@ final class SoftwareVideoLayerFrameManager { self.polling = true let maxPts = self.maxPts self.queue.addTask(ThreadPoolTask { [weak self] state in - if state.cancelled { + if state.cancelled.with({ $0 }) { return } if let strongSelf = self { diff --git a/TelegramUI/StickerPreviewControllerNode.swift b/TelegramUI/StickerPreviewControllerNode.swift index ba35c64832..adc0284541 100644 --- a/TelegramUI/StickerPreviewControllerNode.swift +++ b/TelegramUI/StickerPreviewControllerNode.swift @@ -58,10 +58,6 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.imageNode.frame = imageFrame self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize) - - /*let boundingFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - boundingSize.width) / 2.0), y: (bounds.size.height - boundingSize.height) / 2.0), size: boundingSize) - let textSize = CGSize(width: 32.0, height: 24.0) - self.textNode.frame = CGRect(origin: CGPoint(x: boundingFrame.maxX - 1.0 - textSize.width, y: boundingFrame.height + 10.0 - textSize.height), size: textSize)*/ } } diff --git a/TelegramUI/StickerPreviewPeekContent.swift b/TelegramUI/StickerPreviewPeekContent.swift new file mode 100644 index 0000000000..14629e6c66 --- /dev/null +++ b/TelegramUI/StickerPreviewPeekContent.swift @@ -0,0 +1,84 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +final class StickerPreviewPeekContent: PeekControllerContent { + let account: Account + let item: StickerPackItem + let menu: [PeekControllerMenuItem] + + init(account: Account, item: StickerPackItem, menu: [PeekControllerMenuItem]) { + self.account = account + self.item = item + self.menu = menu + } + + func presentation() -> PeekControllerContentPresentation { + return .freeform + } + + func menuActivation() -> PeerkControllerMenuActivation { + return .press + } + + func menuItems() -> [PeekControllerMenuItem] { + return self.menu + } + + func node() -> PeekControllerContentNode & ASDisplayNode { + return StickerPreviewPeekContentNode(account: self.account, item: self.item) + } +} + +private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode { + private let account: Account + private let item: StickerPackItem + + private var textNode: ASTextNode + private var imageNode: TransformImageNode + private var containerLayout: (ContainerViewLayout, CGFloat)? + + init(account: Account, item: StickerPackItem) { + self.account = account + self.item = item + + self.textNode = ASTextNode() + self.imageNode = TransformImageNode() + self.imageNode.addSubnode(self.textNode) + + for case let .Sticker(text, _, _) in item.file.attributes { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) + break + } + self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false)) + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.imageNode) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size) + + if let dimensitons = self.item.file.dimensions { + let textSpacing: CGFloat = 10.0 + let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) + + let imageSize = dimensitons.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: textSize.height + textSpacing), size: imageSize) + self.imageNode.frame = imageFrame + + self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize) + + return CGSize(width: size.width, height: imageFrame.height + textSize.height + textSpacing) + } else { + return CGSize(width: size.width, height: 10.0) + } + } +} diff --git a/TelegramUI/StorageUsageController.swift b/TelegramUI/StorageUsageController.swift index 207feae076..28b7a0a2e2 100644 --- a/TelegramUI/StorageUsageController.swift +++ b/TelegramUI/StorageUsageController.swift @@ -181,7 +181,7 @@ private func storageUsageControllerEntries(presentationData: PresentationData, c peerSizes += combinedSize } - entries.append(.clearAll(presentationData.theme, presentationData.strings.Cache_ClearCache, dataSizeString(Int(peerSizes + stats.otherSize + stats.cacheSize)))) + entries.append(.clearAll(presentationData.theme, presentationData.strings.Cache_ClearCache, dataSizeString(Int(peerSizes + stats.otherSize + stats.cacheSize + stats.tempSize)))) var index: Int32 = 0 for (peerId, size) in statsByPeerId.sorted(by: { $0.1 > $1.1 }) { @@ -291,8 +291,8 @@ func storageUsageController(account: Account) -> ViewController { } } - if stats.cacheSize + stats.otherSize > 10 * 1024 { - otherSize = (true, stats.cacheSize + stats.otherSize) + if stats.cacheSize + stats.otherSize + stats.tempSize > 10 * 1024 { + otherSize = (true, stats.cacheSize + stats.otherSize + stats.tempSize) } var itemIndex = 0 @@ -395,19 +395,32 @@ func storageUsageController(account: Account) -> ViewController { var updatedOtherPaths = stats.otherPaths var updatedOtherSize = stats.otherSize var updatedCacheSize = stats.cacheSize + var updatedTempPaths = stats.tempPaths + var updatedTempSize = stats.tempSize var signal: Signal = clearCachedMediaResources(account: account, mediaResourceIds: clearResourceIds) if otherSize.0 { - signal = signal |> then(account.postbox.mediaBox.removeOtherCachedResources(paths: stats.otherPaths)) + let removeTempFiles: Signal = Signal { subscriber in + let fileManager = FileManager.default + for path in stats.tempPaths { + let _ = try? fileManager.removeItem(atPath: path) + } + + subscriber.putCompletion() + return EmptyDisposable + } |> runOn(Queue.concurrentDefaultQueue()) + signal = signal |> then(account.postbox.mediaBox.removeOtherCachedResources(paths: stats.otherPaths)) |> then(removeTempFiles) } if otherSize.0 { updatedOtherPaths = [] updatedOtherSize = 0 updatedCacheSize = 0 + updatedTempPaths = [] + updatedTempSize = 0 } - statsPromise.set(.single(.result(CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: updatedOtherSize, otherPaths: updatedOtherPaths, cacheSize: updatedCacheSize)))) + statsPromise.set(.single(.result(CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: updatedOtherSize, otherPaths: updatedOtherPaths, cacheSize: updatedCacheSize, tempPaths: updatedTempPaths, tempSize: updatedTempSize)))) clearDisposable.set(signal.start()) } @@ -521,7 +534,7 @@ func storageUsageController(account: Account) -> ViewController { } } - statsPromise.set(.single(.result(CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize)))) + statsPromise.set(.single(.result(CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize)))) clearDisposable.set(clearCachedMediaResources(account: account, mediaResourceIds: clearResourceIds).start()) } diff --git a/TelegramUI/StringForMessageTimestampStatus.swift b/TelegramUI/StringForMessageTimestampStatus.swift new file mode 100644 index 0000000000..f42b319d36 --- /dev/null +++ b/TelegramUI/StringForMessageTimestampStatus.swift @@ -0,0 +1,29 @@ +import Foundation +import Postbox +import TelegramCore + +func stringForMessageTimestampStatus(message: Message, timeFormat: PresentationTimeFormat, strings: PresentationStrings) -> String { + var dateText = stringForMessageTimestamp(timestamp: message.timestamp, timeFormat: timeFormat) + + var authorTitle: String? + if let author = message.author as? TelegramUser { + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + authorTitle = author.displayTitle + } + } else { + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + for attribute in message.attributes { + if let attribute = attribute as? AuthorSignatureMessageAttribute { + authorTitle = attribute.signature + break + } + } + } + } + + if let authorTitle = authorTitle, !authorTitle.isEmpty { + dateText = "\(authorTitle), \(dateText)" + } + + return dateText +} diff --git a/TelegramUI/StringWithAppliedEntities.swift b/TelegramUI/StringWithAppliedEntities.swift index d160756b2f..3451ff93eb 100644 --- a/TelegramUI/StringWithAppliedEntities.swift +++ b/TelegramUI/StringWithAppliedEntities.swift @@ -90,6 +90,9 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba string.addAttribute(NSAttributedStringKey.font, value: italicFont, range: range) case .Mention: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) + if linkColor.isEqual(baseColor) { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } if linkFont !== baseFont { string.addAttribute(NSAttributedStringKey.font, value: linkFont, range: range) } @@ -99,6 +102,9 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute), value: nsString!.substring(with: range), range: range) case let .TextMention(peerId): string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) + if linkColor.isEqual(baseColor) { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } if linkFont !== baseFont { string.addAttribute(NSAttributedStringKey.font, value: linkFont, range: range) } @@ -118,16 +124,25 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba skipEntity = true let combinedRange = NSRange(location: range.location, length: nextRange.location + nextRange.length - range.location) string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: combinedRange) + if linkColor.isEqual(baseColor) { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: combinedRange) + } string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute), value: TelegramHashtag(peerName: peerName, hashtag: hashtag), range: combinedRange) } } } if !skipEntity { string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) + if linkColor.isEqual(baseColor) { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) } case .BotCommand: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) + if linkColor.isEqual(baseColor) { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } if nsString == nil { nsString = text as NSString } diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index ef8c762a54..76e449424e 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -13,8 +13,9 @@ public final class TelegramApplicationBindings { public let applicationInForeground: Signal public let applicationIsActive: Signal public let clearMessageNotifications: ([MessageId]) -> Void + public let pushIdleTimerExtension: () -> Disposable - public init(isMainApp: Bool, openUrl: @escaping (String) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void) { + public init(isMainApp: Bool, openUrl: @escaping (String) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable) { self.isMainApp = isMainApp self.openUrl = openUrl self.canOpenUrl = canOpenUrl @@ -23,6 +24,7 @@ public final class TelegramApplicationBindings { self.applicationInForeground = applicationInForeground self.applicationIsActive = applicationIsActive self.clearMessageNotifications = clearMessageNotifications + self.pushIdleTimerExtension = pushIdleTimerExtension } } @@ -97,15 +99,22 @@ public final class TelegramApplicationContext { self.presentationDataDisposable.set(self._presentationData.get().start(next: { [weak self] next in if let strongSelf = self { var stringsUpdated = false + var themeUpdated = false let _ = strongSelf.currentPresentationData.modify { current in if next.strings !== current.strings { stringsUpdated = true } + if next.theme !== current.theme { + themeUpdated = true + } return next } if stringsUpdated { updateLegacyLocalization(strings: next.strings) } + if themeUpdated { + updateLegacyTheme() + } } })) diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index da66b48030..f5fe8113f7 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -44,6 +44,9 @@ public class TelegramController: ViewController { private var locationBroadcastDisposable: Disposable? private(set) var playlistStateAndType: (SharedMediaPlaylistItem, MusicPlaybackSettingsOrder, MediaManagerPlayerType)? + + var tempVoicePlaylistEnded: (() -> Void)? + private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode? @@ -79,7 +82,14 @@ public class TelegramController: ViewController { if let playlistStateAndType = playlistStateAndType { strongSelf.playlistStateAndType = (playlistStateAndType.0.item, playlistStateAndType.0.order, playlistStateAndType.1) } else { + var voiceEnded = false + if strongSelf.playlistStateAndType?.2 == .voice { + voiceEnded = true + } strongSelf.playlistStateAndType = nil + if voiceEnded { + strongSelf.tempVoicePlaylistEnded?() + } } strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index c9ef0c14c6..b0f571cc54 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -16,6 +16,10 @@ func updateLegacyLocalization(strings: PresentationStrings) { legacyLocalization = TGLocalization(version: 0, code: strings.languageCode, dict: strings.dict, isActive: true) } +func updateLegacyTheme() { + TGCheckButtonView.resetCache() +} + public func updateLegacyComponentsAccount(_ account: Account?) { legacyComponentsAccount = account } diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index 5dedfc6297..5dbf554d3f 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -394,6 +394,10 @@ final class TextNode: ASDisplayNode { } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + if isCancelled() { + return + } + let context = UIGraphicsGetCurrentContext()! context.setAllowsAntialiasing(true) diff --git a/TelegramUI/ThemeGalleryItem.swift b/TelegramUI/ThemeGalleryItem.swift index f792ec0367..3f7b5a4e8d 100644 --- a/TelegramUI/ThemeGalleryItem.swift +++ b/TelegramUI/ThemeGalleryItem.swift @@ -101,13 +101,13 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) - let copyView = node.view.snapshotContentTree()! + let copyView = node.1()! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame @@ -137,17 +137,17 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { }) } - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + override func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) var positionCompleted = false var boundsCompleted = false var copyCompleted = false - let copyView = node.view.snapshotContentTree()! + let copyView = node.1()! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index 536c3607db..7dc5e0abb9 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -88,7 +88,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, requestMessageUpdate: { _ in diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index 939ae5864b..41c2e4c295 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -327,12 +327,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateIn(from node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { guard let videoNode = self.videoNode else { return } - if let node = node as? OverlayMediaItemNode { + if let node = node.0 as? OverlayMediaItemNode { var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) @@ -345,20 +345,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) } else { - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - let surfaceCopyView = node.view.snapshotContentTree()! - let copyView = node.view.snapshotContentTree()! + let surfaceCopyView = node.1()! + let copyView = node.1()! addToTransitionSurface(surfaceCopyView) var transformedSurfaceFrame: CGRect? var transformedSurfaceFinalFrame: CGRect? if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.view.convert(node.view.bounds, to: contentSurface) + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) transformedSurfaceFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) } @@ -399,7 +399,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) if let pictureInPictureNode = self.pictureInPictureNode { - let transformedPlaceholderFrame = node.view.convert(node.view.bounds, to: pictureInPictureNode.view) + let transformedPlaceholderFrame = node.0.view.convert(node.0.view.bounds, to: pictureInPictureNode.view) let transform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0) pictureInPictureNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: pictureInPictureNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) @@ -413,30 +413,30 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateOut(to node: ASDisplayNode, addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { guard let videoNode = self.videoNode else { completion() return } - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) var positionCompleted = false var boundsCompleted = false var copyCompleted = false - let copyView = node.view.snapshotContentTree()! - let surfaceCopyView = node.view.snapshotContentTree()! + let copyView = node.1()! + let surfaceCopyView = node.1()! addToTransitionSurface(surfaceCopyView) var transformedSurfaceFrame: CGRect? var transformedSurfaceCopyViewInitialFrame: CGRect? if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.view.convert(node.view.bounds, to: contentSurface) + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) transformedSurfaceCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) } @@ -493,7 +493,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) if let pictureInPictureNode = self.pictureInPictureNode { - let transformedPlaceholderFrame = node.view.convert(node.view.bounds, to: pictureInPictureNode.view) + let transformedPlaceholderFrame = node.0.view.convert(node.0.view.bounds, to: pictureInPictureNode.view) let pictureInPictureTransform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0) pictureInPictureNode.layer.animate(from: NSValue(caTransform3D: pictureInPictureNode.layer.transform), to: NSValue(caTransform3D: pictureInPictureTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in }) @@ -652,7 +652,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { _, _ in if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { - return GalleryTransitionArguments(transitionNode: overlayNode, addToTransitionSurface: { [weak overlaySupernode, weak overlayNode] view in + return GalleryTransitionArguments(transitionNode: (overlayNode, { [weak overlayNode] in + return overlayNode?.view.snapshotContentTree() + }), addToTransitionSurface: { [weak overlaySupernode, weak overlayNode] view in overlaySupernode?.view.addSubview(view) overlayNode?.canAttachContent = false }) diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index 9727984246..58f64bb7e0 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -13,6 +13,7 @@ private enum ParsedInternalUrl { case peerName(String, ParsedInternalPeerUrlParameter?) case stickerPack(String) case join(String) + case proxy(host: String, port: Int32, username: String?, password: String?) } private enum ParsedUrl { @@ -28,6 +29,7 @@ enum ResolvedUrl { case channelMessage(peerId: PeerId, messageId: MessageId) case stickerPack(name: String) case instantView(TelegramMediaWebpage, String?) + case proxy(host: String, port: Int32, username: String?, password: String?) case join(String) } @@ -41,14 +43,40 @@ private func parseInternalUrl(query: String) -> ParsedInternalUrl? { let peerName: String = pathComponents[0] if pathComponents.count == 1 { if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "start" { - return .peerName(peerName, .botStart(value)) - } else if queryItem.name == "startgroup" { - return .peerName(peerName, .groupBotStart(value)) - } else if queryItem.name == "game" { - return nil + if peerName == "socks" { + var server: String? + var port: String? + var user: String? + var pass: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "server" || queryItem.name == "proxy" { + server = value + } else if queryItem.name == "port" { + port = value + } else if queryItem.name == "user" { + user = value + } else if queryItem.name == "pass" { + pass = value + } + } + } + } + + if let server = server, !server.isEmpty, let port = port, let portValue = Int32(port) { + return .proxy(host: server, port: portValue, username: user, password: pass) + } + } else { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "start" { + return .peerName(peerName, .botStart(value)) + } else if queryItem.name == "startgroup" { + return .peerName(peerName, .groupBotStart(value)) + } else if queryItem.name == "game" { + return nil + } } } } @@ -99,6 +127,8 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig return .single(.stickerPack(name: name)) case let .join(link): return .single(.join(link)) + case let .proxy(host, port, username, password): + return .single(.proxy(host: host, port: port, username: username, password: password)) } } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 910d02ce81..08e9a9f092 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -549,7 +549,6 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } let galleryController = AvatarGalleryController(account: account, peer: peer, remoteEntries: cachedAvatarEntries.with { $0 }, replaceRootController: { controller, ready in - }) hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first @@ -869,7 +868,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: (ASDisplayNode, CGRect)? + var result: ((ASDisplayNode, () -> UIView?), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() diff --git a/TelegramUI/ZoomableContentGalleryItemNode.swift b/TelegramUI/ZoomableContentGalleryItemNode.swift index cc2b7b0c47..2e63b1e3c5 100644 --- a/TelegramUI/ZoomableContentGalleryItemNode.swift +++ b/TelegramUI/ZoomableContentGalleryItemNode.swift @@ -203,4 +203,12 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate) } } + + override func contentSize() -> CGSize? { + if let (_, contentNode) = self.zoomableContent { + let size = contentNode.view.convert(contentNode.bounds, to: self.view).size + return CGSize(width: floor(size.width), height: floor(size.height)) + } + return nil + } }