From baf29247eb439be2dedb7f941e40f722599517c9 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 5 Sep 2017 21:27:04 +0300 Subject: [PATCH] no message --- .../Contents.json | 1 + ...odernConversationAudioSlideToCancel@2x.png | Bin 111 -> 496 bytes ...odernConversationAudioSlideToCancel@3x.png | Bin 0 -> 645 bytes .../IconAttachment.imageset/Contents.json | 12 +- .../ModernConversationAttach@2x.png | Bin 0 -> 1473 bytes .../ModernConversationAttach@3x.png | Bin 0 -> 2158 bytes .../IconAttachment.imageset/ic_attach.pdf | Bin 16786 -> 0 bytes .../IconMicrophone.imageset/Contents.json | 12 +- .../ModernConversationMicButton@2x.png | Bin 0 -> 843 bytes .../ModernConversationMicButton@3x.png | Bin 0 -> 1262 bytes .../Text/IconMicrophone.imageset/ic_voice.pdf | Bin 8160 -> 0 bytes .../Text/IconSend.imageset/Contents.json | 12 +- .../ModernConversationSend@2x.png | Bin 0 -> 682 bytes .../ModernConversationSend@3x.png | Bin 0 -> 1019 bytes .../Input/Text/IconSend.imageset/Send.pdf | Bin 9016 -> 0 bytes .../Text/IconVideo.imageset}/Contents.json | 4 +- .../IconVideo.imageset/RecordVideoIcon@2x.png | Bin 0 -> 651 bytes .../IconVideo.imageset/RecordVideoIcon@3x.png | Bin 0 -> 1098 bytes .../ActionIcon.imageset/Contents.json | 22 + .../ActionIcon.imageset/ic_share@2x.png | Bin 0 -> 1216 bytes .../ActionIcon.imageset/ic_share@3x.png | Bin 0 -> 1806 bytes .../InstantPageBackArrow@2x.png | Bin 306 -> 0 bytes .../Contents.json | 3 +- .../MoreIcon.imageset/ic_more@2x.png | Bin 0 -> 453 bytes .../MoreIcon.imageset/ic_more@3x.png | Bin 0 -> 350 bytes .../PanelCheck.imageset/Contents.json | 22 + .../InstantViewCheck@2x.png | Bin 0 -> 359 bytes .../InstantViewCheck@3x.png | Bin 0 -> 502 bytes .../SettingsArrow.imageset/Contents.json | 22 + .../InstantViewRightCorner@2x.png | Bin 0 -> 1236 bytes .../InstantViewRightCorner@3x.png | Bin 0 -> 1368 bytes .../Contents.json | 22 + .../InstantViewBrightnessMaxIcon@2x.png | Bin 0 -> 513 bytes .../InstantViewBrightnessMaxIcon@3x.png | Bin 0 -> 447 bytes .../Contents.json | 22 + .../InstantViewBrightnessMinIcon@2x.png | Bin 0 -> 398 bytes .../InstantViewBrightnessMinIcon@3x.png | Bin 0 -> 356 bytes .../Contents.json | 22 + .../InstantViewFontMaxIcon@2x.png | Bin 0 -> 508 bytes .../InstantViewFontMaxIcon@3x.png | Bin 0 -> 687 bytes .../Contents.json | 22 + .../InstantViewFontMinIcon@2x.png | Bin 0 -> 277 bytes .../InstantViewFontMinIcon@3x.png | Bin 0 -> 372 bytes .../InstantViewSettingsIcon@2x.png | Bin 829 -> 0 bytes .../InstantViewSettingsIcon@3x.png | Bin 1133 -> 0 bytes TelegramUI.xcodeproj/project.pbxproj | 138 ++++- .../AuthorizationSequenceController.swift | 2 +- .../AutomaticMediaDownloadSettings.swift | 16 +- .../BotCheckoutInfoControllerNode.swift | 14 +- TelegramUI/CallListControllerNode.swift | 4 +- TelegramUI/ChatButtonKeyboardInputNode.swift | 8 + TelegramUI/ChatController.swift | 180 +++++- TelegramUI/ChatControllerNode.swift | 8 +- TelegramUI/ChatHistoryGridNode.swift | 14 +- TelegramUI/ChatHistoryListNode.swift | 79 ++- TelegramUI/ChatHistoryViewForLocation.swift | 82 ++- TelegramUI/ChatInterfaceInputContexts.swift | 8 +- TelegramUI/ChatInterfaceState.swift | 123 +++- .../ChatInterfaceStateInputPanels.swift | 4 +- TelegramUI/ChatListItem.swift | 6 +- TelegramUI/ChatListNode.swift | 4 +- TelegramUI/ChatMediaInputNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 80 ++- .../ChatPanelInterfaceInteraction.swift | 19 +- .../ChatTextInputAudioRecordingButton.swift | 131 ---- ...xtInputAudioRecordingCancelIndicator.swift | 59 +- .../ChatTextInputMediaRecordingButton.swift | 386 ++++++++++++ TelegramUI/ChatTextInputPanelNode.swift | 385 ++++++++---- TelegramUI/ContactListNode.swift | 2 +- TelegramUI/DefaultDarkPresentationTheme.swift | 4 +- TelegramUI/DefaultPresentationTheme.swift | 7 +- TelegramUI/FetchCachedRepresentations.swift | 26 +- TelegramUI/GalleryControllerNode.swift | 5 + TelegramUI/GalleryPagerNode.swift | 5 +- TelegramUI/GeneratedMediaStoreSettings.swift | 4 +- TelegramUI/InAppNotificationSettings.swift | 4 +- TelegramUI/InstantImageGalleryItem.swift | 211 +++++++ TelegramUI/InstantPageAnchorItem.swift | 6 +- TelegramUI/InstantPageAudioItem.swift | 55 ++ TelegramUI/InstantPageAudioNode.swift | 264 ++++++++ TelegramUI/InstantPageController.swift | 54 +- TelegramUI/InstantPageControllerNode.swift | 574 +++++++++++++++--- TelegramUI/InstantPageGalleryController.swift | 255 ++++++++ .../InstantPageGalleryFooterContentNode.swift | 76 +++ TelegramUI/InstantPageImageItem.swift | 61 ++ TelegramUI/InstantPageImageNode.swift | 102 ++++ TelegramUI/InstantPageItem.swift | 6 +- TelegramUI/InstantPageLayout.swift | 339 ++++++++++- TelegramUI/InstantPageLayoutSpacings.swift | 2 +- TelegramUI/InstantPageManagedMediaId.swift | 27 + .../InstantPageMediaAudioPlaylist.swift | 133 ++++ TelegramUI/InstantPageMediaNode.swift | 110 ---- TelegramUI/InstantPageNavigationBar.swift | 150 +++-- TelegramUI/InstantPageNode.swift | 4 + TelegramUI/InstantPagePeerReferenceItem.swift | 53 ++ TelegramUI/InstantPagePeerReferenceNode.swift | 271 +++++++++ ...ift => InstantPagePlayableVideoItem.swift} | 28 +- TelegramUI/InstantPagePlayableVideoNode.swift | 116 ++++ .../InstantPagePresentationSettings.swift | 102 ++++ ...InstantPageSettingsBacklightItemNode.swift | 76 +++ ...nstantPageSettingsFontFamilyItemNode.swift | 89 +++ .../InstantPageSettingsFontSizeItemNode.swift | 82 +++ TelegramUI/InstantPageSettingsItemNode.swift | 125 ++++ TelegramUI/InstantPageSettingsItemTheme.swift | 101 +++ TelegramUI/InstantPageSettingsNode.swift | 241 ++++++++ .../InstantPageSettingsSwitchItemNode.swift | 86 +++ .../InstantPageSettingsThemeItemNode.swift | 167 +++++ TelegramUI/InstantPageShapeItem.swift | 6 +- TelegramUI/InstantPageSlideshowItem.swift | 50 ++ TelegramUI/InstantPageSlideshowItemNode.swift | 417 +++++++++++++ TelegramUI/InstantPageTextItem.swift | 269 ++++---- TelegramUI/InstantPageTheme.swift | 190 +++++- TelegramUI/InstantPageTileNode.swift | 22 +- TelegramUI/InstantPageWebEmbedItem.swift | 6 +- TelegramUI/InstantPageWebEmbedNode.swift | 10 + TelegramUI/InstantVideoNode.swift | 10 + TelegramUI/ItemListControllerNode.swift | 10 +- TelegramUI/LegacyController.swift | 10 + TelegramUI/LegacyInstantVideoController.swift | 156 +++++ TelegramUI/MapResources.swift | 4 +- TelegramUI/MediaPlayerScrubbingNode.swift | 24 +- TelegramUI/MediaResources.swift | 18 +- TelegramUI/MultiplexedSoftwareVideoNode.swift | 3 + TelegramUI/Notices.swift | 6 +- .../PeerMediaCollectionController.swift | 8 +- TelegramUI/PhotoResources.swift | 8 - TelegramUI/PreferencesKeys.swift | 2 + .../PreparedChatHistoryViewTransition.swift | 104 ++-- TelegramUI/PresentationPasscodeSettings.swift | 4 +- TelegramUI/PresentationResourceKey.swift | 3 + TelegramUI/PresentationResourcesChat.swift | 18 + TelegramUI/PresentationTheme.swift | 74 +-- TelegramUI/PresentationThemeSettings.swift | 10 +- TelegramUI/RadialStatusNode.swift | 58 +- TelegramUI/ShareControllerNode.swift | 8 + .../StickerPackPreviewControllerNode.swift | 8 + .../TelegramInitializeLegacyComponents.swift | 25 + TelegramUI/UserInfoController.swift | 2 +- TelegramUI/VoiceCallSettings.swift | 4 +- TelegramUI/Wallpapers.swift | 4 +- .../ZoomableContentGalleryItemNode.swift | 3 + third-party/opus/include/opus/opus.h | 4 +- 142 files changed, 6533 insertions(+), 998 deletions(-) create mode 100644 Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@3x.png create mode 100644 Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@3x.png delete mode 100644 Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf create mode 100644 Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@3x.png delete mode 100644 Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf create mode 100644 Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png delete mode 100644 Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf rename Images.xcassets/{Instant View/SettingsIcon.imageset => Chat/Input/Text/IconVideo.imageset}/Contents.json (71%) create mode 100644 Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@3x.png create mode 100644 Images.xcassets/Instant View/ActionIcon.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/ActionIcon.imageset/ic_share@2x.png create mode 100644 Images.xcassets/Instant View/ActionIcon.imageset/ic_share@3x.png delete mode 100644 Images.xcassets/Instant View/BackArrow.imageset/InstantPageBackArrow@2x.png rename Images.xcassets/Instant View/{BackArrow.imageset => MoreIcon.imageset}/Contents.json (78%) create mode 100644 Images.xcassets/Instant View/MoreIcon.imageset/ic_more@2x.png create mode 100644 Images.xcassets/Instant View/MoreIcon.imageset/ic_more@3x.png create mode 100644 Images.xcassets/Instant View/PanelCheck.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@2x.png create mode 100644 Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@3x.png create mode 100644 Images.xcassets/Instant View/SettingsArrow.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@2x.png create mode 100644 Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@3x.png create mode 100644 Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@2x.png create mode 100644 Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@3x.png create mode 100644 Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@2x.png create mode 100644 Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@3x.png create mode 100644 Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@2x.png create mode 100644 Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@3x.png create mode 100644 Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@2x.png create mode 100644 Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@3x.png delete mode 100644 Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@2x.png delete mode 100644 Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png delete mode 100644 TelegramUI/ChatTextInputAudioRecordingButton.swift create mode 100644 TelegramUI/ChatTextInputMediaRecordingButton.swift create mode 100644 TelegramUI/InstantImageGalleryItem.swift create mode 100644 TelegramUI/InstantPageAudioItem.swift create mode 100644 TelegramUI/InstantPageAudioNode.swift create mode 100644 TelegramUI/InstantPageGalleryController.swift create mode 100644 TelegramUI/InstantPageGalleryFooterContentNode.swift create mode 100644 TelegramUI/InstantPageImageItem.swift create mode 100644 TelegramUI/InstantPageImageNode.swift create mode 100644 TelegramUI/InstantPageManagedMediaId.swift create mode 100644 TelegramUI/InstantPageMediaAudioPlaylist.swift delete mode 100644 TelegramUI/InstantPageMediaNode.swift create mode 100644 TelegramUI/InstantPagePeerReferenceItem.swift create mode 100644 TelegramUI/InstantPagePeerReferenceNode.swift rename TelegramUI/{InstantPageMediaItem.swift => InstantPagePlayableVideoItem.swift} (54%) create mode 100644 TelegramUI/InstantPagePlayableVideoNode.swift create mode 100644 TelegramUI/InstantPagePresentationSettings.swift create mode 100644 TelegramUI/InstantPageSettingsBacklightItemNode.swift create mode 100644 TelegramUI/InstantPageSettingsFontFamilyItemNode.swift create mode 100644 TelegramUI/InstantPageSettingsFontSizeItemNode.swift create mode 100644 TelegramUI/InstantPageSettingsItemNode.swift create mode 100644 TelegramUI/InstantPageSettingsItemTheme.swift create mode 100644 TelegramUI/InstantPageSettingsNode.swift create mode 100644 TelegramUI/InstantPageSettingsSwitchItemNode.swift create mode 100644 TelegramUI/InstantPageSettingsThemeItemNode.swift create mode 100644 TelegramUI/InstantPageSlideshowItem.swift create mode 100644 TelegramUI/InstantPageSlideshowItemNode.swift create mode 100644 TelegramUI/LegacyInstantVideoController.swift diff --git a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json index 0932520418..e885e800ab 100644 --- a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json @@ -11,6 +11,7 @@ }, { "idiom" : "universal", + "filename" : "ModernConversationAudioSlideToCancel@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png index 890ef7345a7b00ac3c77cd996daffcee71020e44..4eaca6a59020e74d3fbbbf6c27dae5b76f643373 100644 GIT binary patch literal 496 zcmVk00001b5ch_0Itp) z=>Px$s!2paR7ee-mO)O!Kp2HPe}Tval(i@D3?#19#FdL`NQh~aLx@4|04Tefa0wF5 zV4{bBD*%~sUg2-1(oSdEgmk9q`{um|e?kz{xW*q}SNK3GeD&jLGE_>AQZa?sRX$Q94CZNzk zRc;e-)PYNG6OeOolsgDk%|wN9F+hI(_5-fcW-t1OUXRk6Z^3 z^X1w{p#wl%`zQqf;?jo|07z9Ir5!-bm#vRV2Y}f6aB{#-@Y9w%xzPLcU<$>JlLH2z m4wG{8MP001@!1^@s6G7~a@00001b5ch_0Itp) z=>Px%KS@MER9FeMm`_RrF%*ZBRG}7KxUpU9!V`Fb-a!S0LKjl?4pnIHP=ziE!U*cp zrQphIbZ?h}I7)5C{6okwyjQHwB(I$Hz?{g^RUm8AXaD&OPMlCA`MjvPr1oF4vvK?HItygM#I&1a|E^}F}HYd)ar zHRM)sH>1z0DKikc5^^SUByuafLuR{eJp);mp9};uTZW$;1R!cZGa&#``YD7^`YD89 zHB^4)LI9%hvk(GMs2?jxs2?i`RukgK8Um27A14SvK7O1bK7O1bSdFV6F9<*`e!L+7 z+4>2B*!m&KMv!bQ^#jgaKPs1-u#dx_?E*kwMSjpv2GM?0In?0vetq?U#EA}yQh(^g feTh}>@V)B?eXN*y$P!Y>00000NkvXXu0mjfaG4a+ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json index a4e9ce4cfd..6a61e6831b 100644 --- a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json @@ -2,7 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_attach.pdf" + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernConversationAttach@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ModernConversationAttach@3x.png", + "scale" : "3x" } ], "info" : { diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@2x.png b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..60e641e6d584e300b69b3e903290f5877c393b1c GIT binary patch literal 1473 zcmV;y1wQ(TP)RB+R;^;|ojOmpZOTGnK)coeEz1!^W81cEJ2{GNp4hfAJ-ZKGv-4Qh zi`cep+qUiGt4=cS*LE^HzyF^Yz3v^5! zSipz6x~`mNT<7KBCgyxI7hpMcSn1?J2Aft6=k)dOyu(L~7%@?k1C%-5((D7R7hX7Z zs@9|F=V*T3fF^>a6D`f&$6(WU7yLb1qiHp_4|KAXdhSKUsTw^q${Z(jU5A?sv7w(g z2`rv!fxVl-Hcl^et>WUNgu6NgEp4`vo7xPpr(0%xm%j4KDPvz4ae4*#xE${o{VyCH zAIy1~g5?_60()l?SWnXvV*GGV(~e`ho-UVX@&JE|p63*mVQq|O)kkBxo%`Fk@c^|M z6ws3rtgq=H$2buPbACQcu;*Alds`#0e)lCadVo*WfdzAbTPiZ^D!@PcvoK9G`Hz zIR2w?$*jzcxYTT;Du;uCg*gGwD(je9w5a94Q|rhDqZ0&S9&-|jD&iq;OZVBFX?#90*33leN0Fnc;_wp3eG z_PtCCWyV}`^p%S@BGFV>omFXkQG)dXv%AygcG6<(rxL7J!VNR*Yt8l|V0^?I5DOOQ zWyvu5K)boDRSYVNdw`z`g70S)()t-U@_2sq0$fJQ&m_ah)J;8Dvy0O@9k_(j_?ZM- z2+Hea3YME|1@vX~5-mV0B+@PnHZVImX%@_q5-gXGO(EUeI?MCRRxFl*0QzpU6__)f zG#gfz;<<%pe^gLjJ8^?}OxjWm%^HG$d+h&?IZwSoDjy1RGYCw;F-(i(z@SrV@j)2#|+faSehz z*JTPOZa$9k3h--DkAKItaSn()Rr_DO>ukX9N?lGcgpaqhm-AqLaunh%i&VU1FljJo!hEKk|A(jrPYBL%ubZ14$HqTb@nfd zm}er;ff&qInA>8%vM-NuwdJZ+W1f|b20BjZGnW!?dU#c zMAEmK&UQOZ%>HLafC|yeP>lsZU9zuQJzH?XUMOgJc`!42b z4wR;^B(Qeq^nvo}5+a(bO4G204jnq1^7;Coug_w^m96|O70}65vVU2bAY5E&zRd+- z1Mq?V(4lj7c*wPWZWE>X)krQu(_Ko_sEp=(M$>qu`M!*%&x-=rbbt4jUdwh|OKzYY zZJ?A-A1TeXN@utJ*|q%F^cnelevTT=d*U$6`24jDoL^Fny%z0okUbslBtgbOr~>l5 z9s?GP^O*<%VeLxu59SBQHz8E)K$e`{S^4yZV12K70dHE;Lk##}=}=WZHFT7NZ&wL2 zd~RJ!ehC-%scE8hl}~RP?(qZuu6?@;pLWam{G|Z-96pn}@ew$PL*& z%#d;M-8Dq>m*c)FptDR?ooUFf)9xT6Dv&ZgglvFZ&N_+G^kTt%1M=DxWYrMvWr2K0 zV`KmM%I~uH6hmx%Z~K89@IG>E%eU9&a`U!T=4sf&_Xr=;?=Jl8z7k}l+jhwAm(u*$ zKlgz!Gv$!O{gtL~gmn1UmX@LOwDq1)^Z4gpynqf(Eo9V$Es$>pL4TS&G_`K;FD)nxDXG}eG4s`JEsgX9cqC*BMxMw;57m7 zt~D*`81yZLhav5lBFN$4B4nSY$0@pdnvedyqKA5j3V1&kdnBU&oBED*jkIz~s&IUS z()^uZ$y4J%Rz7t?Z2OOd=6P3zEJGasK5E=4fJ{fpkZB4E@-ec49NJbu2P{M-SE=)+G{0KB!n{EL$8E;?HvkBeh{ z&|6p_k8lCGwlUnoR5e7$9j+~@kh{w6g2u*ztm#wokic4|M_AYs_wl&`mIkFquBNF}FUt$u9h;7tk3NHPH|^mP{K-kdYY*Mp4-X zv5C_3a(q83_w5A#KFCGl00}Rm3Bi(|!S_j|;1G3V!H{E+9izCP{mh z&rjDj<(5ijtn7!E36^nhoU}UQ)43MN{arwY*hYYi{XUgg$bL4v5K>>mrr&_NK6-tD z&bL7B`?E^_KMte>n`5s+LF-Am0$BM4E37lPi?8veVpQ}llpv$V-~uw@}|6@Xh97yx{QE( zx`6EIa0eS?)!b*{bZIgSUk27{h0u~+#pp6LjR)kij$ion0Al_)B?UBlRTHgIG04!t znWBvdQObFS=;A{c*%)+jfGkZ}0dg#D_Mh3~_b3Zg1aw_PepOQ;CW2p|Qur&ND;?Co zcd?CMKm}TwY?!~UNb$cW$ZluS$whECPZ#>;aM#kM0sN&@Yl$;{fx&!9(Tayv5v7sA za)uR!VkqX6l9fTBWE;N#WH+*58d0(N62oCLso4KBO}wIw)Y?6`8o$7y2gsF`4GTRs zQ|J@{^gV?kVR*c|@e5phC&n*a$cE8n5g}J<^Z;)Mh~T4}O!8cWcCr1agBcgEfe*B4 zvn&2C4-|V0%A@ZgGATAUX!=s<54QFA1GA(i>2o!Hp>%kNMbSDlQK}@Q%%*}+ga>q(T(zU=m;$J83RlFhMm6(lzbR1f!{58` zqqx6KHVnv#n+G(#tEia2#ot4dQS6-=2G>-wAB8sCA{(BT$QPm@Q!=Hz2)iN(z2`Dm zX{-yA0kI(K@JO3B8{a3%9hNbsx4UHYpE-O12<+UdWFl0`f`Hx4KC$#WeD+STuk-v1Vfe=Hf|nupvx;15p{{L%}&9p)!~|^jtjae2&vi> zvPDmg!#h=w<+=g?V1kX}zC99gGaHC##G(@!Ek`OTTL5Z_7)BtZE0<&##VJcTI5Pc^ zV`Y8(0tRvrJ-Ct!qacOKq3r>uAF_~$6u>s<-B8nLEpUMQ^}yqoip1T`?fwOUh&T#; zcERPhtJFL2mVOz9RFd_TWGO k0000$kiYeVQ~&?~BajzTKg+YrRR91007*qoM6N<$g0JNd;{X5v literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf deleted file mode 100644 index 3a75f7a4f7ec51e5f22a788a5feb8b40654e5891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16786 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 diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json index 72555bf44c..1bc8a07ac1 100644 --- a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json @@ -2,7 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_voice.pdf" + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernConversationMicButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ModernConversationMicButton@3x.png", + "scale" : "3x" } ], "info" : { diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@2x.png b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b407292b296bddb62c1df61f6675934bf88b66ab GIT binary patch literal 843 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bYNg$WD4*JaRqW`&YamkVP;z&5Y1}s zpV`(w8%VbH&1|1I>;M1%ZT+)=Y@k%zgjpSv=1!fus;hq%P_SppoVNa%Kn1NpDIn^f z*$&p&F%f72$eiA(^V%lO1M2OW2{a9)WzwW=KxIJGIbklC0W^75$AsBHDIjW}wiKus zh&n(9&g|@;-2oJzIJ<4~%+?7YmF-i3HqPpsGpBvh9FUDrOJ{ZT%>g@mRy$BT&>29* zKsQWT*g1DDL`^Hu3b2{26M-6`J_S0U738&92ZT3g0z)CJB*-tAfi-mL&Cfx1{s%I3 zcYUhU=P}qTf0r?=@t9MC+^N&DqOV1M{$u}fUwwViy0{~;g1`T2Z`btn(wP2M<*|_A zm)il+z8}xH-Rg4`=52l`KKX<5#0x7|&A(@{@b7wHcrWsFaSW+oe0!C7n{uE?>qFuH z=X-K**K9NWztAL4dli>h)K9Tf>vnHn^GA@!ZSu_|!`VG-ku$CyE8k`sdFB_Jv)T1j zLFoh6zMXsb?%p@C4LdK`GXG{@B=d%u`_9gX0^i<#np-QM!!~m|^ESRG%bRr#PVMp8 zpzFNtpvq^X!+ktAnKz#~&9vJnMO?>7-y!*EeZ_}Y53U~cn;!i?Vf$%isXY%a%TKD~ zI8oxr;mg}RS3XgoX@gICzd-NHqX9gR7O66N{yelou)))#W17g4&@Z=+E&d+hnlELv zNR`{tbhDYc=7b-Nd@nU*6_${(tFnO#T~HTb9&P^ gJG*ZwsYlWU?&)0VV*hm*m>w8BUHx3vIVCg!0BxO|Jpcdz literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@3x.png b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6e0c6307ad1b92c385f509906aacfbde6d6ba92c GIT binary patch literal 1262 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 Dc4I(^ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png b/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f74122a57e663786971e9661b2a229646bc8214a GIT binary patch literal 1019 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 diff --git a/Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/Contents.json similarity index 71% rename from Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json rename to Images.xcassets/Chat/Input/Text/IconVideo.imageset/Contents.json index a738861b67..0ef8e70c7f 100644 --- a/Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "InstantViewSettingsIcon@2x.png", + "filename" : "RecordVideoIcon@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "InstantViewSettingsIcon@3x.png", + "filename" : "RecordVideoIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@2x.png b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c13b7b513498e4b8bfc9271795c1dd7cbb067982 GIT binary patch literal 651 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2Jl0X`wFK)SVmc3a=9uBr1|`)5v_ zx(di%wQ5yc|12OENKTrxtz*J0pkONyfkoQ;XSGe4)j4@?+oV}Q1who^KfASWCQ!lu z|Nq;7jEOTlC(UjJn*h|(K5c2+1d!g&33GwUK4&EA3N4$8VaZCaZYugJw6)?nz(>)pES zs&9k)hHnLY$^Cx(s`IC|uWUa)q58&wBU}A7{a#M)R!^|WQ7nA(n)OE9dC#JKd5x!6 zHw$l&W4lwTzhVB$^J&#Pc4RZ(t(bM__s7@YKgwRpRIxkoZ-40*IljUjnu*Kyqy?A- zMjQ@f&2@BtD7H>5?|k7BzUE1hc(3TBMmHlb&o+ybKQts_RFyyLf4gq~ YQM%wfXR&_|FcKL&UHx3vIVCg!0HKUFQ~&?~ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@3x.png b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..7e019781f77952be450d1f5534f996a9e951ae8e GIT binary patch literal 1098 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S903?%u>HW~n_CjmYou0XoIe^%R+S#A9@Tl;4I z|NnpL)K%@%mbUc)MP{{5m758P zaCYDHg8Q_Xv3^G$EWCYZ{ksjX z45^5FJ2Nv_$xxuJ+HD4NqJ@osY=D4(rHkH97sv9_^3VVOpAWM-+U~15duBjd_UdOp zXDqvSO6Bg{cWnQdg@lB>jI|@4>32-%{$sQuP4&V7-s{UL0J;lv|o;@nS7cEc048$bVh9s6x&e$DRo zh&%P^b(h#J-{+iPA>%#!Z8cNSI>Vr6mL0x%-5ZtrPb>X$6E?|MG+)H=ko2=&){KYA zc7={U9d=h3bfsQwRqVJKu$XmLL`W60lh=&-*$PL_>fE|l@n);IrbMKUU z$M5R)b`~&j2@a3m)o!lQbKc}$s(<*cqwD1wCNR#Mx?0nADffB>#=q68G<$FEyFR5^nVZ~YXD z7dMh*Zp<^XeW`wReoT<*Z;=`G2foaYoqV)u*OAJr8ihO}Y3$P{+pkbGh-EpyZ&^vW zxlHhUQ<;Auiyt<8eVBXcR!#BosG$D~rd>%;7rT zSZ=Ej)W7vv;jyUbuMylcv-ltL^az~gx0@J$>v5Fn-}1eRn}1%kI3K;E)9Awe7QKvs qUt5}{{>kr{dhNczGfxkXAH1y1`$d)>s$K=mEDWBmelF{r5}E+;G7ZE4 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/ActionIcon.imageset/Contents.json b/Images.xcassets/Instant View/ActionIcon.imageset/Contents.json new file mode 100644 index 0000000000..22c1331282 --- /dev/null +++ b/Images.xcassets/Instant View/ActionIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_share@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_share@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@2x.png b/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..67586711db652fa896dccb056152e8d8ea8970fc GIT binary patch literal 1216 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 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/BackArrow.imageset/InstantPageBackArrow@2x.png b/Images.xcassets/Instant View/BackArrow.imageset/InstantPageBackArrow@2x.png deleted file mode 100644 index a5090c6c8d68b111ab0138a5d9d66248b8b5cd3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^l0dA*!3HGV*gqryDVB6cUq=Rpjs4tz5?O)#m!2+; zArYKgufOIxtiZ#XAX|O(t>Sa%U9KUQwqA?zV_K`(`g!8N6*Gi(WUbBLRq_q&UoDfVKDY6E(j!PC{xWt~$(697Ug Ba6|wA diff --git a/Images.xcassets/Instant View/BackArrow.imageset/Contents.json b/Images.xcassets/Instant View/MoreIcon.imageset/Contents.json similarity index 78% rename from Images.xcassets/Instant View/BackArrow.imageset/Contents.json rename to Images.xcassets/Instant View/MoreIcon.imageset/Contents.json index 2b39b50424..6b7a481581 100644 --- a/Images.xcassets/Instant View/BackArrow.imageset/Contents.json +++ b/Images.xcassets/Instant View/MoreIcon.imageset/Contents.json @@ -6,11 +6,12 @@ }, { "idiom" : "universal", - "filename" : "InstantPageBackArrow@2x.png", + "filename" : "ic_more@2x.png", "scale" : "2x" }, { "idiom" : "universal", + "filename" : "ic_more@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@2x.png b/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a1c317201393c9e6c8e6517b2d23544e3ee701 GIT binary patch literal 453 zcmV;$0XqJPP)Px$e@R3^R5%fhls_*7K^%s6seA;DMxvlVPDOXcl~f8*NED(H-F<*-bUKwz6r%Aj zp>T;pp+KS0C=^0mB0(HNSkG*3JK2YC%*h5U$F)!4U52lW1550VPh;Tc}x1k5KX`=lY-aX12Z vSvTPn{0lp8k{sPx$7)eAyR7efYS3wTKAP_7)=o|FbUo}3^2lx*CMomn7K)v8hLX&nOGT6(KZ0Iht z!)$?+cHOGQ7<vsKaIWE8^F!Q>0uHqz3gaIXe=Ni|AdG-TFb`^9EKZwiMc} wKM~Y!VjOR}+Ad<<^FyG|zpmvCR{B=`Ut|Lsa}dwl{r~^~07*qoM6N<$f{KcrR{#J2 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/PanelCheck.imageset/Contents.json b/Images.xcassets/Instant View/PanelCheck.imageset/Contents.json new file mode 100644 index 0000000000..d4e3338f6e --- /dev/null +++ b/Images.xcassets/Instant View/PanelCheck.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewCheck@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewCheck@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@2x.png b/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..72914fbdf05bc73d79b5da7d2175b98f289eda8c GIT binary patch literal 359 zcmV-t0hs=YP)L@sAIJk^!V$X&6Xw%!YRulF*_KDh9RtDYc#hrBDIBqnNR;DBh(9fZnuQ4?5KqJ6i09xq#W007;}CppLETVDVG002ovPDHLk FV1l^1qN4x+ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@3x.png b/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9224bfcb02814652f2d9898baead8ac86e1f831 GIT binary patch literal 502 zcmVYS>9Fr*i77-r+llTlg1LB4Zq=6G6$KN61L;1$fgm~bzXyBAc@%M=M2$;%u@BkwN zYv7E)_y&2184h8S=04yYxb#04|Qd_=^jz#K8ZWLuz`coJ7k z9P23&p8<2F_?g=PmBhub@wkT_DhC!w`A4%3N{NUYdd?dOABgkvV2K=m^{b$mh`FT| z|BX1l2$sq@Us(y)${pGMm$(iFq$OMjuYFN+0^mjg_ znHgUTn^~H}&w(Tp8U}fi7AzZCsS=07??FOLn2Bde0{8v^K*7iAWdWaj57fJ{tG z$}cUkRRX#c;)UD-xUqS~&|m@vn0`fKfxe-h0mw@*g}%P{mFDKcRTq~8r6Sym)!^cg z%7Rq=pw#00(xPNw#HA^NtSYc_E=o--$uA1Y&(DE{Vn9ZINq%ugeu09sGbq%|6*PPk zlQZ)`f|_7mzP?tTdBr7(dC94sF1AWQbM!JZQ>-i;jh&oLT;0sw%pDC4UCkYx%#AI~ z-HZ%ffjna~1DIZy{N&Qy)Vvay-W0fAHv^n{K?x$a0BEyIYEfocYKmJ?ey#%8<5rot z-Qt4NJgD9joNjS-#i>^x=oo!a^ddz!ObD2UKumbz1#;lYKQ#}S=8J%dds#w2HvubtQM$yF<^;e7_iL^zP^9Ij>iqe>dk>>AB+bwO+?&s3iF&vde~iXiT2-=yi1~ zOV^TC|IfvH6Wy*&RhoOjYMtSY^t0At$t<&uXsZ>ixqF>2jr~0R#SL(U4 z=+@N}2um#Mjrk0jS$kc&FRoMqSO z6g%_F>80s25^PQ8+17mx&|m4#`#^g#Sw|PANz8VbU2$m7H|6H_V+Po~;1FfeOmhD4M^`1)8S=jZArg4F0$^BXQ!4Z zB&DWj=GiK}-@RW+Av48RDcsc8z_-9TH6zobswg$M$}c3jDm&RSMakYy!KT6rXh3di zNuokUZcbjYRfVk**jy_h8zii+qySb@l5ML5aa4qFfP!;=QL2Keo|$g4p|OR6xuu?= zsilRHiH?GifuWhcfu+8oiLQa6m4T&|fuRBvC;@FNN=dT{a&d#&1?1T(Wt5Z@Sn2DR zmzV368|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)VF`a7isrF3Kz@$;{7F0GXJW zlwVq6s|0i@#0$9vaAWg|p}_1U13Be0{Av^NLFn^O93NU2K(r=ICW+rdU}x8ap|gxEY(fnL8R9x|%yWnHyV} zyBQg}0(r(}1~9!Y`N^fZsd*(Zy(tL2COGwi5=3qR&}Ns^qRg_?6t|-MTm`Vltuk@D z#So`?P`xR*-C~4OuRhQ*`k?4Vif)(?Fb#p2@Wcz`z>|M!9x%-p0TZ`wWx*Z>2F7Ea zE{-7;x9-fa)n^G5X*<70w2e)%$t78MZ@*uM&P@-OM%{qS60W5$OqKpIa5*U+aaWqC zYZA>N)EQL5WxCemSoJ1%C*zxQcN%{`v*+aHw{vf*KfhyKe(a-7ztAK@1MW6Q>mMxo z4}yOD`kpNo?8i67_Wz!yL-$K>Ep@-y zc7@HI7AC{G{ZD(()cD2-^@>TEX*%1Y4Y~b%yfX!EPck#H@6_4$NXeor=SY;$<6u|G z9hpLp|9&}eewIeQ(094VT(K^?qT40r>+n6{opDKJ8}GBnyCP$fvYl$)3LgFtF=6Ao zC7wBKb53$LUs9UJetp5gW6egZY|Z%A<#@WPnrTkW+EmG5s%xe@wdiD#PnT|U@r4x+ z&t9E!gYCw4!@qyxz8q>ZZ?VBDi@%S0KMpERUmc#-UHEreD-~Hx$Jb4=NB&g}|^6LJe g89Yl(?3s8NDx}v=60%YL3M%V7UHx3vIVCg!0CcYE^Z)<= literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json new file mode 100644 index 0000000000..0931c327d2 --- /dev/null +++ b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewBrightnessMaxIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewBrightnessMaxIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@2x.png b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..da102fa6e44c31c7a0a728b584d2cab95ba40bfb GIT binary patch literal 513 zcmV+c0{;DpP)MMU-mhgB9L^X{05usoX+efHstwn^oIqV8yQ)^8k444SN#KYzIx;upIH=A`4lK%I( z;y+SFaivSle*=3^x=2g$C!?%Sxkgyiq7rz#e&sV2;y)sX2o)`C2jRjz0eOUH&8bas zgHW<5+B1YkXvPi@?n&6)%pSrisX$Z*yy3BJdpo!2BRo>{iy*-cEd-CfpY)9tTtDSq z9{gr>Zx@xrf-+)(@9i9qRw(Xa;)++@!^-=tH4x00000NkvXXu0mjf DY7ghM literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@3x.png b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ac0611dd1cf68f4fe0b6c2da5a358987159d93b6 GIT binary patch literal 447 zcmV;w0YLtVP)x0004oNklmSgkQr9iSlOx{h|x zx8lqJeLIFN)ad!Yhe`uvY4*Yo=$V}Xa>v;$LjmOoFeIa)rQ=3NOG8FD0xhi-55Sg+ zt4i8%T&dW8VvF0;kKles&wgH9K*?PNKT(hnvnD2?;H0kKjiffI!3*ig-IR-kzMrJ@ zF~bAV6OV|4n(2NX(ew(qdf~NF(0&dfRVt;?gkPl6TiF6bwJcH%-&8G`&<>d?mFn3; zy_p@dK@bW}?*c(+?2s*jkZ5`*2tun@#SF*LIs_rsM8*h0Hx>Ui@`fO+HIZus;bw>Y z$Sgg4%db|!opXRD%MVaRlEv|m8*Y@8csETMRPiq^QbHIa%@_XBNxJT9yRp}Y?3@% zn_Ot>xmCxE12+a<+-YkdaaKk~td5okk;%R-Bvwj$04*QFy_HTc~adPx;bon$s8v>jgCHQ<%qNx_5j_35LRI0oA+< zb@bD{7Sn_~VW)sv!VoNE_Ku@1T$-9d9idx#2cV%Z9Oi0!!dpqxnb;D-xlOZbS~^15 zuLg4xY#RvS)4LGfQqm|V7tSPDa_m_ML*2Boqp}J3e*8bra$Y@i*3!Z0l+VjJM)P)+ sFD|>DAdmXFb2MGqsQhRe|Kb<+1&e-*QCWMCQ2+n{07*qoM6N<$f^{OO@c;k- literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@3x.png b/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0f04c3e7274e250bb4e76deb704f181ef8822566 GIT binary patch literal 356 zcmV-q0h|7bP)9C6vy!|3pdEXkclk|61QOBAUOxuATty9;1n5=*s>xe`E{sEqt*tazAjbG#6@ zsL4plsOfkh#C;!_yUebXUm_(}mth{y^$t6DVh0EIu~^}TSq9>z8*&lHH3-qI^ymrWe73)gzwyp5a|k+BE%fW{U|%*x%>4$YrO7^cZK<$ zH9k?y&O6i7*z~S2y~&L4S>r0exJEIrp#F>5ecT&oC^u+cOj2I}0000<+#NR3?nhUrlQ;^OcI=>=c-rmAv_tCA1Zv*lH zJ~D)n%l8o7Ph{-{cq7BIqKxnib#ehi+}@xlMff1aEf;V~&1WiYgS2N@jqUaP{UMcV zXVqaGutbgTC{(Ey+>*X;WVhml8hvS%DSld&`T@N>YHM1h9A8wK)ei_#(~YpUL$!Nk zY1^xqL6gF2H&55D9pIvdD9VJ@96V8d%ZNb%%@52opNhrhP98CU1&!_Hjxlk7Uj;F} zeJN&8L^>dj=JbGZekd0|e_QQ+icjiW)nd@0;Zt7`^U21eISm7neCk)rYyTLex6Y~e zSi~TY6fZ`3hT4=g$Wo(JK$1_3Dd~iSkTi5R)qF}aXjnR+jU7FSt6Rt*y=B22dG`J? yXdPzObc#<79>;Dfpn~}{103iuNN?>X`1B32cr4tg2OOXP0000w$`AV4wzWF)IdTa zA%dJi@^$C^yLWf@mfr9Cz3;b4llOVAfbMF8kghg3Ut3DSmh1RxyeI`PsCHX0s>BNj zjJhqj@DyCQjBoM83kbBhD_CXdHE#HM)m3~I9vC`>DSm!Hm79V=Mt-0cwfNwE&`rT9 z0}lWI2i%{!h;P6b1A71fJ=}kx!MA%zATbIPJCar=diEmxe_C4RnMBQsI4#P~Y zNWM*2#R~{bgcn>{7hHwKr>)}!1k%9;8#EPS-`)#6yAjN=XPU-i7mV}la}eJ~A{iF8 zWWJ5~TXHn)7(FEH@O;isZkB$g4x@q&DWTabFKaSPk8jIfhLtJK7#GYiqx_$5{Sw3K zj0@@{zV)lpF@+--(RlyT1&3<6ziH!7%gQkOZ>aKK*+SGhGQhF*7SOY~9)4KkI*d`vC>O Vp)@nz@z4MO002ovPDHLkV1hKELd5_8 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json new file mode 100644 index 0000000000..49aa90fa7a --- /dev/null +++ b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewFontMinIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewFontMinIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@2x.png b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eae01e16f577fe046108e2ee8921fef812d7100f GIT binary patch literal 277 zcmV+w0qXvVP)T1B20`tQM1^$f7JP%EF>7Qp^^`XfY{^3zw7@&cp94idge+E=Am_E1!GCjSDeS4%Wqrd9hPA*SinMXVM{D=A|^_~ bpt$-3hYeXtk)yd7pK@e=;$G518h`YXkZi1UQiIbZyE`knqNb{#{nqHu! z*M7(EE_V?Wtw)lsUh$)F0Fh(o0cuS3eoCzx7}5qFJb*UCYCy_Zv2u&b11&}X#KZuM zX!(H&b>PI6D<`1N#1BMieDjQaK$kx7Wdp#5FVLr34uk;gcmeQYXL%h!z|4|jb~6Gu zFrWiG*=x_9C(vQw24s3`e%kY6olo2VO~yGv=50)~2uSCJa|Sqbv4UlQ$PwRh8VXQ} z&Gjs(HMa_40h!V{OqE;%AypRIfIR*;4Fbn%@YZdU0y3p7A7|_jTl1H&S~xD16Pwl- zdPr^@OMxRdHC#dqT6j}lC{ZFE^n|4sSdz#KLZny8fx8#DbC6e%C~b+77Wo5D!%&wy Ski-1|00005ta9+8qMS@r)R>qkV7!=!kHo~NAYM#Vd>l+bd-NGK z@i6~xyLf0sz^FX{TH^u5S0eaGAc%@7th4`2+uiNVQqpPX(4Co`|C?|AZ~kw6zN~&Q z6aEm>YFdpiS3j+eK@bx-!|5SO#X(8 z)$k}KaZLId2k7FsB zkzt&Ru31R07J%9kIa>hE`^Q|5(mSy%_w@;Xp0572|lzu1K-*a(IR zuaGG3HO6R{o%Jd@`4jr+J~mD0h0mn(N7yDpOT{JcDxJJW8Y$E*X<1y&limtf#7%2v zcL**DtC3X}LIGF=<7A|Ly5umL?S|{wL_Vo+q3Cara2tK9m3ZM`#vmqaPv7H1nu`+% zvY)>2M95<=UmhbbJlhDqrMZ@ei^(Xpol@j9gZzeZW1%3aZ7DDGlz9VgChqb{IA|r) ze%+u!>V?N90(m^2`J{H*3G$o{?n(A4DM)V`$a&_4`m7A9LMY8AjWQbamtL-|WR2dK z0jjihF7M_-&)W=h~6iHB!%FYLty#-x0wmM1^p)2d~KnLMIokK^m> zIrUwW`Zg=#KH1`f2z@vpJXNYEc!hAc5#)!+3q21k$b9lYEYSJXbNp!700000NkvXX Hu0mjfP&<#C diff --git a/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png b/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png deleted file mode 100644 index 736367a1086b6965caaa359167b8fa808602db2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1133 zcmV-z1d{uSP)001@!1ONa4CHt&?000CuNkl|qn@fEyAnf&-g4SP2di91s^IT*NxqO&rwg zG2#sBRHQ1_7EQ$)NdkwPIWwO>y7ez7yMbSIcWLew* z+UOVA!`(PwswhVk9HXpOXvP1>jwlmlSu1ea?i}wO@5!CNY^^ZU-2v@0OUtuZ2Gc|> zrjQ5prc&x@rD(l}#T2t_)rlh9o=D#lPb6`py*f=1<%xhpl+cAzyYSheC&CPOFF{*9 zE~WaPE~S!3bo&xB4$>L4%YtNS7f|ku@h4Us+~Dw)61kNq_r?)tw5?iD)Tt{*T;qx% zr&!lw^e)`Np$BD>2hm_vdxeqqKOYiF5wss>{P6GV*QvYJPP%S)~q*y zRY@({;?nU>ai6{;m)NG(D9>8MAtzL&PoTZl4Chg6zUnt1-(XgSezb#2vz-gu^&85g zT$?6Nw;9N)+!ryAYED4Js@xY=56Q*ubA$3Mf;RY}yWBr;48rOmL)iTgyC_?-z@yeO zUGymvSk;!y9H!=pi-Kt#Af=^2E&3hha*ZDQLXzd{xDol@RgPm_(PA&(mk=Z z!<9w0wLGMknsg4!bF|#6b7JL%Ey{b_^3*rETlzV8{+zhAH!zH1=NrL(uz07TlONR% zI#0Kv{ZF={`A9`8*mTB{KiG9c(9F?*Q~IOAs@xQ<9+AvugxYUP!yjw(cvN!_i&&Mb zQD|U)3s>?r=KTh)>~B?zT0`1ln^`5izkqBNPB zFojeZlnXp+O$<5hlqj$&luHNL*Oj%eu!Qqi`!lT2jTA(kx} z?ormKuw;G0e(puA`kXh=3pfLE2Baex9%W@!(&4w&8#t#Myij!$7t)!WTi+=2DCr41 z@a>VYDpc`lR!uY06fIPVIw}g5M_E~w6!AlXG$X7it1B?k57XF<{Y4T5cJ!}lA5|s! z92cJK=~^$@(|zX}+%G5jd6Zn^iB CGFloat { return defaultPortraitPanelHeight } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 7f9fe92f15..7ce8d514bd 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -10,6 +10,8 @@ import SafariServices public class ChatController: TelegramController { private var containerLayout = ContainerViewLayout() + public let v = 1 + private let account: Account public let peerId: PeerId private let messageId: MessageId? @@ -66,6 +68,11 @@ public class ChatController: TelegramController { private var audioRecorder = Promise() private var audioRecorderDisposable: Disposable? + private var videoRecorderValue: InstantVideoController? + private var tempVideoRecorderValue: InstantVideoController? + private var videoRecorder = Promise() + private var videoRecorderDisposable: Disposable? + private var buttonKeyboardMessageDisposable: Disposable? private var cachedDataDisposable: Disposable? private var chatUnreadCountDisposable: Disposable? @@ -742,11 +749,11 @@ public class ChatController: TelegramController { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputTextPanelState { panelState in if let audioRecorder = audioRecorder { - if panelState.audioRecordingState == nil { - return panelState.withUpdatedAudioRecordingState(ChatTextInputPanelAudioRecordingState(recorder: audioRecorder)) + if panelState.mediaRecordingState == nil { + return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorder, isLocked: false)) } } else { - return panelState.withUpdatedAudioRecordingState(nil) + return panelState.withUpdatedMediaRecordingState(nil) } return panelState } @@ -759,6 +766,45 @@ public class ChatController: TelegramController { } }) + self.videoRecorderDisposable = (self.videoRecorder.get() |> deliverOnMainQueue).start(next: { [weak self] videoRecorder in + if let strongSelf = self { + if strongSelf.videoRecorderValue !== videoRecorder { + let previousVideoRecorderValue = strongSelf.videoRecorderValue + strongSelf.videoRecorderValue = videoRecorder + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + if let videoRecorder = videoRecorder { + if panelState.mediaRecordingState == nil { + return panelState.withUpdatedMediaRecordingState(.video(status: .recording(videoRecorder.audioStatus), isLocked: false)) + } + } else { + return panelState.withUpdatedMediaRecordingState(nil) + } + return panelState + } + }) + + if let videoRecorder = videoRecorder { + videoRecorder.onDismiss = { + if let strongSelf = self { + strongSelf.videoRecorder.set(.single(nil)) + } + } + strongSelf.present(videoRecorder, in: .window(.root)) + } + + if let previousVideoRecorderValue = previousVideoRecorderValue { + previousVideoRecorderValue.dismissVideo() + } + + /*if let videoRecorder = videoRecorder { + videoRecorder.start() + }*/ + } + } + }) + if let botStart = botStart, case .automatic = botStart.behavior { self.startBot(botStart.payload) } @@ -817,6 +863,7 @@ public class ChatController: TelegramController { self.contextQueryState?.1.dispose() self.urlPreviewQueryState?.1.dispose() self.audioRecorderDisposable?.dispose() + self.videoRecorderDisposable?.dispose() self.buttonKeyboardMessageDisposable?.dispose() self.cachedDataDisposable?.dispose() self.resolveUrlDisposable?.dispose() @@ -898,6 +945,14 @@ public class ChatController: TelegramController { } }) }) + if let readStateData = combinedInitialData.readStateData { + let globalRemainingUnreadCount = readStateData.totalUnreadCount - readStateData.unreadCount + if globalRemainingUnreadCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + } else { + strongSelf.navigationItem.badge = "" + } + } } } } @@ -1064,14 +1119,14 @@ public class ChatController: TelegramController { insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil)) } - let scrollToItem = ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Spring(duration: 0.4), directionHint: .Up) + let scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up) var stationaryItemRange: (Int, Int)? if let maxInsertedItem = maxInsertedItem { stationaryItemRange = (maxInsertedItem + 1, Int.max) } - mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, scrolledToIndex: transition.scrolledToIndex), updateSizeAndInsets) + mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex), updateSizeAndInsets) }) if let mappedTransition = mappedTransition { @@ -1193,10 +1248,17 @@ public class ChatController: TelegramController { self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { - let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId) - strongSelf.navigationActionDisposable.set((signal |> take(1) |> deliverOnMainQueue).start(next: { messageId in - if let strongSelf = self, let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: messageId) + let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: strongSelf.peerId) + strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { result in + if let strongSelf = self { + switch result { + case let .result(messageId): + if let messageId = messageId { + strongSelf.navigateToMessage(from: nil, to: messageId) + } + case .loading: + break + } } })) } @@ -1522,10 +1584,33 @@ public class ChatController: TelegramController { if let strongSelf = self { strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: strongSelf.peerId))), fromMessageId: nil) } - }, beginAudioRecording: { [weak self] in - self?.requestAudioRecorder() - }, finishAudioRecording: { [weak self] sendAudio in - self?.dismissAudioRecorder(sendAudio: sendAudio) + }, beginMediaRecording: { [weak self] isVideo in + if let strongSelf = self { + if isVideo { + strongSelf.requestVideoRecorder() + } else { + strongSelf.requestAudioRecorder() + } + } + }, finishMediaRecording: { [weak self] sendMedia in + self?.dismissMediaRecorder(sendMedia: sendMedia) + }, stopMediaRecording: { [weak self] in + self?.stopMediaRecorder() + }, lockMediaRecording: { [weak self] in + self?.lockMediaRecorder() + }, switchMediaRecordingMode: { [weak self] in + self?.updateChatPresentationInterfaceState(interactive: true, { + return $0.updatedInterfaceState { current in + let mode: ChatTextInputMediaRecordingButtonMode + switch current.mediaRecordingMode { + case .audio: + mode = .video + case .video: + mode = .audio + } + return current.withUpdatedMediaRecordingMode(mode) + } + }) }, setupMessageAutoremoveTimeout: { [weak self] in if let strongSelf = self, strongSelf.peerId.namespace == Namespaces.Peer.SecretChat { strongSelf.chatDisplayNode.dismissInput() @@ -1645,16 +1730,29 @@ public class ChatController: TelegramController { } |> switchToLatest).start() } } + }, presentController: { [weak self] controller in + self?.present(controller, in: .window(.root)) }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) - self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in + self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId), .total]) |> deliverOnMainQueue).start(next: { [weak self] items in if let strongSelf = self { var unreadCount: Int32 = 0 if let count = items.count(for: .peer(strongSelf.peerId)) { unreadCount = count } + var totalCount: Int32 = 0 + if let count = items.count(for: .total) { + totalCount = count + } strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount + + let globalRemainingUnreadCount = totalCount - unreadCount + if globalRemainingUnreadCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + } else { + strongSelf.navigationItem.badge = "" + } } }) @@ -1737,7 +1835,8 @@ public class ChatController: TelegramController { self.chatDisplayNode.historyNode.canReadHistory.set(.single(false)) let timestamp = Int32(Date().timeIntervalSince1970) - let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp) + let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() + let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp).withUpdatedHistoryScrollState(scrollState) let _ = updatePeerChatInterfaceState(account: account, peerId: self.peerId, state: interfaceState).start() } @@ -2044,10 +2143,18 @@ public class ChatController: TelegramController { } } - private func dismissAudioRecorder(sendAudio: Bool) { + private func requestVideoRecorder() { + if self.videoRecorderValue == nil { + if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() { + self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: currentInputPanelFrame, account: self.account, peerId: self.peerId))) + } + } + } + + private func dismissMediaRecorder(sendMedia: Bool) { if let audioRecorderValue = self.audioRecorderValue { audioRecorderValue.stop() - if sendAudio { + if sendMedia { let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self, let data = data { if data.duration < 0.5 { @@ -2074,8 +2181,45 @@ public class ChatController: TelegramController { } }) } + self.audioRecorder.set(.single(nil)) + } else if let videoRecorderValue = self.videoRecorderValue { + if sendMedia { + videoRecorderValue.completeVideo() + self.tempVideoRecorderValue = videoRecorderValue + self.videoRecorder.set(.single(nil)) + } else { + self.videoRecorder.set(.single(nil)) + } } - self.audioRecorder.set(.single(nil)) + } + + private func stopMediaRecorder() { + if let audioRecorderValue = self.audioRecorderValue { + audioRecorderValue.stop() + self.audioRecorder.set(.single(nil)) + } else if let videoRecorderValue = self.videoRecorderValue { + if videoRecorderValue.stopVideo() { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false)) + } + }) + } else { + self.videoRecorder.set(.single(nil)) + } + } + } + + private func lockMediaRecorder() { + if self.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(panelState.mediaRecordingState?.withLocked(true)) + } + }) + } + + self.videoRecorderValue?.lockVideo() } private func navigateToMessage(from fromId: MessageId?, to toId: MessageId, rememberInStack: Bool = true) { diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 1235008417..b466e7552b 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -139,7 +139,9 @@ class ChatControllerNode: ASDisplayNode { self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - self.textInputPanelNode = ChatTextInputPanelNode() + self.textInputPanelNode = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak self] controller in + self?.interfaceInteraction?.presentController(controller) + }) self.textInputPanelNode?.updateHeight = { [weak self] in if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight { strongSelf.requestLayout(.animated(duration: 0.1, curve: .easeInOut)) @@ -724,4 +726,8 @@ class ChatControllerNode: ASDisplayNode { let _ = inputNode.updateLayout(width: self.bounds.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } } + + func currentInputPanelFrame() -> CGRect? { + return self.inputPanelNode?.frame + } } diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index 88e64adddd..a6d12630ab 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -54,11 +54,11 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI if let scrollToItem = transition.scrollToItem { let mappedPosition: GridNodeScrollToItemPosition switch scrollToItem.position { - case .Top: + case .top: mappedPosition = .top - case .Center: + case .center: mappedPosition = .center - case .Bottom: + case .bottom: mappedPosition = .bottom } let scrollTransition: ContainedViewLayoutTransition @@ -236,7 +236,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, theme: themeAndStrings.0, strings: themeAndStrings.1)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -303,15 +303,15 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } public func scrollToStartOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .Bottom, animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .bottom(0.0), animated: true)) } public func scrollToEndOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .Top, animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true)) } public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .center(.bottom), animated: true)) } public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index e5859838d7..848c43a201 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -11,8 +11,9 @@ public enum ChatHistoryListMode { } enum ChatHistoryViewScrollPosition { - case Unread(index: MessageIndex) - case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) + case unread(index: MessageIndex) + case positionRestoration(index: MessageIndex, relativeOffset: CGFloat) + case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } enum ChatHistoryViewUpdateType { @@ -20,10 +21,16 @@ enum ChatHistoryViewUpdateType { case Generic(type: ViewUpdateType) } +public struct ChatHistoryCombinedInitialReadStateData { + public let unreadCount: Int32 + public let totalUnreadCount: Int32 +} + public struct ChatHistoryCombinedInitialData { let initialData: InitialMessageHistoryData? let buttonKeyboardMessage: Message? let cachedData: CachedPeerData? + let readStateData: ChatHistoryCombinedInitialReadStateData? } enum ChatHistoryViewUpdate { @@ -68,6 +75,7 @@ struct ChatHistoryViewTransition { let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? + let readStateData: ChatHistoryCombinedInitialReadStateData? let scrolledToIndex: MessageIndex? } @@ -82,6 +90,7 @@ struct ChatHistoryListViewTransition { let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? + let readStateData: ChatHistoryCombinedInitialReadStateData? let scrolledToIndex: MessageIndex? } @@ -163,7 +172,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, scrolledToIndex: transition.scrolledToIndex) + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex) } private final class ChatHistoryTransactionOpaqueState { @@ -282,6 +291,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var additionalData: [AdditionalMessageHistoryViewData] = [] additionalData.append(.cachedPeerData(peerId)) + additionalData.append(.totalUnreadCount) let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged @@ -302,7 +312,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let initialData: ChatHistoryCombinedInitialData? switch update { case let .Loading(data): - let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil, cachedData: nil) + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil, cachedData: nil, readStateData: nil) initialData = combinedInitialData Queue.mainQueue().async { [weak self] in if let strongSelf = self { @@ -345,7 +355,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles, includeChatInfoEntry: true, theme: themeAndStrings.0, strings: themeAndStrings.1)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -406,19 +416,25 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var messageIdsWithUnseenPersonalMention: [MessageId] = [] for i in (indexRange.0 ... indexRange.1) { if case let .MessageEntry(message, _, _, _, _) = historyView.filteredEntries[i] { + var hasUnconsumedMention = false + var hasUnsonsumedContent = false if message.tags.contains(.unseenPersonalMessage) { for attribute in message.attributes { if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending { - messageIdsWithUnseenPersonalMention.append(message.id) + hasUnconsumedMention = true } } } - inner: for attribute in message.attributes { + for attribute in message.attributes { if attribute is ViewCountMessageAttribute { messageIdsWithViewCount.append(message.id) - break inner + } else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { + hasUnsonsumedContent = true } } + if hasUnconsumedMention && !hasUnsonsumedContent { + messageIdsWithUnseenPersonalMention.append(message.id) + } } } @@ -504,15 +520,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } public func scrollToStartOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .Bottom, animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .bottom(0.0), animated: true)) } public func scrollToEndOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .Top, animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true)) } public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .center(.bottom), animated: true)) } public func anchorMessageInCurrentHistoryView() -> Message? { @@ -558,7 +574,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData))) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData))) } strongSelf.enqueuedHistoryViewTransition = (transition, { @@ -573,10 +589,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if strongSelf.isNodeLoaded { strongSelf.dequeueHistoryViewTransition() } else { - /*if !strongSelf.didSetInitialData { - strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData))) - }*/ strongSelf._cachedPeerData.set(.single(transition.cachedData)) let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { @@ -612,7 +624,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData))) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData))) } strongSelf._cachedPeerData.set(.single(transition.cachedData)) let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) @@ -675,4 +687,37 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } + + func immediateScrollState() -> ChatInterfaceHistoryScrollState? { + var currentMessage: Message? + if let historyView = self.historyView { + if let visibleRange = self.displayedItemRange.visibleRange { + var index = 0 + loop: for entry in historyView.filteredEntries.reversed() { + if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { + if case let .MessageEntry(message, _, _, _, _) = entry { + if index != 0 || historyView.originalView.laterId != nil { + currentMessage = message + } + break loop + } + } + index += 1 + } + } + } + + if let message = currentMessage { + var relativeOffset: CGFloat = 0.0 + self.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == message.id { + if let offsetValue = self.itemNodeRelativeOffset(itemNode) { + relativeOffset = offsetValue + } + } + } + return ChatInterfaceHistoryScrollState(messageIndex: MessageIndex(message), relativeOffset: Double(relativeOffset)) + } + return nil + } } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 32faf70ac5..9638320208 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -13,24 +13,34 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun if let tagMask = tagMask { signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics) } else { - signal = account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + signal = account.viewTracker.aroundMessageOfInterestHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var readStateData: ChatHistoryCombinedInitialReadStateData? for data in view.additionalData { - if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { - cachedData = value - break + switch data { + case let .cachedPeerData(peerIdValue, value): + if peerIdValue == peerId { + cachedData = value + } + case let .totalUnreadCount(totalUnreadCount): + if let readState = view.combinedReadState { + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + } + default: + break } } + if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) } else { var scrollPosition: ChatHistoryViewScrollPosition? if let maxReadIndex = view.maxReadIndex, tagMask == nil { let aroundIndex = maxReadIndex - scrollPosition = .Unread(index: maxReadIndex) + scrollPosition = .unread(index: maxReadIndex) var targetIndex = 0 for i in 0 ..< view.entries.count { @@ -64,6 +74,8 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } } } + } else if let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState { + scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset)) } else { var messageCount = 0 for entry in view.entries.reversed() { @@ -80,7 +92,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) } } case let .InitialSearch(searchLocation, count): @@ -97,14 +109,24 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var readStateData: ChatHistoryCombinedInitialReadStateData? for data in view.additionalData { - if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { - cachedData = value - break + switch data { + case let .cachedPeerData(peerIdValue, value): + if peerIdValue == peerId { + cachedData = value + } + case let .totalUnreadCount(totalUnreadCount): + if let readState = view.combinedReadState { + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + } + default: + break } } + if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) } else { let anchorIndex = view.anchorIndex @@ -127,17 +149,26 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) } } case let .Navigation(index, anchorIndex): var first = true return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var readStateData: ChatHistoryCombinedInitialReadStateData? for data in view.additionalData { - if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { - cachedData = value - break + switch data { + case let .cachedPeerData(peerIdValue, value): + if peerIdValue == peerId { + cachedData = value + } + case let .totalUnreadCount(totalUnreadCount): + if let readState = view.combinedReadState { + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + } + default: + break } } @@ -148,18 +179,27 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) } case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up - let chatScrollPosition = ChatHistoryViewScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) + let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var readStateData: ChatHistoryCombinedInitialReadStateData? for data in view.additionalData { - if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { - cachedData = value - break + switch data { + case let .cachedPeerData(peerIdValue, value): + if peerIdValue == peerId { + cachedData = value + } + case let .totalUnreadCount(totalUnreadCount): + if let readState = view.combinedReadState { + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + } + default: + break } } @@ -171,7 +211,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) } } } diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index bd20737127..3009d3511e 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -167,10 +167,10 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } switch chatPresentationInterfaceState.inputMode { case .media, .inputButtons: - return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) + return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) case .none, .text: if let _ = chatPresentationInterfaceState.interfaceState.editMessage { - return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) + return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } else { if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty { var accessoryItems: [ChatTextInputAccessoryItem] = [] @@ -181,9 +181,9 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup { accessoryItems.append(.inputButtons) } - return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) + return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } else { - return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) + return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } } } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index eee12a0546..b3eb013e5a 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import TelegramCore -struct ChatInterfaceSelectionState: Coding, Equatable { +struct ChatInterfaceSelectionState: PostboxCoding, Equatable { let selectedIds: Set static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool { @@ -13,7 +13,7 @@ struct ChatInterfaceSelectionState: Coding, Equatable { self.selectedIds = selectedIds } - init(decoder: Decoder) { + init(decoder: PostboxDecoder) { if let data = decoder.decodeBytesForKeyNoCopy("i") { self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data)) } else { @@ -21,14 +21,14 @@ struct ChatInterfaceSelectionState: Coding, Equatable { } } - func encode(_ encoder: Encoder) { + func encode(_ encoder: PostboxEncoder) { let buffer = WriteBuffer() MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer) encoder.encodeBytes(buffer, forKey: "i") } } -public struct ChatTextInputState: Coding, Equatable { +public struct ChatTextInputState: PostboxCoding, Equatable { let inputText: String let selectionRange: Range @@ -52,19 +52,19 @@ public struct ChatTextInputState: Coding, Equatable { self.selectionRange = length ..< length } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.inputText = decoder.decodeStringForKey("t", orElse: "") self.selectionRange = Int(decoder.decodeInt32ForKey("s0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("s1", orElse: 0)) } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.inputText, forKey: "t") encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "s0") encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "s1") } } -struct ChatEditMessageState: Coding, Equatable { +struct ChatEditMessageState: PostboxCoding, Equatable { let messageId: MessageId let inputState: ChatTextInputState @@ -73,7 +73,7 @@ struct ChatEditMessageState: Coding, Equatable { self.inputState = inputState } - init(decoder: Decoder) { + init(decoder: PostboxDecoder) { self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp", orElse: 0)), namespace: decoder.decodeInt32ForKey("mn", orElse: 0), id: decoder.decodeInt32ForKey("mi", orElse: 0)) if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { self.inputState = inputState @@ -82,7 +82,7 @@ struct ChatEditMessageState: Coding, Equatable { } } - func encode(_ encoder: Encoder) { + func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.messageId.peerId.toInt64(), forKey: "mp") encoder.encodeInt32(self.messageId.namespace, forKey: "mn") encoder.encodeInt32(self.messageId.id, forKey: "mi") @@ -107,12 +107,12 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { self.text = text } - init(decoder: Decoder) { + init(decoder: PostboxDecoder) { self.timestamp = decoder.decodeInt32ForKey("d", orElse: 0) self.text = decoder.decodeStringForKey("t", orElse: "") } - func encode(_ encoder: Encoder) { + func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.timestamp, forKey: "d") encoder.encodeString(self.text, forKey: "t") } @@ -126,7 +126,7 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { } } -struct ChatInterfaceMessageActionsState: Coding, Equatable { +struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable { let closedButtonKeyboardMessageId: MessageId? let processedSetupReplyMessageId: MessageId? let closedPinnedMessageId: MessageId? @@ -147,7 +147,7 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable { self.closedPinnedMessageId = closedPinnedMessageId } - init(decoder: Decoder) { + init(decoder: PostboxDecoder) { if let closedMessageIdPeerId = decoder.decodeOptionalInt64ForKey("cb.p"), let closedMessageIdNamespace = decoder.decodeOptionalInt32ForKey("cb.n"), let closedMessageIdId = decoder.decodeOptionalInt32ForKey("cb.i") { self.closedButtonKeyboardMessageId = MessageId(peerId: PeerId(closedMessageIdPeerId), namespace: closedMessageIdNamespace, id: closedMessageIdId) } else { @@ -167,7 +167,7 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable { } } - func encode(_ encoder: Encoder) { + func encode(_ encoder: PostboxEncoder) { if let closedButtonKeyboardMessageId = self.closedButtonKeyboardMessageId { encoder.encodeInt64(closedButtonKeyboardMessageId.peerId.toInt64(), forKey: "cb.p") encoder.encodeInt32(closedButtonKeyboardMessageId.namespace, forKey: "cb.n") @@ -216,6 +216,39 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable { } } +struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable { + let messageIndex: MessageIndex + let relativeOffset: Double + + init(messageIndex: MessageIndex, relativeOffset: Double) { + self.messageIndex = messageIndex + self.relativeOffset = relativeOffset + } + + init(decoder: PostboxDecoder) { + self.messageIndex = MessageIndex(id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("m.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("m.n", orElse: 0), id: decoder.decodeInt32ForKey("m.i", orElse: 0)), timestamp: decoder.decodeInt32ForKey("m.t", orElse: 0)) + self.relativeOffset = decoder.decodeDoubleForKey("ro", orElse: 0.0) + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.messageIndex.timestamp, forKey: "m.t") + encoder.encodeInt64(self.messageIndex.id.peerId.toInt64(), forKey: "m.p") + encoder.encodeInt32(self.messageIndex.id.namespace, forKey: "m.n") + encoder.encodeInt32(self.messageIndex.id.id, forKey: "m.i") + encoder.encodeDouble(self.relativeOffset, forKey: "ro") + } + + static func ==(lhs: ChatInterfaceHistoryScrollState, rhs: ChatInterfaceHistoryScrollState) -> Bool { + if lhs.messageIndex != rhs.messageIndex { + return false + } + if !lhs.relativeOffset.isEqual(to: rhs.relativeOffset) { + return false + } + return true + } +} + final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { let timestamp: Int32 let composeInputState: ChatTextInputState @@ -225,6 +258,8 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { let editMessage: ChatEditMessageState? let selectionState: ChatInterfaceSelectionState? let messageActionsState: ChatInterfaceMessageActionsState + let historyScrollState: ChatInterfaceHistoryScrollState? + let mediaRecordingMode: ChatTextInputMediaRecordingButtonMode var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? { if !self.composeInputState.inputText.isEmpty && self.timestamp != 0 { @@ -242,6 +277,10 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } } + var historyScrollMessageIndex: MessageIndex? { + return self.historyScrollState?.messageIndex + } + func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> SynchronizeableChatInterfaceState { return self.withUpdatedComposeInputState(ChatTextInputState(inputText: state?.text ?? "")).withUpdatedReplyMessageId(state?.replyToMessageId) } @@ -263,9 +302,11 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { self.editMessage = nil self.selectionState = nil self.messageActionsState = ChatInterfaceMessageActionsState() + self.historyScrollState = nil + self.mediaRecordingMode = .audio } - init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState) { + init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) { self.timestamp = timestamp self.composeInputState = composeInputState self.composeDisableUrlPreview = composeDisableUrlPreview @@ -274,9 +315,11 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { self.editMessage = editMessage self.selectionState = selectionState self.messageActionsState = messageActionsState + self.historyScrollState = historyScrollState + self.mediaRecordingMode = mediaRecordingMode } - init(decoder: Decoder) { + init(decoder: PostboxDecoder) { self.timestamp = decoder.decodeInt32ForKey("ts", orElse: 0) if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { self.composeInputState = inputState @@ -317,9 +360,13 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } else { self.messageActionsState = ChatInterfaceMessageActionsState() } + + self.historyScrollState = decoder.decodeObjectForKey("hss", decoder: { ChatInterfaceHistoryScrollState(decoder: $0) }) as? ChatInterfaceHistoryScrollState + + self.mediaRecordingMode = ChatTextInputMediaRecordingButtonMode(rawValue: decoder.decodeInt32ForKey("mrm", orElse: 0))! } - func encode(_ encoder: Encoder) { + func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.timestamp, forKey: "ts") encoder.encodeObject(self.composeInputState, forKey: "is") if let composeDisableUrlPreview = self.composeDisableUrlPreview { @@ -358,6 +405,12 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } else { encoder.encodeObject(self.messageActionsState, forKey: "as") } + if let historyScrollState = self.historyScrollState { + encoder.encodeObject(historyScrollState, forKey: "hss") + } else { + encoder.encodeNil(forKey: "hss") + } + encoder.encodeInt32(self.mediaRecordingMode.rawValue, forKey: "mrm") } func isEqual(to: PeerChatInterfaceState) -> Bool { @@ -382,17 +435,23 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { if lhs.messageActionsState != rhs.messageActionsState { return false } + if lhs.historyScrollState != rhs.historyScrollState { + return false + } + if lhs.mediaRecordingMode != rhs.mediaRecordingMode { + return false + } return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage } func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { let updatedComposeInputState = inputState - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { @@ -404,15 +463,15 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { updatedComposeInputState = inputState } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -421,7 +480,7 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -434,22 +493,30 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } else { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) } func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState)) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode) + } + + func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode) + } + + func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode) } } diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index 627186d809..a9135aa5e3 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -150,7 +150,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState textInputPanelNode.account = account return textInputPanelNode } else { - let panel = ChatTextInputPanelNode() + let panel = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak interfaceInteraction] controller in + interfaceInteraction?.presentController(controller) + }) panel.interfaceInteraction = interfaceInteraction panel.account = account return panel diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 56f845659b..2915324202 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -547,9 +547,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - if let combinedReadState = combinedReadState { - let unreadCount = combinedReadState.count - if unreadCount != 0 { + if let unreadCount = combinedReadState?.count, unreadCount > 0 { + if let message = message, message.tags.contains(.unseenPersonalMessage), unreadCount == 1 { + } else { let badgeTextColor: UIColor if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { if case .unmuted = notificationSettings.muteState { diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 1acc03ec5a..a0764a252c 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -382,10 +382,10 @@ final class ChatListNode: ListView { func scrollToLatest() { if let view = self.chatListView?.originalView, view.laterIndex == nil { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { let location: ChatListNodeLocation = .scroll(index: ChatListIndex.absoluteUpperBound, sourceIndex: ChatListIndex.absoluteLowerBound - , scrollPosition: .Top, animated: true) + , scrollPosition: .top(0.0), animated: true) self.currentLocation = location self.chatListLocation.set(location) } diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 15beb4890f..0ce882870e 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -510,7 +510,7 @@ final class ChatMediaInputNode: ChatInputNode { let firstVisibleIndex = currentView.collectionInfos.index(where: { id, _, _ in return id == firstVisibleCollectionId }) if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex { let toRight = targetIndex > firstVisibleIndex - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .Bottom : .Top, animated: true, curve: .Default, directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default, directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) } } } diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 31efabb756..36cec2582f 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -9,6 +9,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { var hostedVideoNode: InstantVideoNode? var tapRecognizer: UITapGestureRecognizer? + private var statusNode: RadialStatusNode? + private var videoFrame: CGRect? + private var selectionNode: ChatMessageSelectionNode? private var appliedItem: ChatMessageItem? @@ -93,6 +96,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.theme) } + let theme = item.theme + let isSecretMedia = item.message.containsSecretMedia + let incoming = item.message.effectivelyIncoming let imageSize = displaySize @@ -111,7 +117,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { var updatedPlaybackStatus: Signal? if let updatedFile = updatedFile, updatedMedia { - updatedPlaybackStatus = fileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message) + updatedPlaybackStatus = combineLatest(fileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) + |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in + if let pendingStatus = pendingStatus { + return .fetchStatus(.Fetching(progress: pendingStatus.progress)) + } else { + return resourceStatus + } + } } let avatarInset: CGFloat @@ -207,6 +220,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { strongSelf.appliedItem = item + strongSelf.videoFrame = videoFrame if let updatedMuteIconImage = updatedMuteIconImage { strongSelf.muteIconNode.image = updatedMuteIconImage @@ -220,7 +234,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if let updatedPlaybackStatus = updatedPlaybackStatus { strongSelf.playbackStatusDisposable.set((updatedPlaybackStatus |> deliverOnMainQueue).start(next: { status in - if let strongSelf = self { + if let strongSelf = self, let videoFrame = strongSelf.videoFrame { let displayMute: Bool switch status { case let .fetchStatus(fetchStatus): @@ -244,6 +258,68 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { strongSelf.muteIconNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) } } + + var progressRequired = false + if case let .fetchStatus(fetchStatus) = status { + if case .Local = fetchStatus { + if let file = updatedFile, file.isVideo { + progressRequired = true + } else if isSecretMedia { + progressRequired = true + } + } else { + progressRequired = true + } + } + + if progressRequired { + if strongSelf.statusNode == nil { + let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) + statusNode.frame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floor((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floor((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) + strongSelf.statusNode = statusNode + strongSelf.addSubnode(statusNode) + } else if let _ = updatedTheme { + + //strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) + } + } else { + if let statusNode = strongSelf.statusNode { + statusNode.transitionToState(.none, completion: { [weak statusNode] in + statusNode?.removeFromSupernode() + }) + strongSelf.statusNode = nil + } + } + + var state: RadialStatusNodeState + let bubbleTheme = theme.chat.bubble + switch status { + case let .fetchStatus(fetchStatus): + switch fetchStatus { + case let .Fetching(progress): + state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(progress), cancelEnabled: true) + case .Local: + state = .none + /*if isSecretMedia && secretProgressIcon != nil { + state = .customIcon(secretProgressIcon!) + } else */ + case .Remote: + state = .download(bubbleTheme.mediaOverlayControlForegroundColor) + } + default: + state = .none + break + } + if let statusNode = strongSelf.statusNode { + if state == .none { + strongSelf.statusNode = nil + } + statusNode.transitionToState(state, completion: { [weak statusNode] in + if state == .none { + statusNode?.removeFromSupernode() + } + }) + } } })) } diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index b31fb7de38..732f281604 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -2,6 +2,7 @@ import Foundation import Postbox import SwiftSignalKit import TelegramCore +import Display final class ChatPanelInterfaceInteractionStatuses { let editingMessage: Signal @@ -45,8 +46,11 @@ final class ChatPanelInterfaceInteraction { let sendBotCommand: (Peer, String) -> Void let sendBotStart: (String?) -> Void let botSwitchChatWithPayload: (PeerId, String) -> Void - let beginAudioRecording: () -> Void - let finishAudioRecording: (Bool) -> Void + let beginMediaRecording: (Bool) -> Void + let finishMediaRecording: (Bool) -> Void + let stopMediaRecording: () -> Void + let lockMediaRecording: () -> Void + let switchMediaRecordingMode: () -> Void let setupMessageAutoremoveTimeout: () -> Void let sendSticker: (TelegramMediaFile) -> Void let unblockPeer: () -> Void @@ -57,9 +61,10 @@ final class ChatPanelInterfaceInteraction { let deleteChat: () -> Void let beginCall: () -> Void let toggleMessageStickerStarred: (MessageId) -> Void + let presentController: (ViewController) -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> 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, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> 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, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> 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 (Bool) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @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) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection @@ -80,8 +85,11 @@ final class ChatPanelInterfaceInteraction { self.sendBotCommand = sendBotCommand self.sendBotStart = sendBotStart self.botSwitchChatWithPayload = botSwitchChatWithPayload - self.beginAudioRecording = beginAudioRecording - self.finishAudioRecording = finishAudioRecording + self.beginMediaRecording = beginMediaRecording + self.finishMediaRecording = finishMediaRecording + self.stopMediaRecording = stopMediaRecording + self.lockMediaRecording = lockMediaRecording + self.switchMediaRecordingMode = switchMediaRecordingMode self.setupMessageAutoremoveTimeout = setupMessageAutoremoveTimeout self.sendSticker = sendSticker self.unblockPeer = unblockPeer @@ -92,6 +100,7 @@ final class ChatPanelInterfaceInteraction { self.deleteChat = deleteChat self.beginCall = beginCall self.toggleMessageStickerStarred = toggleMessageStickerStarred + self.presentController = presentController self.statuses = statuses } } diff --git a/TelegramUI/ChatTextInputAudioRecordingButton.swift b/TelegramUI/ChatTextInputAudioRecordingButton.swift deleted file mode 100644 index c67f4e8205..0000000000 --- a/TelegramUI/ChatTextInputAudioRecordingButton.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import TelegramCore -import SwiftSignalKit - -private let offsetThreshold: CGFloat = 10.0 -private let dismissOffsetThreshold: CGFloat = 70.0 - -final class ChatTextInputAudioRecordingButton: UIButton { - var account: Account? - var beginRecording: () -> Void = { } - var endRecording: (Bool) -> Void = { _ in } - var offsetRecordingControls: () -> Void = { } - - private var recordingOverlay: ChatTextInputAudioRecordingOverlay? - private var startTouchLocation: CGPoint? - private(set) var controlsOffset: CGFloat = 0.0 - - private var micLevelDisposable: MetaDisposable? - - var audioRecorder: ManagedAudioRecorder? { - didSet { - if self.audioRecorder !== oldValue { - if self.micLevelDisposable == nil { - micLevelDisposable = MetaDisposable() - } - if let audioRecorder = self.audioRecorder { - self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in - Queue.mainQueue().async { - self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level)) - } - })) - } else { - self.micLevelDisposable?.set(nil) - } - } - } - } - - init() { - super.init(frame: CGRect()) - - self.isExclusiveTouch = true - self.adjustsImageWhenHighlighted = false - self.adjustsImageWhenDisabled = false - self.disablesInteractiveTransitionGestureRecognizer = true - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateTheme(theme: PresentationTheme) { - self.setImage(PresentationResourcesChat.chatInputPanelVoiceButtonImage(theme), for: []) - } - - deinit { - if let micLevelDisposable = self.micLevelDisposable { - micLevelDisposable.dispose() - } - if let recordingOverlay = self.recordingOverlay { - recordingOverlay.dismiss() - } - } - - func cancelRecording() { - self.isEnabled = false - self.isEnabled = true - } - - override func beginTracking(_ touch: UITouch, with touchEvent: UIEvent?) -> Bool { - if super.beginTracking(touch, with: touchEvent) { - self.startTouchLocation = touch.location(in: self) - - self.controlsOffset = 0.0 - self.beginRecording() - let recordingOverlay: ChatTextInputAudioRecordingOverlay - if let currentRecordingOverlay = self.recordingOverlay { - recordingOverlay = currentRecordingOverlay - } else { - recordingOverlay = ChatTextInputAudioRecordingOverlay(anchorView: self) - self.recordingOverlay = recordingOverlay - } - if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.applicationBindings.getTopWindow() { - recordingOverlay.present(in: topWindow) - } - return true - } else { - return false - } - } - - override func endTracking(_ touch: UITouch?, with touchEvent: UIEvent?) { - super.endTracking(touch, with: touchEvent) - - self.endRecording(self.controlsOffset < 40.0) - self.dismissRecordingOverlay() - } - - override func cancelTracking(with event: UIEvent?) { - super.cancelTracking(with: event) - - self.endRecording(false) - self.dismissRecordingOverlay() - } - - override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - if super.continueTracking(touch, with: event) { - if let startTouchLocation = self.startTouchLocation { - let horiontalOffset = startTouchLocation.x - touch.location(in: self).x - let controlsOffset = max(0.0, horiontalOffset - offsetThreshold) - if !controlsOffset.isEqual(to: self.controlsOffset) { - self.recordingOverlay?.dismissFactor = 1.0 - controlsOffset / dismissOffsetThreshold - self.controlsOffset = controlsOffset - self.offsetRecordingControls() - } - } - return true - } else { - return false - } - } - - private func dismissRecordingOverlay() { - if let recordingOverlay = self.recordingOverlay { - self.recordingOverlay = nil - recordingOverlay.dismiss() - } - } -} diff --git a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift index 1e05311d40..51bcce420c 100644 --- a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift @@ -2,11 +2,20 @@ import Foundation import AsyncDisplayKit import Display +private let cancelFont = Font.regular(17.0) + final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { + private let cancel: () -> Void + private let arrowNode: ASImageNode private let labelNode: TextNode + private let cancelButton: HighlightableButtonNode - init(theme: PresentationTheme, strings: PresentationStrings) { + private var isDisplayingCancel = false + + init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + self.cancel = cancel + self.arrowNode = ASImageNode() self.arrowNode.isLayerBacked = true self.arrowNode.displayWithoutProcessing = true @@ -16,10 +25,15 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.labelNode = TextNode() self.labelNode.isLayerBacked = true + self.cancelButton = HighlightableButtonNode() + self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: []) + self.cancelButton.alpha = 0.0 + super.init() self.addSubnode(self.arrowNode) self.addSubnode(self.labelNode) + self.addSubnode(self.cancelButton) let makeLayout = TextNode.asyncLayout(self.labelNode) let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) @@ -30,9 +44,52 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height)) self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize) self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: floor((height - labelLayout.size.height) / 2.0) - UIScreenPixel), size: labelLayout.size) + + let cancelSize = self.cancelButton.measure(CGSize(width: 200.0, height: 100.0)) + self.cancelButton.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - cancelSize.width) / 2.0), y: floor((height - cancelSize.height) / 2.0)), size: cancelSize) + + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) } func updateTheme(theme: PresentationTheme) { self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme) } + + func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) { + if self.isDisplayingCancel != isDisplayingCancel { + self.isDisplayingCancel = isDisplayingCancel + if isDisplayingCancel { + self.arrowNode.alpha = 0.0 + self.labelNode.alpha = 0.0 + self.cancelButton.alpha = 1.0 + + if animated { + self.arrowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } else { + self.arrowNode.alpha = 1.0 + self.labelNode.alpha = 1.0 + self.cancelButton.alpha = 0.0 + + if animated { + self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.cancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.cancelButton.alpha.isZero, self.cancelButton.frame.insetBy(dx: -5.0, dy: -5.0).contains(point) { + return self.cancelButton.view + } + return super.hitTest(point, with: event) + } + + @objc func cancelPressed() { + self.cancel() + } } diff --git a/TelegramUI/ChatTextInputMediaRecordingButton.swift b/TelegramUI/ChatTextInputMediaRecordingButton.swift new file mode 100644 index 0000000000..840f5f34fd --- /dev/null +++ b/TelegramUI/ChatTextInputMediaRecordingButton.swift @@ -0,0 +1,386 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit + +import LegacyComponents + +private let offsetThreshold: CGFloat = 10.0 +private let dismissOffsetThreshold: CGFloat = 70.0 + +enum ChatTextInputMediaRecordingButtonMode: Int32 { + case audio = 0 + case video = 1 +} + +private final class ChatTextInputMediaRecordingButtonPresenterContainer: UIView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in self.subviews { + if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) { + return result + } + } + return super.hitTest(point, with: event) + } +} + +private final class ChatTextInputMediaRecordingButtonPresenterController: ViewController { + override func loadDisplayNode() { + self.displayNode = ChatTextInputMediaRecordingButtonPresenterControllerNode() + } +} + +private final class ChatTextInputMediaRecordingButtonPresenterControllerNode: ViewControllerTracingNode { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} + +private final class ChatTextInputMediaRecordingButtonPresenter : NSObject, TGModernConversationInputMicButtonPresentation { + private let account: Account? + private let presentController: (ViewController) -> Void + private let container: ChatTextInputMediaRecordingButtonPresenterContainer + private var presentationController: ViewController? + + init(account: Account, presentController: @escaping (ViewController) -> Void) { + self.account = account + self.presentController = presentController + self.container = ChatTextInputMediaRecordingButtonPresenterContainer() + } + + deinit { + self.container.removeFromSuperview() + if let presentationController = self.presentationController { + presentationController.presentingViewController?.dismiss(animated: false, completion: {}) + self.presentationController = nil + } + } + + func view() -> UIView! { + return self.container + } + + func setUserInteractionEnabled(_ enabled: Bool) { + self.container.isUserInteractionEnabled = enabled + } + + func present() { + if let keyboardWindow = LegacyComponentsGlobals.provider().applicationKeyboardWindow(), !keyboardWindow.isHidden { + keyboardWindow.addSubview(self.container) + } else { + var presentNow = false + if self.presentationController == nil { + let presentationController = ChatTextInputMediaRecordingButtonPresenterController(navigationBarTheme: nil) + presentationController.statusBar.statusBarStyle = .Ignore + self.presentationController = presentationController + presentNow = true + } + + self.presentationController?.displayNode.view.addSubview(self.container) + if let presentationController = self.presentationController, presentNow { + self.presentController(presentationController) + } + } + } + + func dismiss() { + self.container.removeFromSuperview() + if let presentationController = self.presentationController { + presentationController.presentingViewController?.dismiss(animated: false, completion: {}) + self.presentationController = nil + } + } +} + +final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButton, TGModernConversationInputMicButtonDelegate { + private var theme: PresentationTheme + + var mode: ChatTextInputMediaRecordingButtonMode = .audio + var account: Account? + let presentController: (ViewController) -> Void + var beginRecording: () -> Void = { } + var endRecording: (Bool) -> Void = { _ in } + var stopRecording: () -> Void = { _ in } + var offsetRecordingControls: () -> Void = { } + var switchMode: () -> Void = { } + var updateLocked: (Bool) -> Void = { _ in } + + private var modeTimeoutTimer: SwiftSignalKit.Timer? + + private let innerIconView: UIImageView + + private var recordingOverlay: ChatTextInputAudioRecordingOverlay? + private var startTouchLocation: CGPoint? + private(set) var controlsOffset: CGFloat = 0.0 + + private var micLevelDisposable: MetaDisposable? + + var audioRecorder: ManagedAudioRecorder? { + didSet { + if self.audioRecorder !== oldValue { + if self.micLevelDisposable == nil { + micLevelDisposable = MetaDisposable() + } + if let audioRecorder = self.audioRecorder { + self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in + Queue.mainQueue().async { + //self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level)) + self?.addMicLevel(CGFloat(level)) + } + })) + } else if self.videoRecordingStatus == nil { + self.micLevelDisposable?.set(nil) + } + + self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil + } + } + } + + var videoRecordingStatus: InstantVideoControllerRecordingStatus? { + didSet { + if self.videoRecordingStatus !== oldValue { + if self.micLevelDisposable == nil { + micLevelDisposable = MetaDisposable() + } + + if let videoRecordingStatus = self.videoRecordingStatus { + self.micLevelDisposable?.set(videoRecordingStatus.micLevel.start(next: { [weak self] level in + Queue.mainQueue().async { + //self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level)) + self?.addMicLevel(CGFloat(level)) + } + })) + } else if self.audioRecorder == nil { + self.micLevelDisposable?.set(nil) + } + + self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil + } + } + } + + private var hasRecorder: Bool = false { + didSet { + if self.hasRecorder != oldValue { + if self.hasRecorder { + self.animateIn() + } else { + self.animateOut() + } + } + } + } + + init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) { + self.theme = theme + self.innerIconView = UIImageView() + self.presentController = presentController + + super.init(frame: CGRect()) + + self.insertSubview(self.innerIconView, at: 0) + + self.isExclusiveTouch = true + self.adjustsImageWhenHighlighted = false + self.adjustsImageWhenDisabled = false + self.disablesInteractiveTransitionGestureRecognizer = true + + self.updateMode(mode: self.mode, animated: false, force: true) + + self.delegate = self + + self.centerOffset = CGPoint(x: 0.0, y: -1.0 + UIScreenPixel) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool) { + self.updateMode(mode: mode, animated: animated, force: false) + } + + private func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool, force: Bool) { + if mode != self.mode || force { + self.mode = mode + + if animated { + let previousView = UIImageView(image: self.innerIconView.image) + previousView.frame = self.innerIconView.frame + self.addSubview(previousView) + previousView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + previousView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false, completion: { [weak previousView] _ in + previousView?.removeFromSuperview() + }) + } + + switch self.mode { + case .audio: + self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme) + self.innerIconView.image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme) + case .video: + self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme) + self.innerIconView.image = PresentationResourcesChat.chatInputPanelVideoButtonImage(self.theme) + } + if let image = self.innerIconView.image { + let size = self.bounds.size + let iconSize = image.size + self.innerIconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + } + + if animated { + self.innerIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false) + self.innerIconView.layer.animateSpring(from: 0.4 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } + } + + func updateTheme(theme: PresentationTheme) { + self.theme = theme + + switch self.mode { + case .audio: + self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme) + self.innerIconView.image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme) + case .video: + self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme) + self.innerIconView.image = PresentationResourcesChat.chatInputPanelVideoButtonImage(self.theme) + } + } + + deinit { + if let micLevelDisposable = self.micLevelDisposable { + micLevelDisposable.dispose() + } + if let recordingOverlay = self.recordingOverlay { + recordingOverlay.dismiss() + } + } + + func cancelRecording() { + self.isEnabled = false + self.isEnabled = true + } + + /*override func beginTracking(_ touch: UITouch, with touchEvent: UIEvent?) -> Bool { + if super.beginTracking(touch, with: touchEvent) { + self.startTouchLocation = touch.location(in: self) + + self.controlsOffset = 0.0 + self.beginRecording() + let recordingOverlay: ChatTextInputAudioRecordingOverlay + if let currentRecordingOverlay = self.recordingOverlay { + recordingOverlay = currentRecordingOverlay + } else { + recordingOverlay = ChatTextInputAudioRecordingOverlay(anchorView: self) + self.recordingOverlay = recordingOverlay + } + if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.applicationBindings.getTopWindow() { + recordingOverlay.present(in: topWindow) + } + return true + } else { + return false + } + } + + override func endTracking(_ touch: UITouch?, with touchEvent: UIEvent?) { + super.endTracking(touch, with: touchEvent) + + self.endRecording(self.controlsOffset < 40.0) + self.dismissRecordingOverlay() + } + + override func cancelTracking(with event: UIEvent?) { + super.cancelTracking(with: event) + + self.endRecording(false) + self.dismissRecordingOverlay() + } + + override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + if super.continueTracking(touch, with: event) { + if let startTouchLocation = self.startTouchLocation { + let horiontalOffset = startTouchLocation.x - touch.location(in: self).x + let controlsOffset = max(0.0, horiontalOffset - offsetThreshold) + if !controlsOffset.isEqual(to: self.controlsOffset) { + self.recordingOverlay?.dismissFactor = 1.0 - controlsOffset / dismissOffsetThreshold + self.controlsOffset = controlsOffset + self.offsetRecordingControls() + } + } + return true + } else { + return false + } + } + + private func dismissRecordingOverlay() { + if let recordingOverlay = self.recordingOverlay { + self.recordingOverlay = nil + recordingOverlay.dismiss() + } + }*/ + + func micButtonInteractionBegan() { + self.modeTimeoutTimer?.invalidate() + let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.modeTimeoutTimer = nil + strongSelf.beginRecording() + } + }, queue: Queue.mainQueue()) + self.modeTimeoutTimer = modeTimeoutTimer + modeTimeoutTimer.start() + } + + func micButtonInteractionCancelled(_ velocity: CGPoint) { + self.modeTimeoutTimer?.invalidate() + self.endRecording(false) + } + + func micButtonInteractionCompleted(_ velocity: CGPoint) { + if let modeTimeoutTimer = self.modeTimeoutTimer { + modeTimeoutTimer.invalidate() + self.modeTimeoutTimer = nil + self.switchMode() + } + self.endRecording(true) + } + + func micButtonInteractionUpdate(_ offset: CGPoint) { + self.controlsOffset = offset.x + self.offsetRecordingControls() + } + + func micButtonInteractionLocked() { + self.updateLocked(true) + } + + func micButtonInteractionRequestedLockedAction() { + } + + func micButtonInteractionStopped() { + self.stopRecording() + } + + func micButtonShouldLock() -> Bool { + return true + } + + func micButtonPresenter() -> TGModernConversationInputMicButtonPresentation! { + return ChatTextInputMediaRecordingButtonPresenter(account: self.account!, presentController: self.presentController) + } + + private var previousSize = CGSize() + func layoutItems() { + let size = self.bounds.size + if size != self.previousSize { + self.previousSize = size + let iconSize = self.innerIconView.bounds.size + self.innerIconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + } + } +} diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 731867f93c..5ba85cf1cb 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -53,33 +53,83 @@ enum ChatTextInputAccessoryItem: Equatable { } } -struct ChatTextInputPanelAudioRecordingState: Equatable { - let recorder: ManagedAudioRecorder +enum ChatVideoRecordingStatus: Equatable { + case recording(InstantVideoControllerRecordingStatus) + case editing - init(recorder: ManagedAudioRecorder) { - self.recorder = recorder + static func ==(lhs: ChatVideoRecordingStatus, rhs: ChatVideoRecordingStatus) -> Bool { + switch lhs { + case let .recording(lhsStatus): + if case let .recording(rhsStatus) = rhs, lhsStatus === rhsStatus { + return true + } else { + return false + } + case .editing: + if case .editing = rhs { + return true + } else { + return false + } + } + } +} + +enum ChatTextInputPanelMediaRecordingState: Equatable { + case audio(recorder: ManagedAudioRecorder, isLocked: Bool) + case video(status: ChatVideoRecordingStatus, isLocked: Bool) + + var isLocked: Bool { + switch self { + case let .audio(_, isLocked): + return isLocked + case let .video(_, isLocked): + return isLocked + } } - static func ==(lhs: ChatTextInputPanelAudioRecordingState, rhs: ChatTextInputPanelAudioRecordingState) -> Bool { - return lhs.recorder === rhs.recorder + func withLocked(_ isLocked: Bool) -> ChatTextInputPanelMediaRecordingState { + switch self { + case let .audio(recorder, _): + return .audio(recorder: recorder, isLocked: isLocked) + case let .video(status, _): + return .video(status: status, isLocked: isLocked) + } + } + + static func ==(lhs: ChatTextInputPanelMediaRecordingState, rhs: ChatTextInputPanelMediaRecordingState) -> Bool { + switch lhs { + case let .audio(lhsRecorder, lhsIsLocked): + if case let .audio(rhsRecorder, rhsIsLocked) = rhs, lhsRecorder === rhsRecorder, lhsIsLocked == rhsIsLocked { + return true + } else { + return false + } + case let .video(status, isLocked): + if case .video(status, isLocked) = rhs { + return true + } else { + return false + } + } } } struct ChatTextInputPanelState: Equatable { let accessoryItems: [ChatTextInputAccessoryItem] let contextPlaceholder: NSAttributedString? - let audioRecordingState: ChatTextInputPanelAudioRecordingState? + let mediaRecordingState: ChatTextInputPanelMediaRecordingState? - init(accessoryItems: [ChatTextInputAccessoryItem], contextPlaceholder: NSAttributedString?, audioRecordingState: ChatTextInputPanelAudioRecordingState?) { + init(accessoryItems: [ChatTextInputAccessoryItem], contextPlaceholder: NSAttributedString?, mediaRecordingState: ChatTextInputPanelMediaRecordingState?) { self.accessoryItems = accessoryItems self.contextPlaceholder = contextPlaceholder - self.audioRecordingState = audioRecordingState + self.mediaRecordingState = mediaRecordingState } init() { self.accessoryItems = [] self.contextPlaceholder = nil - self.audioRecordingState = nil + self.mediaRecordingState = nil } static func ==(lhs: ChatTextInputPanelState, rhs: ChatTextInputPanelState) -> Bool { @@ -91,14 +141,14 @@ struct ChatTextInputPanelState: Equatable { } else if (lhs.contextPlaceholder != nil) != (rhs.contextPlaceholder != nil) { return false } - if lhs.audioRecordingState != rhs.audioRecordingState { + if lhs.mediaRecordingState != rhs.mediaRecordingState { return false } return true } - func withUpdatedAudioRecordingState(_ audioRecordingState: ChatTextInputPanelAudioRecordingState?) -> ChatTextInputPanelState { - return ChatTextInputPanelState(accessoryItems: self.accessoryItems, contextPlaceholder: self.contextPlaceholder, audioRecordingState: audioRecordingState) + func withUpdatedMediaRecordingState(_ mediaRecordingState: ChatTextInputPanelMediaRecordingState?) -> ChatTextInputPanelState { + return ChatTextInputPanelState(accessoryItems: self.accessoryItems, contextPlaceholder: self.contextPlaceholder, mediaRecordingState: mediaRecordingState) } } @@ -171,7 +221,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textInputNode: ASEditableTextNode? let textInputBackgroundView: UIImageView - let micButton: ChatTextInputAudioRecordingButton + let micButton: ChatTextInputMediaRecordingButton let sendButton: HighlightableButton let attachmentButton: HighlightableButton let searchLayoutClearButton: HighlightableButton @@ -265,7 +315,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let accessoryButtonSpacing: CGFloat = 0.0 let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel - override init() { + init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) { self.textInputBackgroundView = UIImageView() self.textPlaceholderNode = TextNode() self.textPlaceholderNode.isLayerBacked = true @@ -273,7 +323,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.searchLayoutClearButton = HighlightableButton() self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage) self.searchLayoutProgressView.isHidden = true - self.micButton = ChatTextInputAudioRecordingButton() + self.micButton = ChatTextInputMediaRecordingButton(theme: theme, presentController: presentController) self.sendButton = HighlightableButton() super.init() @@ -282,13 +332,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.view.addSubview(self.attachmentButton) self.micButton.beginRecording = { [weak self] in - if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.beginAudioRecording() + if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction { + let isVideo: Bool + switch presentationInterfaceState.interfaceState.mediaRecordingMode { + case .audio: + isVideo = false + case .video: + isVideo = true + } + interfaceInteraction.beginMediaRecording(isVideo) } } - self.micButton.endRecording = { [weak self] sendAudio in + self.micButton.endRecording = { [weak self] sendMedia in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.finishAudioRecording(sendAudio) + interfaceInteraction.finishMediaRecording(sendMedia) } } self.micButton.offsetRecordingControls = { [weak self] in @@ -296,6 +353,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let _ = strongSelf.updateLayout(width: strongSelf.bounds.size.width, transition: .immediate, interfaceState: presentationInterfaceState) } } + self.micButton.stopRecording = { [weak self] in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.stopMediaRecording() + } + } + self.micButton.updateLocked = { [weak self] _ in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.lockMediaRecording() + } + } + self.micButton.switchMode = { [weak self] in + if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + interfaceInteraction.switchMediaRecordingMode() + } + } self.view.addSubview(self.micButton) self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside) @@ -522,18 +594,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: width) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) + self.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated) + + var hideMicButton = false var audioRecordingItemsVerticalOffset: CGFloat = 0.0 - if let audioRecordingState = interfaceState.inputTextPanelState.audioRecordingState { - self.micButton.audioRecorder = audioRecordingState.recorder - let audioRecordingInfoContainerNode: ASDisplayNode - if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { - audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode - } else { - audioRecordingInfoContainerNode = ASDisplayNode() - self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode - self.insertSubnode(audioRecordingInfoContainerNode, at: 0) - } - + if let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState { audioRecordingItemsVerticalOffset = panelHeight * 2.0 transition.updateAlpha(layer: self.textInputBackgroundView.layer, alpha: 0.0) if let textInputNode = self.textInputNode { @@ -543,81 +608,107 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { transition.updateAlpha(layer: button.layer, alpha: 0.0) } - var animateCancelSlideIn = false - let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator - if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator { - audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator - } else { - animateCancelSlideIn = transition.isAnimated - - audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings) - self.audioRecordingCancelIndicator = audioRecordingCancelIndicator - self.insertSubnode(audioRecordingCancelIndicator, at: 0) - } - - audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: floor((width - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) - - if animateCancelSlideIn { - let position = audioRecordingCancelIndicator.layer.position - audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: width + audioRecordingCancelIndicator.bounds.size.width, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - } - - var animateTimeSlideIn = false - let audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode - if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode { - audioRecordingTimeNode = currentAudioRecordingTimeNode - } else { - audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode(theme: interfaceState.theme) - self.audioRecordingTimeNode = audioRecordingTimeNode - audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode) - - if transition.isAnimated { - animateTimeSlideIn = true - } - } - - let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0)) - - audioRecordingInfoContainerNode.frame = CGRect(origin: CGPoint(x: min(0.0, audioRecordingCancelIndicator.frame.minX - audioRecordingTimeSize.width - 8.0 - 28.0), y: 0.0), size: CGSize(width: width, height: panelHeight)) - - audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 28.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize) - if animateTimeSlideIn { - let position = audioRecordingTimeNode.layer.position - audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 28.0 - audioRecordingTimeSize.width, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - } - - audioRecordingTimeNode.audioRecorder = audioRecordingState.recorder - - var animateDotSlideIn = false - let audioRecordingDotNode: ASImageNode - if let currentAudioRecordingDotNode = self.audioRecordingDotNode { - audioRecordingDotNode = currentAudioRecordingDotNode - } else { - animateDotSlideIn = transition.isAnimated - - audioRecordingDotNode = ASImageNode() - audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme) - self.audioRecordingDotNode = audioRecordingDotNode - audioRecordingInfoContainerNode.addSubnode(audioRecordingDotNode) - } - audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: audioRecordingTimeNode.frame.minX - 17.0, y: panelHeight - minimalHeight + floor((minimalHeight - 9.0) / 2.0)), size: CGSize(width: 9.0, height: 9.0)) - if animateDotSlideIn { - let position = audioRecordingDotNode.layer.position - audioRecordingDotNode.layer.animatePosition(from: CGPoint(x: position.x - 9.0 - 51.0, y: position.y), to: position, duration: 0.7, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak audioRecordingDotNode] finished in - if finished { - let animation = CAKeyframeAnimation(keyPath: "opacity") - animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber] - animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] - animation.duration = 0.5 - animation.autoreverses = true - animation.repeatCount = Float.infinity - - audioRecordingDotNode?.layer.add(animation, forKey: "recording") + switch mediaRecordingState { + case let .audio(recorder, isLocked): + self.micButton.audioRecorder = recorder + let audioRecordingInfoContainerNode: ASDisplayNode + if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { + audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode + } else { + audioRecordingInfoContainerNode = ASDisplayNode() + self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode + self.insertSubnode(audioRecordingInfoContainerNode, at: 0) + } + + var animateCancelSlideIn = false + let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator + if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator { + audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator + } else { + animateCancelSlideIn = transition.isAnimated + + audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in + self?.interfaceInteraction?.finishMediaRecording(false) + }) + self.audioRecordingCancelIndicator = audioRecordingCancelIndicator + self.insertSubnode(audioRecordingCancelIndicator, at: 0) + } + + audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: floor((width - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) + + if animateCancelSlideIn { + let position = audioRecordingCancelIndicator.layer.position + audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: width + audioRecordingCancelIndicator.bounds.size.width, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + + audioRecordingCancelIndicator.updateIsDisplayingCancel(isLocked, animated: !animateCancelSlideIn) + + var animateTimeSlideIn = false + let audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode + if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode { + audioRecordingTimeNode = currentAudioRecordingTimeNode + } else { + audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode(theme: interfaceState.theme) + self.audioRecordingTimeNode = audioRecordingTimeNode + audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode) + + if transition.isAnimated { + animateTimeSlideIn = true + } + } + + let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0)) + + audioRecordingInfoContainerNode.frame = CGRect(origin: CGPoint(x: min(0.0, audioRecordingCancelIndicator.frame.minX - audioRecordingTimeSize.width - 8.0 - 28.0), y: 0.0), size: CGSize(width: width, height: panelHeight)) + + audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 28.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize) + if animateTimeSlideIn { + let position = audioRecordingTimeNode.layer.position + audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 28.0 - audioRecordingTimeSize.width, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + audioRecordingTimeNode.audioRecorder = recorder + + var animateDotSlideIn = false + let audioRecordingDotNode: ASImageNode + if let currentAudioRecordingDotNode = self.audioRecordingDotNode { + audioRecordingDotNode = currentAudioRecordingDotNode + } else { + animateDotSlideIn = transition.isAnimated + + audioRecordingDotNode = ASImageNode() + audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme) + self.audioRecordingDotNode = audioRecordingDotNode + audioRecordingInfoContainerNode.addSubnode(audioRecordingDotNode) + } + audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: audioRecordingTimeNode.frame.minX - 17.0, y: panelHeight - minimalHeight + floor((minimalHeight - 9.0) / 2.0)), size: CGSize(width: 9.0, height: 9.0)) + if animateDotSlideIn { + let position = audioRecordingDotNode.layer.position + audioRecordingDotNode.layer.animatePosition(from: CGPoint(x: position.x - 9.0 - 51.0, y: position.y), to: position, duration: 0.7, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak audioRecordingDotNode] finished in + if finished { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber] + animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] + animation.duration = 0.5 + animation.autoreverses = true + animation.repeatCount = Float.infinity + + audioRecordingDotNode?.layer.add(animation, forKey: "recording") + } + }) + } + case let .video(status, _): + switch status { + case let .recording(recordingStatus): + self.micButton.videoRecordingStatus = recordingStatus + case .editing: + self.micButton.videoRecordingStatus = nil + hideMicButton = true } - }) } } else { self.micButton.audioRecorder = nil + self.micButton.videoRecordingStatus = nil transition.updateAlpha(layer: self.textInputBackgroundView.layer, alpha: 1.0) if let textInputNode = self.textInputNode { transition.updateAlpha(node: textInputNode, alpha: 1.0) @@ -662,6 +753,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { composeButtonsOffset = 44.0 textInputBackgroundWidthOffset = 36.0 } + + self.micButton.layoutItems() transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) @@ -731,6 +824,24 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { + hideMicButton = true + } + + if self.extendedSearchLayout { + hideMicButton = true + } + + if hideMicButton { + if !self.micButton.alpha.isZero { + transition.updateAlpha(layer: self.micButton.layer, alpha: 0.0) + } + } else { + if self.micButton.alpha.isZero { + transition.updateAlpha(layer: self.micButton.layer, alpha: 1.0) + } + } + return panelHeight } @@ -744,12 +855,25 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private func updateTextNodeText(animated: Bool) { var hasText = false + var hideMicButton = false if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { hasText = true + hideMicButton = true } self.textPlaceholderNode.isHidden = hasText + if let presentationInterfaceState = self.presentationInterfaceState { + if let mediaRecordingState = presentationInterfaceState.inputTextPanelState.mediaRecordingState { + if case .video(.editing, false) = mediaRecordingState { + hideMicButton = true + } + } + } + + var animateWithBounce = false if self.extendedSearchLayout { + hideMicButton = true + if !self.sendButton.alpha.isZero { self.sendButton.alpha = 0.0 if animated { @@ -757,13 +881,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.sendButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2) } } - if !self.micButton.alpha.isZero { - self.micButton.alpha = 0.0 - if animated { - self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - self.micButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2) - } - } if self.searchLayoutClearButton.alpha.isZero { self.searchLayoutClearButton.alpha = 1.0 if animated { @@ -772,7 +889,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } else { - var animateWithBounce = true + animateWithBounce = true if !self.searchLayoutClearButton.alpha.isZero { animateWithBounce = false self.searchLayoutClearButton.alpha = 0.0 @@ -783,6 +900,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if hasText || self.keepSendButtonEnabled { + hideMicButton = true if self.sendButton.alpha.isZero { self.sendButton.alpha = 1.0 if animated { @@ -794,24 +912,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } - if !self.micButton.alpha.isZero { - self.micButton.alpha = 0.0 - if animated { - self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - } - } } else { - if self.micButton.alpha.isZero { - self.micButton.alpha = 1.0 - if animated { - self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - if animateWithBounce { - self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) - } else { - self.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) - } - } - } if !self.sendButton.alpha.isZero { self.sendButton.alpha = 0.0 if animated { @@ -821,6 +922,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + if hideMicButton { + if !self.micButton.alpha.isZero { + self.micButton.alpha = 0.0 + if animated { + self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } else { + if self.micButton.alpha.isZero { + self.micButton.alpha = 1.0 + if animated { + self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + if animateWithBounce { + self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + } else { + self.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) + } + } + } + } + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: self.bounds.size.width) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) if !self.bounds.size.height.isEqual(to: panelHeight) { @@ -916,4 +1038,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator { + if let result = audioRecordingCancelIndicator.hitTest(point.offsetBy(dx: -audioRecordingCancelIndicator.frame.minX, dy: -audioRecordingCancelIndicator.frame.minY), with: event) { + return result + } + } + return super.hitTest(point, with: event) + } } diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index aada66e8b0..50b56c4653 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -632,6 +632,6 @@ final class ContactListNode: ASDisplayNode { } func scrollToTop() { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index a32b5b1501..736ac0cc58 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -24,7 +24,9 @@ private let rootNavigationBar = PresentationThemeRootNavigationBar( controlColor: UIColor(rgb: 0x5e5e5e), accentTextColor: accentColor, backgroundColor: UIColor(rgb: 0x121212), - separatorColor: UIColor(rgb: 0x1a1a1a) + separatorColor: UIColor(rgb: 0x1a1a1a), + badgeBackgroundColor: UIColor(rgb: 0xff3600), + badgeTextColor: .white ) private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar( diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 733956a613..68a01a7105 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -15,7 +15,8 @@ private let rootTabBar = PresentationThemeRootTabBar( textColor: UIColor(rgb: 0x929292), selectedTextColor: accentColor, badgeBackgroundColor: UIColor(rgb: 0xff3b30), - badgeTextColor: .white) + badgeTextColor: .white +) private let rootNavigationBar = PresentationThemeRootNavigationBar( buttonColor: accentColor, @@ -24,7 +25,9 @@ private let rootNavigationBar = PresentationThemeRootNavigationBar( controlColor: UIColor(rgb: 0x7e8791), accentTextColor: accentColor, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), - separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), + badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeTextColor: .white ) private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar( diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index 194d92c41e..1080198af6 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -134,20 +134,16 @@ private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource if resourceData.complete { let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov" - _ = try? FileManager.default.removeItem(atPath: tempFilePath) - _ = try? FileManager.default.linkItem(atPath: resourceData.path, toPath: tempFilePath) - - var fullSizeImage: CGImage? - - let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) - imageGenerator.appliesPreferredTrackTransform = true - if let image = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) { - fullSizeImage = image - } - - if let fullSizeImage = fullSizeImage { + do { + let _ = try? FileManager.default.removeItem(atPath: tempFilePath) + try FileManager.default.linkItem(atPath: resourceData.path, toPath: tempFilePath) + + let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) + imageGenerator.appliesPreferredTrackTransform = true + let fullSizeImage = try imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) + var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let path = NSTemporaryDirectory() + "\(randomId)" @@ -170,6 +166,8 @@ private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) subscriber.putCompletion() + } catch (let e) { + print("\(e)") } } return EmptyDisposable diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index 2340535ccf..4c66f92d12 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -35,6 +35,11 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = UIColor.black self.scrollView = UIScrollView() + + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + self.pager = GalleryPagerNode(pageGap: pageGap) self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction) diff --git a/TelegramUI/GalleryPagerNode.swift b/TelegramUI/GalleryPagerNode.swift index f9795e44de..f42ef22299 100644 --- a/TelegramUI/GalleryPagerNode.swift +++ b/TelegramUI/GalleryPagerNode.swift @@ -31,6 +31,9 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { init(pageGap: CGFloat) { self.pageGap = pageGap self.scrollView = UIScrollView() + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } super.init() @@ -145,7 +148,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } private func updateItemNodes() { - if self.items.isEmpty { + if self.items.isEmpty || self.containerLayout == nil { return } diff --git a/TelegramUI/GeneratedMediaStoreSettings.swift b/TelegramUI/GeneratedMediaStoreSettings.swift index 8f3b65b74a..2ca6918bfa 100644 --- a/TelegramUI/GeneratedMediaStoreSettings.swift +++ b/TelegramUI/GeneratedMediaStoreSettings.swift @@ -13,11 +13,11 @@ public struct GeneratedMediaStoreSettings: PreferencesEntry, Equatable { self.storeEditedPhotos = storeEditedPhotos } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.storeEditedPhotos = decoder.decodeInt32ForKey("eph", orElse: 0) != 0 } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.storeEditedPhotos ? 1 : 0, forKey: "eph") } diff --git a/TelegramUI/InAppNotificationSettings.swift b/TelegramUI/InAppNotificationSettings.swift index 5aa146e72b..623853ff83 100644 --- a/TelegramUI/InAppNotificationSettings.swift +++ b/TelegramUI/InAppNotificationSettings.swift @@ -17,13 +17,13 @@ struct InAppNotificationSettings: PreferencesEntry, Equatable { self.displayPreviews = displayPreviews } - init(decoder: Decoder) { + 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 } - func encode(_ encoder: Encoder) { + 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") diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift new file mode 100644 index 0000000000..23f879516a --- /dev/null +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -0,0 +1,211 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +class InstantImageGalleryItem: GalleryItem { + let account: Account + let theme: PresentationTheme + let strings: PresentationStrings + let image: TelegramMediaImage + let caption: String + let location: InstantPageGalleryEntryLocation + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, image: TelegramMediaImage, caption: String, location: InstantPageGalleryEntryLocation) { + self.account = account + self.theme = theme + self.strings = strings + self.image = image + self.caption = caption + self.location = location + } + + func node() -> GalleryItemNode { + let node = InstantImageGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) + + node.setImage(image: self.image) + + node._title.set(.single("\(self.location.position + 1) of \(self.location.totalCount)")) + + node.setCaption(self.caption) + + return node + } + + func updateNode(node: GalleryItemNode) { + if let node = node as? InstantImageGalleryItemNode { + node._title.set(.single("\(self.location.position + 1) of \(self.location.totalCount)")) + + node.setCaption(self.caption) + } + } +} + +final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { + private let account: Account + + private let imageNode: TransformImageNode + fileprivate let _ready = Promise() + fileprivate let _title = Promise() + private let footerContentNode: InstantPageGalleryFooterContentNode + + private var accountAndMedia: (Account, Media)? + + private var fetchDisposable = MetaDisposable() + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.account = account + + self.imageNode = TransformImageNode() + self.footerContentNode = InstantPageGalleryFooterContentNode(account: account, theme: theme, strings: strings) + + super.init() + + self.imageNode.imageUpdated = { [weak self] in + self?._ready.set(.single(Void())) + } + + self.imageNode.view.contentMode = .scaleAspectFill + self.imageNode.clipsToBounds = true + } + + deinit { + self.fetchDisposable.dispose() + } + + override func ready() -> Signal { + return self._ready.get() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + fileprivate func setCaption(_ caption: String) { + self.footerContentNode.setCaption(caption) + } + + fileprivate func setImage(image: TelegramMediaImage) { + if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(image) { + if let largestSize = largestRepresentationForPhoto(image) { + let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor + self.imageNode.alphaTransitionOnFirstUpdate = false + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: false) + self.zoomableContent = (largestSize.dimensions, self.imageNode) + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + } else { + self._ready.set(.single(Void())) + } + } + self.accountAndMedia = (account, image) + } + + func setFile(account: Account, file: TelegramMediaFile) { + if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(file) { + if let largestSize = file.dimensions { + self.imageNode.alphaTransitionOnFirstUpdate = false + let displaySize = largestSize.dividedByScreenScale() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: false), dispatchOnDisplayLink: false) + self.zoomableContent = (largestSize, self.imageNode) + } else { + self._ready.set(.single(Void())) + } + } + self.accountAndMedia = (account, file) + } + + override func animateIn(from node: ASDisplayNode) { + 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) + let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) + + let copyView = node.view.snapshotContentTree()! + + self.view.insertSubview(copyView, belowSubview: self.scrollView) + copyView.frame = transformedSelfFrame + + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in + copyView?.removeFromSuperview() + }) + + 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) + 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) + + 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.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) + } + + override func animateOut(to node: ASDisplayNode, 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) + 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()! + + self.view.insertSubview(copyView, belowSubview: self.scrollView) + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView] in + if positionCompleted && boundsCompleted && copyCompleted { + copyView?.removeFromSuperview() + completion() + } + } + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + self.imageNode.layer.animatePosition(from: self.imageNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + } + + override func visibilityUpdated(isVisible: Bool) { + super.visibilityUpdated(isVisible: isVisible) + + if let (account, media) = self.accountAndMedia, let file = media as? TelegramMediaFile { + if isVisible { + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + } else { + self.fetchDisposable.set(nil) + } + } + } + + override func title() -> Signal { + return self._title.get() + } + + override func footerContent() -> Signal { + return .single(self.footerContentNode) + } +} diff --git a/TelegramUI/InstantPageAnchorItem.swift b/TelegramUI/InstantPageAnchorItem.swift index 6feba4fce6..25600dc8b4 100644 --- a/TelegramUI/InstantPageAnchorItem.swift +++ b/TelegramUI/InstantPageAnchorItem.swift @@ -1,8 +1,8 @@ import Foundation +import Postbox import TelegramCore final class InstantPageAnchorItem: InstantPageItem { - let hasLinks: Bool = false let wantsNode: Bool = false let medias: [InstantPageMedia] = [] @@ -21,7 +21,7 @@ final class InstantPageAnchorItem: InstantPageItem { func drawInTile(context: CGContext) { } - func node(account: Account) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { return nil } @@ -29,7 +29,7 @@ final class InstantPageAnchorItem: InstantPageItem { return false } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + func linkSelectionRects(at point: CGPoint) -> [CGRect] { return [] } diff --git a/TelegramUI/InstantPageAudioItem.swift b/TelegramUI/InstantPageAudioItem.swift new file mode 100644 index 0000000000..476a314d31 --- /dev/null +++ b/TelegramUI/InstantPageAudioItem.swift @@ -0,0 +1,55 @@ +import Foundation +import Postbox +import TelegramCore + +final class InstantPageAudioItem: InstantPageItem { + var frame: CGRect + let wantsNode: Bool = true + let medias: [InstantPageMedia] + + let media: InstantPageMedia + let webpage: TelegramMediaWebpage + + init(frame: CGRect, media: InstantPageMedia, webpage: TelegramMediaWebpage) { + self.frame = frame + self.media = media + self.webpage = webpage + self.medias = [media] + } + + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + return InstantPageAudioNode(account: account, strings: strings, theme: theme, webpage: self.webpage, media: self.media, openMedia: openMedia) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + if let node = node as? InstantPageAudioNode { + return self.media == node.media + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 4 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 1000.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func drawInTile(context: CGContext) { + } +} + diff --git a/TelegramUI/InstantPageAudioNode.swift b/TelegramUI/InstantPageAudioNode.swift new file mode 100644 index 0000000000..cb86ace557 --- /dev/null +++ b/TelegramUI/InstantPageAudioNode.swift @@ -0,0 +1,264 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import AsyncDisplayKit +import Display + +private func generatePlayButton(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.setStrokeColor(color.cgColor) + context.setLineWidth(1.65) + let _ = try? drawSvgPath(context, path: "M24,0.825 C11.2008009,0.825 0.825,11.2008009 0.825,24 C0.825,36.7991991 11.2008009,47.175 24,47.175 C36.7991991,47.175 47.175,36.7991991 47.175,24 C47.175,11.2008009 36.7991991,0.825 24,0.825 S ") + let _ = try? drawSvgPath(context, path: "M19,16.8681954 L19,32.1318046 L19,32.1318046 C19,32.6785665 19.4432381,33.1218046 19.99,33.1218046 C20.1882157,33.1218046 20.3818677,33.0623041 20.5458864,32.9510057 L31.7927564,25.319201 L31.7927564,25.319201 C32.2451886,25.0121934 32.3630786,24.3965458 32.056071,23.9441136 C31.9857457,23.8404762 31.8963938,23.7511243 31.7927564,23.680799 L20.5458864,16.0489943 L20.5458864,16.0489943 C20.0934542,15.7419868 19.4778066,15.8598767 19.170799,16.312309 C19.0595006,16.4763277 19,16.6699796 19,16.8681954 Z ") + }) +} + +private func generatePauseButton(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.setStrokeColor(color.cgColor) + context.setLineWidth(1.65) + + let _ = try? drawSvgPath(context, path: "M24,0.825 C11.2008009,0.825 0.825,11.2008009 0.825,24 C0.825,36.7991991 11.2008009,47.175 24,47.175 C36.7991991,47.175 47.175,36.7991991 47.175,24 C47.175,11.2008009 36.7991991,0.825 24,0.825 S ") + let _ = try? drawSvgPath(context, path: "M17,16 L21,16 C21.5567619,16 22,16.4521029 22,17 L22,32 C22,32.5478971 21.5567619,33 21,33 L17,33 C16.4432381,33 16,32.5478971 16,32 L16,17 C16,16.4521029 16.4432381,16 17,16 Z ") + let _ = try? drawSvgPath(context, path: "M26.99,16 L31.01,16 C31.5567619,16 32,16.4432381 32,16.99 L32,32.01 C32,32.5567619 31.5567619,33 31.01,33 L26.99,33 C26.4432381,33 26,32.5567619 26,32.01 L26,16.99 C26,16.4432381 26.4432381,16 26.99,16 Z ") + }) +} + +private func titleString(media: InstantPageMedia, theme: InstantPageTheme) -> NSAttributedString { + let string = NSMutableAttributedString() + if let file = media.media as? TelegramMediaFile { + loop: for attribute in file.attributes { + if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice { + let titleText: String = title ?? "Unknown Track" + let subtitleText: String = performer ?? "Unknown Artist" + + let titleString = NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: theme.textCategories.paragraph.color) + let subtitleString = NSAttributedString(string: " — \(subtitleText)", font: Font.regular(17.0), textColor: theme.textCategories.paragraph.color) + + string.append(titleString) + string.append(subtitleString) + + break loop + } + } + } + return string +} + +final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { + private let account: Account + let media: InstantPageMedia + private let openMedia: (InstantPageMedia) -> Void + private var strings: PresentationStrings + private var theme: InstantPageTheme + + private var playImage: UIImage + private var pauseImage: UIImage + + private let buttonNode: HighlightableButtonNode + private let statusNode: RadialStatusNode + private let titleNode: ASTextNode + private let scrubbingNode: MediaPlayerScrubbingNode + private var playbackStatusDisposable: Disposable? + private var playerStatusDisposable: Disposable? + + private var isPlaying: Bool = false + private var playlistStateAndStatus: AudioPlaylistStateAndStatus? + + init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, webpage: TelegramMediaWebpage, media: InstantPageMedia, openMedia: @escaping (InstantPageMedia) -> Void) { + self.account = account + self.strings = strings + self.theme = theme + self.media = media + self.openMedia = openMedia + + self.playImage = generatePlayButton(color: theme.textCategories.paragraph.color)! + self.pauseImage = generatePauseButton(color: theme.textCategories.paragraph.color)! + + self.buttonNode = HighlightableButtonNode() + self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 1 + + var backgroundAlpha: CGFloat = 0.1 + var brightness: CGFloat = 0.0 + theme.textCategories.paragraph.color.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil) + if brightness > 0.5 { + backgroundAlpha = 0.4 + } + self.scrubbingNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color) + + super.init() + + self.titleNode.attributedText = titleString(media: media, theme: theme) + + self.addSubnode(self.statusNode) + self.addSubnode(self.buttonNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.scrubbingNode) + + self.statusNode.transitionToState(RadialStatusNodeState.customIcon(self.playImage), animated: false, completion: {}) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.statusNode.layer.removeAnimation(forKey: "opacity") + strongSelf.statusNode.alpha = 0.4 + } else { + strongSelf.statusNode.alpha = 1.0 + strongSelf.statusNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.scrubbingNode.seek = { [weak self] timestamp in + if let strongSelf = self { + if let _ = strongSelf.playlistStateAndStatus { + strongSelf.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.seek(timestamp))) + } + } + } + + if let applicationContext = account.applicationContext as? TelegramApplicationContext, let (playlistId, itemId) = instantPageAudioPlaylistAndItemIds(webpage: webpage, media: self.media) { + let playbackStatus: Signal = applicationContext.mediaManager.filteredPlaylistPlayerStateAndStatus(playlistId: playlistId, itemId: itemId) + |> mapToSignal { status -> Signal in + if let status = status, let playbackStatus = status.status { + return playbackStatus + |> map { playbackStatus -> MediaPlayerPlaybackStatus? in + return playbackStatus.status + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) + } else { + return .single(nil) + } + } + self.playbackStatusDisposable = (playbackStatus |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + var isPlaying = false + if let status = status { + switch status { + case .paused: + break + case let .buffering(whilePlaying): + isPlaying = whilePlaying + case .playing: + isPlaying = true + } + } + if strongSelf.isPlaying != isPlaying { + strongSelf.isPlaying = isPlaying + if isPlaying { + strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.pauseImage), animated: false, completion: {}) + } else { + strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.playImage), animated: false, completion: {}) + } + } + } + }) + self.playerStatusDisposable = (applicationContext.mediaManager.playlistPlayerStateAndStatus + |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndStatus in + if let strongSelf = self { + var filteredValue: AudioPlaylistStateAndStatus? + if let playlistStateAndStatus = playlistStateAndStatus { + if playlistStateAndStatus.state.playlistId.isEqual(to: playlistId) { + if let item = playlistStateAndStatus.state.item { + if item.id.isEqual(to: itemId) { + filteredValue = playlistStateAndStatus + } + } + } + } + if strongSelf.playlistStateAndStatus != filteredValue { + strongSelf.playlistStateAndStatus = filteredValue + strongSelf.scrubbingNode.status = filteredValue?.status + } + } + }) + } + } + + deinit { + self.playbackStatusDisposable?.dispose() + self.playerStatusDisposable?.dispose() + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + if self.strings !== strings || self.theme !== theme { + let themeUpdated = self.theme !== theme + self.strings = strings + self.theme = theme + + if themeUpdated { + self.playImage = generatePlayButton(color: theme.textCategories.paragraph.color)! + self.pauseImage = generatePauseButton(color: theme.textCategories.paragraph.color)! + + self.titleNode.attributedText = titleString(media: self.media, theme: theme) + + var backgroundAlpha: CGFloat = 0.1 + var brightness: CGFloat = 0.0 + theme.textCategories.paragraph.color.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil) + if brightness > 0.5 { + backgroundAlpha = 0.4 + } + + self.setNeedsLayout() + } + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + } + + func updateIsVisible(_ isVisible: Bool) { + } + + @objc func buttonPressed() { + if let _ = self.playlistStateAndStatus { + if self.isPlaying { self.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.pause)) + } else { + self.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.play)) + } + } else { + self.openMedia(self.media) + } + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + let insets = UIEdgeInsets(top: 18.0, left: 17.0, bottom: 18.0, right: 17.0) + let leftInset: CGFloat = 46.0 + 10.0 + let rightInset: CGFloat = 0.0 + + let maxTitleWidth = max(1.0, size.width - insets.left - leftInset - rightInset - insets.right) + let titleSize = self.titleNode.measure(CGSize(width: maxTitleWidth, height: size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: insets.left + leftInset, y: 2.0), size: titleSize) + + self.buttonNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 48.0, height: 48.0)) + self.statusNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 48.0, height: 48.0)) + + var topOffset: CGFloat = 0.0 + if self.titleNode.attributedText == nil || self.titleNode.attributedText!.length == 0 { + topOffset = -10.0 + } + + let leftScrubberInset: CGFloat = insets.left + 46.0 + 10.0 + let rightScrubberInset: CGFloat = insets.right + self.scrubbingNode.frame = CGRect(origin: CGPoint(x: leftScrubberInset, y: 26.0 + topOffset), size: CGSize(width: size.width - leftScrubberInset - rightScrubberInset, height: 15.0)) + } +} + diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index e8eb0f1b33..0c8cba5c4b 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -1,17 +1,29 @@ import Foundation import TelegramCore +import Postbox +import SwiftSignalKit import Display final class InstantPageController: ViewController { private let account: Account - private let webPage: TelegramMediaWebpage + private var webPage: TelegramMediaWebpage private var presentationData: PresentationData + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + var controllerNode: InstantPageControllerNode { return self.displayNode as! InstantPageControllerNode } + private var webpageDisposable: Disposable? + + private var settings: InstantPagePresentationSettings? + private var settingsDisposable: Disposable? + init(account: Account, webPage: TelegramMediaWebpage) { self.account = account self.presentationData = (account.telegramApplicationContext.currentPresentationData.with { $0 }) @@ -21,14 +33,52 @@ final class InstantPageController: ViewController { super.init(navigationBarTheme: nil) self.statusBar.statusBarStyle = .White + + self.webpageDisposable = (actualizedWebpage(postbox: self.account.postbox, network: self.account.network, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + strongSelf.webPage = result + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.updateWebPage(result) + } + } + }) + + self.settingsDisposable = (self.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.instantPagePresentationSettings]) |> deliverOnMainQueue).start(next: { [weak self] view in + if let strongSelf = self { + let settings: InstantPagePresentationSettings + if let current = view.values[ApplicationSpecificPreferencesKeys.instantPagePresentationSettings] as? InstantPagePresentationSettings { + settings = current + } else { + settings = InstantPagePresentationSettings.defaultSettings + } + strongSelf.settings = settings + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.update(settings: settings, strings: strongSelf.presentationData.strings) + } + strongSelf._ready.set(.single(true)) + } + }) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.webpageDisposable?.dispose() + self.settingsDisposable?.dispose() + } + override public func loadDisplayNode() { - self.displayNode = InstantPageControllerNode(account: self.account, strings: self.presentationData.strings, statusBar: self.statusBar) + self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, strings: self.presentationData.strings, statusBar: self.statusBar, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, openPeer: { [weak self] peerId in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + } + }, navigateBack: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }) self.displayNodeDidLoad() diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 333eca95d0..77d620a5ad 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -1,37 +1,55 @@ import Foundation +import Postbox import TelegramCore +import SwiftSignalKit import AsyncDisplayKit import Display +import SafariServices final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private let account: Account + private var settings: InstantPagePresentationSettings? + private var strings: PresentationStrings + private var theme: InstantPageTheme? + private let present: (ViewController, Any?) -> Void + private let openPeer: (PeerId) -> Void private var webPage: TelegramMediaWebpage? - private var containerLayout: ContainerViewLayout? private let statusBar: StatusBar private let navigationBar: InstantPageNavigationBar private let scrollNode: ASScrollNode private let scrollNodeHeader: ASDisplayNode + private var linkHighlightingNode: LinkHighlightingNode? + private var textSelectionNode: LinkHighlightingNode? + private var settingsNode: InstantPageSettingsNode? + private var settingsDimNode: ASDisplayNode? var currentLayout: InstantPageLayout? var currentLayoutTiles: [InstantPageTile] = [] var currentLayoutItemsWithViews: [InstantPageItem] = [] - var currentLayoutItemsWithLinks: [InstantPageItem] = [] var distanceThresholdGroupCount: [Int: Int] = [:] var visibleTiles: [Int: InstantPageTileNode] = [:] var visibleItemsWithViews: [Int: InstantPageNode] = [:] - var visibleLinkSelectionViews: [Int: InstantPageLinkSelectionView] = [:] var previousContentOffset: CGPoint? var isDeceleratingBecauseOfDragging = false - init(account: Account, strings: PresentationStrings, statusBar: StatusBar) { + private let hiddenMediaDisposable = MetaDisposable() + private let resolveUrlDisposable = MetaDisposable() + + init(account: Account, settings: InstantPagePresentationSettings?, strings: PresentationStrings, statusBar: StatusBar, present: @escaping (ViewController, Any?) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { self.account = account + self.strings = strings + self.settings = settings + self.theme = settings.flatMap(instantPageThemeForSettings) self.statusBar = statusBar + self.present = present + self.openPeer = openPeer + self.navigationBar = InstantPageNavigationBar(strings: strings) self.scrollNode = ASScrollNode() self.scrollNodeHeader = ASDisplayNode() @@ -43,11 +61,129 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return UITracingLayerView() }) - self.backgroundColor = .white + if let theme = self.theme { + self.backgroundColor = theme.pageBackgroundColor + } self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.scrollNodeHeader) self.addSubnode(self.navigationBar) + self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.delegate = self + + self.navigationBar.back = navigateBack + self.navigationBar.share = { [weak self] in + if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + var shareImpl: (([PeerId]) -> Void)? + let shareController = ShareController(account: account, shareAction: { peerIds in + shareImpl?(peerIds) + }, defaultAction: nil) + strongSelf.present(shareController, nil) + shareImpl = { [weak shareController] peerIds in + shareController?.dismiss() + + for peerId in peerIds { + let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: content.url, attributes: [], media: nil, replyToMessageId: nil)]).start() + } + } + } + } + self.navigationBar.settings = { [weak self] in + if let strongSelf = self { + strongSelf.presentSettings() + } + } + self.navigationBar.scrollToTop = { [weak self] in + if let strongSelf = self { + strongSelf.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -strongSelf.scrollNode.view.contentInset.top), animated: true) + } + } + } + + deinit { + self.hiddenMediaDisposable.dispose() + self.resolveUrlDisposable.dispose() + } + + func update(settings: InstantPagePresentationSettings, strings: PresentationStrings) { + if self.settings != settings || self.strings !== strings { + let previousSettings = self.settings + var updateLayout = previousSettings == nil + + self.settings = settings + let theme = instantPageThemeForSettings(settings) + self.theme = theme + self.strings = strings + + var animated = false + if let previousSettings = previousSettings { + if previousSettings.themeType != settings.themeType { + updateLayout = true + animated = true + } + if previousSettings.fontSize != settings.fontSize || previousSettings.forceSerif != settings.forceSerif { + animated = false + updateLayout = true + } + } + + self.backgroundColor = theme.pageBackgroundColor + + if updateLayout { + if animated { + if let snapshotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { + self.view.insertSubview(snapshotView, aboveSubview: self.scrollNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + self.updateLayout() + + for (_, itemNode) in self.visibleItemsWithViews { + itemNode.update(strings: strings, theme: theme) + } + + self.updateVisibleItems() + self.updateNavigationBar() + + self.recursivelyEnsureDisplaySynchronously(true) + } + } + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + recognizer.delaysTouchesBegan = false + recognizer.tapActionAtPoint = { [weak self] point in + if let strongSelf = self { + if let currentLayout = strongSelf.currentLayout { + for item in currentLayout.items { + if item.frame.contains(point) { + if item is InstantPagePeerReferenceItem { + return .fail + } else if item is InstantPageAudioItem { + return .fail + } + break + } + } + } + } + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + strongSelf.updateTouchesAtPoint(point) + } + } + self.scrollNode.view.addGestureRecognizer(recognizer) } func updateWebPage(_ webPage: TelegramMediaWebpage?) { @@ -67,18 +203,30 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = layout + if let settingsDimNode = self.settingsDimNode { + transition.updateFrame(node: settingsDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + + if let settingsNode = self.settingsNode { + settingsNode.updateLayout(layout: layout, transition: transition) + transition.updateFrame(node: settingsNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 let scrollInsetTop = 44.0 + statusBarHeight + let resetOffset = self.scrollNode.bounds.size.width.isZero + let widthUpdated = !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width) + if self.scrollNode.bounds.size != layout.size || !self.scrollNode.view.contentInset.top.isEqual(to: scrollInsetTop) { - if !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width) { - self.updateLayout() - } self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.scrollNodeHeader.frame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0), size: CGSize(width: layout.size.width, height: 2000.0)) self.scrollNode.view.contentInset = UIEdgeInsetsMake(scrollInsetTop, 0.0, 0.0, 0.0) - if self.visibleItemsWithViews.isEmpty && self.visibleTiles.isEmpty { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 0.0) + if resetOffset { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top) + } + if widthUpdated { + self.updateLayout() } self.updateVisibleItems() self.updateNavigationBar() @@ -86,26 +234,20 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } private func updateLayout() { - guard let containerLayout = self.containerLayout, let webPage = self.webPage else { + guard let containerLayout = self.containerLayout, let webPage = self.webPage, let theme = self.theme else { return } - let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width) + let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, strings: self.strings, theme: theme) for (_, tileNode) in self.visibleTiles { tileNode.removeFromSupernode() } self.visibleTiles.removeAll() - for (_, linkView) in self.visibleLinkSelectionViews { - linkView.removeFromSuperview() - } - self.visibleLinkSelectionViews.removeAll() - let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width) var currentLayoutItemsWithViews: [InstantPageItem] = [] - var currentLayoutItemsWithLinks: [InstantPageItem] = [] var distanceThresholdGroupCount: [Int: Int] = [:] for item in currentLayout.items { @@ -121,26 +263,25 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { distanceThresholdGroupCount[Int(group)] = count + 1 } } - if item.hasLinks { - currentLayoutItemsWithLinks.append(item) - } } self.currentLayout = currentLayout self.currentLayoutTiles = currentLayoutTiles self.currentLayoutItemsWithViews = currentLayoutItemsWithViews - self.currentLayoutItemsWithLinks = currentLayoutItemsWithLinks self.distanceThresholdGroupCount = distanceThresholdGroupCount self.scrollNode.view.contentSize = currentLayout.contentSize } func updateVisibleItems() { + guard let theme = self.theme else { + return + } + var visibleTileIndices = Set() var visibleItemIndices = Set() - var visibleItemLinkIndices = Set() - var visibleBounds = self.scrollNode.view.bounds + let visibleBounds = self.scrollNode.view.bounds var topNode: ASDisplayNode? for node in self.scrollNode.subnodes.reversed() { @@ -160,7 +301,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { visibleTileIndices.insert(tileIndex) if visibleTiles[tileIndex] == nil { - let tileNode = InstantPageTileNode(tile: tile) + let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor) tileNode.frame = tile.frame if let topNode = topNode { self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode) @@ -200,7 +341,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } if itemNode == nil { - if let itemNode = item.node(account: self.account) { + if let itemNode = item.node(account: self.account, strings: self.strings, theme: theme, openMedia: { [weak self] media in + self?.openMedia(media) + }, openPeer: { [weak self] peerId in + self?.openPeer(peerId) + }) { (itemNode as! ASDisplayNode).frame = item.frame if let topNode = topNode { self.scrollNode.insertSubnode(itemNode as! ASDisplayNode, aboveSubnode: topNode) @@ -245,37 +390,6 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { for index in removeItemIndices { self.visibleItemsWithViews.removeValue(forKey: index) } - - /* - itemIndex = -1; - for (id item in _currentLayoutItemsWithLinks) { - itemIndex++; - CGRect itemFrame = item.frame; - if (CGRectIntersectsRect(itemFrame, visibleBounds)) { - [visibleItemLinkIndices addObject:@(itemIndex)]; - - if (_visibleLinkSelectionViews[@(itemIndex)] == nil) { - NSArray *linkViews = [item linkSelectionViews]; - for (TGInstantPageLinkSelectionView *linkView in linkViews) { - linkView.itemTapped = _urlItemTapped; - - [_scrollView addSubview:linkView]; - } - _visibleLinkSelectionViews[@(itemIndex)] = linkViews; - } - } - } - - NSMutableArray *removeItemLinkIndices = [[NSMutableArray alloc] init]; - [_visibleLinkSelectionViews enumerateKeysAndObjectsUsingBlock:^(NSNumber *nIndex, NSArray *linkViews, __unused BOOL *stop) { - if (![visibleItemLinkIndices containsObject:nIndex]) { - for (UIView *linkView in linkViews) { - [linkView removeFromSuperview]; - } - [removeItemLinkIndices addObject:nIndex]; - } - }]; - [_visibleLinkSelectionViews removeObjectsForKeys:removeItemLinkIndices];*/ } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -300,6 +414,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let bounds = self.scrollNode.view.bounds let contentOffset = self.scrollNode.view.contentOffset + var pageProgress: CGFloat = 0.0 + if !self.scrollNode.view.contentSize.height.isZero { + let value = (contentOffset.y + self.scrollNode.view.contentInset.top) / (self.scrollNode.view.contentSize.height - bounds.size.height + self.scrollNode.view.contentInset.top) + pageProgress = max(0.0, min(1.0, value)) + } + let delta: CGFloat if let previousContentOffset = self.previousContentOffset { delta = contentOffset.y - previousContentOffset.y @@ -308,23 +428,6 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.previousContentOffset = contentOffset - /*void (^block)(CGRect) = ^(CGRect navigationBarFrame) { - _navigationBar.frame = navigationBarFrame; - CGFloat navigationBarHeight = _navigationBar.bounds.size.height; - if (navigationBarHeight < FLT_EPSILON) - navigationBarHeight = 64.0f; - - CGFloat statusBarOffset = -MAX(0.0f, MIN(_statusBarHeight, _statusBarHeight + 44.0f - navigationBarHeight)); - if (ABS(_statusBarOffset - statusBarOffset) > FLT_EPSILON) { - _statusBarOffset = statusBarOffset; - if (_statusBarOffsetUpdated) { - _statusBarOffsetUpdated(statusBarOffset); - } - - _scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(_navigationBar.bounds.size.height, 0.0f, 0.0f, 0.0f); - }; - };*/ - var transition: ContainedViewLayoutTransition = .immediate var navigationBarFrame = self.navigationBar.frame navigationBarFrame.size.width = bounds.size.width @@ -352,21 +455,330 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { assert(true) } - let statusBarAlpha = min(1.0, max(0.0, (navigationBarFrame.size.height - 20.0) / 44.0)) + var statusBarAlpha = min(1.0, max(0.0, (navigationBarFrame.size.height - 20.0) / 44.0)) transition.updateAlpha(node: self.statusBar, alpha: statusBarAlpha * statusBarAlpha) self.statusBar.verticalOffset = navigationBarFrame.size.height - 64.0 transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame) - self.navigationBar.updateLayout(size: navigationBarFrame.size, transition: transition) + self.navigationBar.updateLayout(size: navigationBarFrame.size, pageProgress: pageProgress, transition: transition) transition.animateView { self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: navigationBarFrame.size.height, left: 0.0, bottom: 0.0, right: 0.0) } - - /*CGFloat progress = 0.0f; - if (_scrollView.contentSize.height > FLT_EPSILON) { - progress = MAX(0.0f, MIN(1.0f, (_scrollView.contentOffset.y + _scrollView.contentInset.top) / (_scrollView.contentSize.height - _scrollView.frame.size.height + _scrollView.contentInset.top))); + } + + private func updateTouchesAtPoint(_ location: CGPoint?) { + var rects: [CGRect]? + if let location = location, let currentLayout = self.currentLayout { + for item in currentLayout.items { + if item.frame.contains(location) { + let textNodeFrame = item.frame + var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -item.frame.minX, dy: -item.frame.minY)) + for i in 0 ..< itemRects.count { + itemRects[i] = itemRects[i].offsetBy(dx: textNodeFrame.minX, dy: textNodeFrame.minY).insetBy(dx: -2.0, dy: -2.0) + } + if !itemRects.isEmpty { + rects = itemRects + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: UIColor(rgb: 0x007BE8).withAlphaComponent(0.4)) + linkHighlightingNode.isUserInteractionEnabled = false + self.linkHighlightingNode = linkHighlightingNode + self.scrollNode.addSubnode(linkHighlightingNode) + } + linkHighlightingNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + + private func textItemAtLocation(_ location: CGPoint) -> InstantPageTextItem? { + if let currentLayout = self.currentLayout { + for item in currentLayout.items { + if let item = item as? InstantPageTextItem, item.frame.contains(location) { + return item + } + } + } + return nil + } + + private func urlForTapLocation(_ location: CGPoint) -> InstantPageUrlItem? { + if let item = self.textItemAtLocation(location) { + return item.urlAttribute(at: location.offsetBy(dx: -item.frame.minX, dy: -item.frame.minY)) + } + return nil + } + + @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let url = self.urlForTapLocation(location) { + self.openUrl(url) + } + case .longTap: + if let url = self.urlForTapLocation(location) { + let actionSheet = ActionSheetController() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url.url), + ActionSheetButtonItem(title: self.self.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openUrl(url) + } + }), + ActionSheetButtonItem(title: self.strings.Web_CopyLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url.url + }), + ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url.url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.present(actionSheet, nil) + } else if let item = self.textItemAtLocation(location) { + let textNodeFrame = item.frame + var itemRects = item.lineRects() + for i in 0 ..< itemRects.count { + itemRects[i] = itemRects[i].offsetBy(dx: textNodeFrame.minX, dy: textNodeFrame.minY).insetBy(dx: -2.0, dy: -2.0) + } + self.updateTextSelectionRects(itemRects, text: item.plainText()) + } + default: + break + } + } + default: + break + } + } + + private func updateTextSelectionRects(_ rects: [CGRect], text: String?) { + if let text = text, !rects.isEmpty { + let textSelectionNode: LinkHighlightingNode + if let current = self.textSelectionNode { + textSelectionNode = current + } else { + textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4)) + textSelectionNode.isUserInteractionEnabled = false + self.textSelectionNode = textSelectionNode + self.scrollNode.addSubnode(textSelectionNode) + } + textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + textSelectionNode.updateRects(rects) + + var coveringRect = rects[0] + for i in 1 ..< rects.count { + coveringRect = coveringRect.union(rects[i]) + } + + let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + controller.dismissed = { [weak self] in + self?.updateTextSelectionRects([], text: nil) + } + self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0)) + } else { + return nil + } + })) + textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } else if let textSelectionNode = self.textSelectionNode { + self.textSelectionNode = nil + textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in + textSelectionNode?.removeFromSupernode() + }) + } + } + + private func openUrl(_ url: InstantPageUrlItem) { + guard let items = self.currentLayout?.items else { + return + } + + if let webPage = self.webPage, url.webpageId == webPage.id, let anchorRange = url.url.range(of: "#") { + let anchor = url.url.substring(from: anchorRange.upperBound) + if !anchor.isEmpty { + for item in items { + if let item = item as? InstantPageAnchorItem, item.anchor == anchor { + self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: item.frame.origin.y - self.scrollNode.view.contentInset.top), animated: true) + return + } + } + } + } + + self.resolveUrlDisposable.set((resolveUrl(account: self.account, url: url.url) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + switch result { + case let .externalUrl(url): + if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { + applicationContext.applicationBindings.openUrl(url) + } + default: + break + /*case let .peer(peerId): + strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) + case let .botStart(peerId, payload): + strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)), fromMessageId: nil) + case let .groupBotStart(peerId, payload): + break + case let .channelMessage(peerId, messageId): + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: messageId))*/ + } + } + })) + } + + private func openMedia(_ media: InstantPageMedia) { + guard let items = self.currentLayout?.items, let webPage = self.webPage else { + return + } + + if let file = media.media as? TelegramMediaFile, (file.isVoice || file.isMusic) { + var medias: [InstantPageMedia] = [] + for item in items { + for itemMedia in item.medias { + if let itemFile = itemMedia.media as? TelegramMediaFile, (itemFile.isVoice || itemFile.isMusic) { + medias.append(itemMedia) + } + } + } + let player = ManagedAudioPlaylistPlayer(audioSessionManager: self.account.telegramApplicationContext.mediaManager.audioSession, overlayMediaManager: self.account.telegramApplicationContext.mediaManager.overlayMediaManager, mediaManager: self.account.telegramApplicationContext.mediaManager, account: self.account, postbox: self.account.postbox, playlist: instantPageAudioPlaylist(account: self.account, webpage: webPage, medias: medias, at: media)) + self.account.telegramApplicationContext.mediaManager.setPlaylistPlayer(player) + player.control(.navigation(.next)) + return + } + + var medias: [InstantPageMedia] = [] + for item in items { + medias.append(contentsOf: item.medias) + } + + medias = medias.filter { + $0.media is TelegramMediaImage + } + + var entries: [InstantPageGalleryEntry] = [] + for media in medias { + entries.append(InstantPageGalleryEntry(index: Int32(media.index), media: media, caption: media.caption ?? "", location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count)))) + } + + var centralIndex: Int? + for i in 0 ..< entries.count { + if entries[i].media == media { + centralIndex = i + break + } + } + + if let centralIndex = centralIndex { + let controller = InstantPageGalleryController(account: self.account, entries: entries, centralIndex: centralIndex, replaceRootController: { _, _ in + }) + self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in + if let strongSelf = self { + for (_, itemNode) in strongSelf.visibleItemsWithViews { + if let itemNode = itemNode as? InstantPageNode { + itemNode.updateHiddenMedia(media: entry?.media) + } + } + } + })) + self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in + if let strongSelf = self { + for (_, itemNode) in strongSelf.visibleItemsWithViews { + if let itemNode = itemNode as? InstantPageNode { + if let transitionNode = itemNode.transitionNode(media: entry.media) { + return GalleryTransitionArguments(transitionNode: transitionNode, transitionContainerNode: strongSelf, transitionBackgroundNode: strongSelf) + } + } + } + } + return nil + })) + } + } + + private func presentSettings() { + guard let settings = self.settings, let containerLayout = self.containerLayout else { + return + } + if self.settingsNode == nil { + let settingsNode = InstantPageSettingsNode(strings: self.strings, settings: settings, applySettings: { [weak self] settings in + if let strongSelf = self { + strongSelf.update(settings: settings, strings: strongSelf.strings) + let _ = updateInstantPagePresentationSettingsInteractively(postbox: strongSelf.account.postbox, { _ in + return settings + }).start() + } + }) + self.addSubnode(settingsNode) + self.settingsNode = settingsNode + + let settingsDimNode = ASDisplayNode() + settingsDimNode.backgroundColor = UIColor(rgb: 0, alpha: 0.1) + settingsDimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(settingsDimTapped(_:)))) + self.insertSubnode(settingsDimNode, belowSubnode: self.navigationBar) + self.settingsDimNode = settingsDimNode + + settingsDimNode.frame = CGRect(origin: CGPoint(), size: containerLayout.size) + + settingsNode.frame = CGRect(origin: CGPoint(), size: containerLayout.size) + settingsNode.updateLayout(layout: containerLayout, transition: .immediate) + settingsNode.animateIn() + settingsDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) + self.navigationBar.updateDimmed(true, transition: transition) + transition.updateAlpha(node: self.statusBar, alpha: 0.5) + } + } + + @objc func settingsDimTapped(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let settingsNode = self.settingsNode { + self.settingsNode = nil + settingsNode.animateOut(completion: { [weak settingsNode] in + settingsNode?.removeFromSupernode() + }) + } + + if let settingsDimNode = self.settingsDimNode { + self.settingsDimNode = nil + settingsDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak settingsDimNode] _ in + settingsDimNode?.removeFromSupernode() + }) + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) + self.navigationBar.updateDimmed(false, transition: transition) + transition.updateAlpha(node: self.statusBar, alpha: 1.0) } - [_navigationBar setProgress:progress];*/ } } diff --git a/TelegramUI/InstantPageGalleryController.swift b/TelegramUI/InstantPageGalleryController.swift new file mode 100644 index 0000000000..4bdf71b8b7 --- /dev/null +++ b/TelegramUI/InstantPageGalleryController.swift @@ -0,0 +1,255 @@ +import Foundation +import Display +import QuickLook +import Postbox +import SwiftSignalKit +import AsyncDisplayKit +import TelegramCore + +struct InstantPageGalleryEntryLocation: Equatable { + let position: Int32 + let totalCount: Int32 + + static func ==(lhs: InstantPageGalleryEntryLocation, rhs: InstantPageGalleryEntryLocation) -> Bool { + return lhs.position == rhs.position && lhs.totalCount == rhs.totalCount + } +} + +struct InstantPageGalleryEntry: Equatable { + let index: Int32 + let media: InstantPageMedia + let caption: String + let location: InstantPageGalleryEntryLocation + + static func ==(lhs: InstantPageGalleryEntry, rhs: InstantPageGalleryEntry) -> Bool { + return lhs.index == rhs.index && lhs.media == rhs.media && lhs.caption == rhs.caption && lhs.location == rhs.location + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings) -> GalleryItem { + if let image = self.media.media as? TelegramMediaImage { + return InstantImageGalleryItem(account: account, theme: theme, strings: strings, image: image, caption: self.caption, location: self.location) + } else { + preconditionFailure() + } + } +} + +final class InstantPageGalleryControllerPresentationArguments { + let transitionArguments: (InstantPageGalleryEntry) -> GalleryTransitionArguments? + + init(transitionArguments: @escaping (InstantPageGalleryEntry) -> GalleryTransitionArguments?) { + self.transitionArguments = transitionArguments + } +} + +class InstantPageGalleryController: ViewController { + private var galleryNode: GalleryControllerNode { + return self.displayNode as! GalleryControllerNode + } + + private let account: Account + private var presentationData: PresentationData + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + private var didSetReady = false + + private let disposable = MetaDisposable() + + private var entries: [InstantPageGalleryEntry] = [] + private var centralEntryIndex: Int? + + private let centralItemTitle = Promise() + private let centralItemTitleView = Promise() + private let centralItemNavigationStyle = Promise() + private let centralItemFooterContentNode = Promise() + private let centralItemAttributesDisposable = DisposableSet(); + + private let _hiddenMedia = Promise(nil) + var hiddenMedia: Signal { + return self._hiddenMedia.get() + } + + private let replaceRootController: (ViewController, ValuePromise?) -> Void + + init(account: Account, entries: [InstantPageGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { + self.account = account + self.replaceRootController = replaceRootController + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: GalleryController.darkNavigationTheme) + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + + self.statusBar.statusBarStyle = .White + + let entriesSignal: Signal<[InstantPageGalleryEntry], NoError> = .single(entries) + + self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in + if let strongSelf = self { + strongSelf.entries = entries + strongSelf.centralEntryIndex = centralIndex + if strongSelf.isViewLoaded { + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ + $0.item(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings) + }), centralItemIndex: centralIndex, keepFirst: false) + + let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in + strongSelf?.didSetReady = true + } + strongSelf._ready.set(ready |> map { true }) + } + } + })) + + self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in + self?.navigationItem.title = title + })) + + self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in + self?.navigationItem.titleView = titleView + })) + + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in + self?.galleryNode.updatePresentationState({ + $0.withUpdatedFooterContentNode(footerContentNode) + }, transition: .immediate) + })) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + self.centralItemAttributesDisposable.dispose() + } + + @objc func donePressed() { + self.dismiss(forceAway: false) + } + + private func dismiss(forceAway: Bool) { + var animatedOutNode = true + var animatedOutInterface = false + + let completion = { [weak self] in + if animatedOutNode && animatedOutInterface { + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + } + + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? InstantPageGalleryControllerPresentationArguments { + if !self.entries.isEmpty { + if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { + animatedOutNode = false + centralItemNode.animateOut(to: transitionArguments.transitionNode, completion: { + animatedOutNode = true + completion() + }) + } + } + } + + self.galleryNode.animateOut(animateContent: animatedOutNode, completion: { + animatedOutInterface = true + completion() + }) + } + + override func loadDisplayNode() { + let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in + if let strongSelf = self { + strongSelf.present(controller, in: .window(.root), with: arguments) + } + }, dismissController: { [weak self] in + self?.dismiss(forceAway: true) + }, replaceRootController: { [weak self] controller, ready in + if let strongSelf = self { + strongSelf.replaceRootController(controller, ready) + } + }) + self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction) + self.displayNodeDidLoad() + + self.galleryNode.statusBar = self.statusBar + self.galleryNode.navigationBar = self.navigationBar + + self.galleryNode.transitionNodeForCentralItem = { [weak self] in + if let strongSelf = self { + if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? InstantPageGalleryControllerPresentationArguments { + if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { + return transitionArguments.transitionNode + } + } + } + return nil + } + self.galleryNode.dismiss = { [weak self] in + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + + self.galleryNode.pager.replaceItems(self.entries.map({ + $0.item(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings) + }), centralItemIndex: self.centralEntryIndex) + + self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in + if let strongSelf = self { + var hiddenItem: InstantPageGalleryEntry? + if let index = index { + hiddenItem = strongSelf.entries[index] + + if let node = strongSelf.galleryNode.pager.centralItemNode() { + strongSelf.centralItemTitle.set(node.title()) + strongSelf.centralItemTitleView.set(node.titleView()) + strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) + strongSelf.centralItemFooterContentNode.set(node.footerContent()) + } + } + if strongSelf.didSetReady { + strongSelf._hiddenMedia.set(.single(hiddenItem)) + } + } + } + + let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in + self?.didSetReady = true + } + self._ready.set(ready |> map { true }) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + var nodeAnimatesItself = false + + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? InstantPageGalleryControllerPresentationArguments { + self.centralItemTitle.set(centralItemNode.title()) + self.centralItemTitleView.set(centralItemNode.titleView()) + self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) + self.centralItemFooterContentNode.set(centralItemNode.footerContent()) + + if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) { + nodeAnimatesItself = true + centralItemNode.animateIn(from: transitionArguments.transitionNode) + + self._hiddenMedia.set(.single(self.entries[centralItemNode.index])) + } + } + + self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} diff --git a/TelegramUI/InstantPageGalleryFooterContentNode.swift b/TelegramUI/InstantPageGalleryFooterContentNode.swift new file mode 100644 index 0000000000..9a40e4c4cf --- /dev/null +++ b/TelegramUI/InstantPageGalleryFooterContentNode.swift @@ -0,0 +1,76 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import Photos + +private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionAction"), color: .white) + +private let textFont = Font.regular(16.0) + +final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { + private let account: Account + private var theme: PresentationTheme + private var strings: PresentationStrings + + private let actionButton: UIButton + private let textNode: ASTextNode + + private var currentMessageText: String? + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.account = account + self.theme = theme + self.strings = strings + + self.actionButton = UIButton() + + self.actionButton.setImage(actionImage, for: [.normal]) + + self.textNode = ASTextNode() + + super.init() + + self.view.addSubview(self.actionButton) + self.addSubnode(self.textNode) + + self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside]) + } + + func setCaption(_ caption: String) { + if self.currentMessageText != caption { + self.currentMessageText = caption + + if caption.isEmpty { + self.textNode.isHidden = true + self.textNode.attributedText = nil + } else { + self.textNode.isHidden = false + self.textNode.attributedText = NSAttributedString(string: caption, font: textFont, textColor: .white) + } + + self.requestLayout?(.immediate) + } + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + var panelHeight: CGFloat = 44.0 + if !self.textNode.isHidden { + let sideInset: CGFloat = 8.0 + let topInset: CGFloat = 8.0 + let bottomInset: CGFloat = 8.0 + let textSize = self.textNode.measure(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + panelHeight += textSize.height + topInset + bottomInset + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize)) + } + + self.actionButton.frame = CGRect(origin: CGPoint(x: 0.0, y: panelHeight - 44.0), size: CGSize(width: 44.0, height: 44.0)) + + return panelHeight + } + + @objc func actionButtonPressed() { + } +} diff --git a/TelegramUI/InstantPageImageItem.swift b/TelegramUI/InstantPageImageItem.swift new file mode 100644 index 0000000000..8928bb06e6 --- /dev/null +++ b/TelegramUI/InstantPageImageItem.swift @@ -0,0 +1,61 @@ +import Foundation +import Postbox +import TelegramCore + +final class InstantPageImageItem: InstantPageItem { + var frame: CGRect + + let media: InstantPageMedia + var medias: [InstantPageMedia] { + return [self.media] + } + + let interactive: Bool + let roundCorners: Bool + let fit: Bool + + let wantsNode: Bool = true + + init(frame: CGRect, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool) { + self.frame = frame + self.media = media + self.interactive = interactive + self.roundCorners = roundCorners + self.fit = fit + } + + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + return InstantPageImageNode(account: account, media: self.media, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + if let node = node as? InstantPageImageNode { + return node.media == self.media + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 1 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 400.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func drawInTile(context: CGContext) { + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } +} diff --git a/TelegramUI/InstantPageImageNode.swift b/TelegramUI/InstantPageImageNode.swift new file mode 100644 index 0000000000..df128b06cf --- /dev/null +++ b/TelegramUI/InstantPageImageNode.swift @@ -0,0 +1,102 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +final class InstantPageImageNode: ASDisplayNode, InstantPageNode { + private let account: Account + let media: InstantPageMedia + private let interactive: Bool + private let roundCorners: Bool + private let fit: Bool + private let openMedia: (InstantPageMedia) -> Void + + private let imageNode: TransformImageNode + + private var currentSize: CGSize? + + private var fetchedDisposable = MetaDisposable() + + init(account: Account, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { + self.account = account + self.media = media + self.interactive = interactive + self.roundCorners = roundCorners + self.fit = fit + self.openMedia = openMedia + + self.imageNode = TransformImageNode() + + super.init() + + self.imageNode.alphaTransitionOnFirstUpdate = true + self.addSubnode(self.imageNode) + + if let image = media.media as? TelegramMediaImage { + self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image)) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) + } else if let file = media.media as? TelegramMediaFile { + self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: file)) + } + } + + deinit { + self.fetchedDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + if self.interactive { + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + } + + func updateIsVisible(_ isVisible: Bool) { + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + if self.currentSize != size { + self.currentSize = size + + self.imageNode.frame = CGRect(origin: CGPoint(), size: size) + + if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { + let imageSize = largest.dimensions.aspectFilled(size) + let boundingSize = size + var radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0 + + let makeLayout = self.imageNode.asyncLayout() + let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + apply() + } + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + if media == self.media { + return self.imageNode + } else { + return nil + } + } + + func updateHiddenMedia(media: InstantPageMedia?) { + self.imageNode.isHidden = self.media == media + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.openMedia(self.media) + } + } +} diff --git a/TelegramUI/InstantPageItem.swift b/TelegramUI/InstantPageItem.swift index f92e9eb933..3f0a72f44d 100644 --- a/TelegramUI/InstantPageItem.swift +++ b/TelegramUI/InstantPageItem.swift @@ -1,17 +1,17 @@ import Foundation +import Postbox import TelegramCore protocol InstantPageItem { var frame: CGRect { get set } - var hasLinks: Bool { get } var wantsNode: Bool { get } var medias: [InstantPageMedia] { get } func matchesAnchor(_ anchor: String) -> Bool func drawInTile(context: CGContext) - func node(account: Account) -> InstantPageNode? + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? func matchesNode(_ node: InstantPageNode) -> Bool - func linkSelectionViews() -> [InstantPageLinkSelectionView] + func linkSelectionRects(at point: CGPoint) -> [CGRect] func distanceThresholdGroup() -> Int? func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index 7fe74fe270..706b9164ca 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -23,28 +23,41 @@ final class InstantPageLayout { } } -func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout { +private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) { + let attributes = theme.textCategories.attributes(type: category, link: link) + stack.push(.textColor(attributes.color)) + switch attributes.font.style { + case .sans: + stack.push(.fontSerif(false)) + case .serif: + stack.push(.fontSerif(true)) + } + stack.push(.fontSize(attributes.font.size)) + stack.push(.lineSpacingFactor(attributes.font.lineSpacingFactor)) + if attributes.underline { + stack.push(.underline) + } +} + +func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout { switch block { case let .cover(block): - return layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + return layoutInstantPageBlock(webpage: webpage, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) case let .title(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(28.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) + setupStyleStack(styleStack, theme: theme, category: .header, 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]) case let .subtitle(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) + setupStyleStack(styleStack, theme: theme, category: .subheader, 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]) case let .authorDate(author: author, date: date): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) - styleStack.push(.textColor(UIColor(rgb: 0x79828b))) + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) var text: RichText? if case .empty = author { if date != 0 { @@ -91,31 +104,25 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h } case let .header(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(24.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) + setupStyleStack(styleStack, theme: theme, category: .header, 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]) case let .subheader(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(19.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) + setupStyleStack(styleStack, theme: theme, category: .subheader, 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]) case let .paragraph(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) + setupStyleStack(styleStack, theme: theme, category: .paragraph, 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]) case let .preformatted(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(16.0)) - styleStack.push(.fontFixed(true)) - styleStack.push(.lineSpacingFactor(0.685)) + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) 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) @@ -129,7 +136,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) case .divider: let lineWidth = floor(boundingWidth / 2.0) - let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: UIColor(rgb: 0x79828b)) + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: theme.textCategories.caption.color) return InstantPageLayout(origin: CGPoint(), contentSize: shapeItem.frame.size, items: [shapeItem]) case let .list(contentItems, ordered): var contentSize = CGSize(width: boundingWidth, height: 0.0) @@ -139,14 +146,14 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h for i in 0 ..< contentItems.count { if ordered { let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) let textItem = layoutTextItemWithString(attributedStringForRichText(.plain("\(i + 1)."), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) if let line = textItem.lines.first { maxIndexWidth = max(maxIndexWidth, line.frame.size.width) } indexItems.append(textItem) } else { - let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 6.0, height: 12.0)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 6.0, height: 6.0)), shape: .ellipse, color: UIColor.black) + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 6.0, height: 12.0)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 6.0, height: 6.0)), shape: .ellipse, color: theme.textCategories.paragraph.color) indexItems.append(shapeItem) } } @@ -156,7 +163,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h contentSize.height += 20.0 } let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) let textItem = layoutTextItemWithString(attributedStringForRichText(contentItems[i], styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth) textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + indexSpacing + maxIndexWidth, dy: contentSize.height) @@ -175,8 +182,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h var items: [InstantPageItem] = [] let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) - styleStack.push(.fontSerif(true)) + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) styleStack.push(.italic) let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset) @@ -190,7 +196,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h contentSize.height += 14.0 let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset) captionItem.frame = captionItem.frame.offsetBy(dx: horizontalInset + lineInset, dy: contentSize.height) @@ -212,8 +218,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h var items: [InstantPageItem] = [] let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) - styleStack.push(.fontSerif(true)) + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) styleStack.push(.italic) let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) @@ -228,7 +233,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h contentSize.height += 14.0 let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) @@ -259,7 +264,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h var contentSize = CGSize(width: boundingWidth, height: 0.0) var items: [InstantPageItem] = [] - let mediaItem = InstantPageMediaItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), arguments: InstantPageMediaArguments.image(interactive: true, roundCorners: false, fit: false)) + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) items.append(mediaItem) contentSize.height += filledSize.height @@ -269,7 +274,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h contentSize.height += 10.0 let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) @@ -283,6 +288,206 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h } else { return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } + case let .video(id, caption, autoplay, loop): + if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions { + let imageSize = dimensions + var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth, height: 1200.0)) + + if fillToWidthAndHeight { + filledSize = CGSize(width: boundingWidth, height: boundingWidth) + } else if isCover { + filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth, height: 1.0)) + if !filledSize.height.isZero { + filledSize = filledSize.cropped(CGSize(width: boundingWidth, height: floor(boundingWidth * 3.0 / 5.0))) + } + } + + let mediaIndex = mediaIndexCounter + mediaIndexCounter += 1 + + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] + + if autoplay { + let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true) + + items.append(mediaItem) + } else { + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) + + items.append(mediaItem) + } + contentSize.height += filledSize.height + + if case .empty = caption { + } else { + contentSize.height += 10.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + } else { + return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) + } + case let .collage(items: innerItems, caption: caption): + let spacing: CGFloat = 2.0 + let itemsPerRow = 3 + let itemSize = floor((boundingWidth - spacing * max(0.0, CGFloat(itemsPerRow - 1))) / CGFloat(itemsPerRow)) + + var items: [InstantPageItem] = [] + + var nextItemOrigin = CGPoint(x: 0.0, y: 0.0) + for subItem in innerItems { + if nextItemOrigin.x + itemSize > boundingWidth { + nextItemOrigin.x = 0.0 + nextItemOrigin.y += itemSize + spacing + } + let subLayout = layoutInstantPageBlock(webpage: webpage, block: subItem, boundingWidth: itemSize, horizontalInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: true, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + items.append(contentsOf: subLayout.flattenedItemsWithOrigin(nextItemOrigin)) + nextItemOrigin.x += itemSize + spacing + } + + var contentSize = CGSize(width: boundingWidth, height: nextItemOrigin.y + itemSize) + + if case .empty = caption { + } else { + contentSize.height += 10.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .postEmbed(url, webpageId, avatarId, author, date, blocks, caption): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + let lineInset: CGFloat = 20.0 + let verticalInset: CGFloat = 4.0 + let itemSpacing: CGFloat = 10.0 + var avatarInset: CGFloat = 0.0 + var avatarVerticalInset: CGFloat = 0.0 + + contentSize.height += verticalInset + + var items: [InstantPageItem] = [] + + if !author.isEmpty { + let avatar: TelegramMediaImage? = avatarId.flatMap { media[$0] as? TelegramMediaImage } + if let avatar = avatar { + let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), media: InstantPageMedia(index: -1, media: avatar, caption: ""), interactive: false, roundCorners: true, fit: false) + items.append(avatarItem) + + avatarInset += 62.0 + avatarVerticalInset += 6.0 + if date == 0 { + avatarVerticalInset += 11.0 + } + } + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + styleStack.push(.bold) + + let textItem = layoutTextItemWithString(attributedStringForRichText(.plain(author), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset) + textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset + avatarInset, dy: contentSize.height + avatarVerticalInset) + items.append(textItem) + + contentSize.height += textItem.frame.size.height + avatarVerticalInset + } + if date != 0 { + if items.count != 0 { + contentSize.height += itemSpacing + } + + let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(date)), dateStyle: .long, timeStyle: .none) + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let textItem = layoutTextItemWithString(attributedStringForRichText(.plain(dateString), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset) + textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset + avatarInset, dy: contentSize.height) + items.append(textItem) + + contentSize.height += textItem.frame.size.height + } + + if items.count != 0 { + contentSize.height += itemSpacing + } + + var previousBlock: InstantPageBlock? + for subBlock in blocks { + let subLayout = layoutInstantPageBlock(webpage: webpage, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + + let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) + let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) + items.append(contentsOf: blockItems) + contentSize.height += subLayout.contentSize.height + spacing + previousBlock = subBlock + } + + 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)) + + if case .empty = caption { + } else { + contentSize.height += 14.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .slideshow(items: subItems, caption: caption): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] + + var itemMedias: [InstantPageMedia] = [] + + for subBlock in subItems { + switch subBlock { + case let .image(id, caption): + if let image = media[id] as? TelegramMediaImage, let imageSize = largestImageRepresentation(image.representations)?.dimensions { + let mediaIndex = mediaIndexCounter + mediaIndexCounter += 1 + + let filledSize = imageSize.fitted(CGSize(width: boundingWidth, height: 1200.0)) + contentSize.height = max(contentSize.height, filledSize.height) + + itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, caption: "")) + } + break + default: + break + } + } + + items.append(InstantPageSlideshowItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), medias: itemMedias)) + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .webEmbed(url, html, dimensions, caption, stretchToWidth, allowScrolling, coverId): var embedBoundingWidth = boundingWidth - horizontalInset * 2.0 if stretchToWidth { @@ -294,14 +499,83 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h } else { size = dimensions.aspectFitted(CGSize(width: embedBoundingWidth, height: embedBoundingWidth)) } + + var items: [InstantPageItem] = [] + let item = InstantPageWebEmbedItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size), url: url, html: html, enableScrolling: allowScrolling) + items.append(item) + + var contentSize = item.frame.size + + if case .empty = caption { + } else { + contentSize.height += 10.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .channelBanner(peer): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] + + var rtl = false + if let previousItem = previousItems.last as? InstantPageTextItem, previousItem.containsRTL { + rtl = true + } + + if let peer = peer { + let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, rtl: rtl) + items.append(item) + contentSize.height += 40.0 + } + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .anchor(name): + let item = InstantPageAnchorItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: 0.0)), anchor: name) return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + case let .audio(id: audioId, caption: caption): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] + + if let file = media[audioId] as? TelegramMediaFile { + let mediaIndex = mediaIndexCounter + mediaIndexCounter += 1 + let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: file, caption: ""), webpage: webpage) + + contentSize.height += item.frame.size.height + items.append(item) + + if case .empty = caption { + } else { + contentSize.height += 10.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + } + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) default: return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } } -func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat) -> InstantPageLayout { +func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, strings: PresentationStrings, theme: InstantPageTheme) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content @@ -322,11 +596,10 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: var mediaIndexCounter: Int = 0 var embedIndexCounter: Int = 0 - let theme = InstantPageTheme() var previousBlock: InstantPageBlock? for block in pageBlocks { - let blockLayout = layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: 17.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + let blockLayout = layoutInstantPageBlock(webpage: webPage, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) diff --git a/TelegramUI/InstantPageLayoutSpacings.swift b/TelegramUI/InstantPageLayoutSpacings.swift index 9b72c62046..11140d2193 100644 --- a/TelegramUI/InstantPageLayoutSpacings.swift +++ b/TelegramUI/InstantPageLayoutSpacings.swift @@ -4,7 +4,7 @@ import TelegramCore func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> CGFloat { if let upper = upper, let lower = lower { switch (upper, lower) { - case (_, .cover): + case (_, .cover), (_, .channelBanner): return 0.0 case (.divider, _), (_, .divider): return 25.0 diff --git a/TelegramUI/InstantPageManagedMediaId.swift b/TelegramUI/InstantPageManagedMediaId.swift new file mode 100644 index 0000000000..1502af1bbc --- /dev/null +++ b/TelegramUI/InstantPageManagedMediaId.swift @@ -0,0 +1,27 @@ +import Foundation +import Postbox + +struct InstantPageManagedMediaId: ManagedMediaId { + let media: InstantPageMedia + + init(media: InstantPageMedia) { + self.media = media + } + + var hashValue: Int { + if let id = self.media.media.id { + return id.hashValue + } else { + return 0 + } + } + + func isEqual(to: ManagedMediaId) -> Bool { + if let to = to as? InstantPageManagedMediaId { + return self.media == to.media + } else { + return false + } + } +} + diff --git a/TelegramUI/InstantPageMediaAudioPlaylist.swift b/TelegramUI/InstantPageMediaAudioPlaylist.swift new file mode 100644 index 0000000000..7182df4352 --- /dev/null +++ b/TelegramUI/InstantPageMediaAudioPlaylist.swift @@ -0,0 +1,133 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +struct InstantPageAudioPlaylistItemId: AudioPlaylistItemId { + let index: Int + let id: MediaId + + var hashValue: Int { + return self.id.hashValue &+ self.index.hashValue + } + + func isEqual(to: AudioPlaylistItemId) -> Bool { + if let other = to as? InstantPageAudioPlaylistItemId { + return self.index == other.index && self.id == other.id + } else { + return false + } + } +} + +final class InstantPageAudioPlaylistItem: AudioPlaylistItem { + let media: InstantPageMedia + + var id: AudioPlaylistItemId { + return InstantPageAudioPlaylistItemId(index: self.media.index, id: self.media.media.id!) + } + + var resource: MediaResource? { + if let file = self.media.media as? TelegramMediaFile { + return file.resource + } + return nil + } + + var streamable: Bool { + if let file = self.media.media as? TelegramMediaFile { + if file.isMusic { + return true + } + } + return false + } + + var info: AudioPlaylistItemInfo? { + if let file = self.media.media as? TelegramMediaFile { + for attribute in file.attributes { + switch attribute { + case let .Audio(isVoice, duration, title, performer, _): + if isVoice { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) + } else { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) + } + case let .Video(duration, _, flags): + if flags.contains(.instantRoundVideo) { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .video) + } + default: + break + } + } + return nil + } + return nil + } + + init(media: InstantPageMedia) { + self.media = media + } + + func isEqual(to: AudioPlaylistItem) -> Bool { + if let other = to as? InstantPageAudioPlaylistItem { + return self.media == other.media + } else { + return false + } + } +} + +struct InstantPageAudioPlaylistId: AudioPlaylistId { + let webpageId: MediaId + + func isEqual(to: AudioPlaylistId) -> Bool { + if let other = to as? InstantPageAudioPlaylistId { + if self.webpageId != other.webpageId { + return false + } + return true + } else { + return false + } + } +} + +func instantPageAudioPlaylistAndItemIds(webpage: TelegramMediaWebpage, media: InstantPageMedia) -> (AudioPlaylistId, AudioPlaylistItemId)? { + return (InstantPageAudioPlaylistId(webpageId: webpage.webpageId), InstantPageAudioPlaylistItemId(index: media.index, id: media.media.id!)) +} + +func instantPageAudioPlaylist(account: Account, webpage: TelegramMediaWebpage, medias: [InstantPageMedia], at centralMedia: InstantPageMedia) -> AudioPlaylist { + return AudioPlaylist(id: InstantPageAudioPlaylistId(webpageId: webpage.webpageId), navigate: { item, navigation in + if let item = item as? InstantPageAudioPlaylistItem { + if let index = medias.index(of: item.media) { + switch navigation { + case .previous: + if index == 0 { + return .single(item) + } else { + return .single(InstantPageAudioPlaylistItem(media: medias[index - 1])) + } + case .next: + if index == medias.count - 1 { + return .single(nil) + } else { + return .single(InstantPageAudioPlaylistItem(media: medias[index + 1])) + } + } + } else { + return .single(nil) + } + } else { + if let index = medias.index(of: centralMedia) { + return .single(InstantPageAudioPlaylistItem(media: medias[index])) + } else if let media = medias.first { + return .single(InstantPageAudioPlaylistItem(media: media)) + } else { + return .single(nil) + } + } + }) +} + diff --git a/TelegramUI/InstantPageMediaNode.swift b/TelegramUI/InstantPageMediaNode.swift deleted file mode 100644 index bc617dfb4f..0000000000 --- a/TelegramUI/InstantPageMediaNode.swift +++ /dev/null @@ -1,110 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display -import Postbox -import TelegramCore -import SwiftSignalKit - -final class InstantPageMediaNode: ASDisplayNode, InstantPageNode { - private let account: Account - let media: InstantPageMedia - private let arguments: InstantPageMediaArguments - - private let imageNode: TransformImageNode - - private var currentSize: CGSize? - - private var fetchedDisposable = MetaDisposable() - - init(account: Account, media: InstantPageMedia, arguments: InstantPageMediaArguments) { - self.account = account - self.media = media - self.arguments = arguments - - self.imageNode = TransformImageNode() - - super.init() - - self.imageNode.alphaTransitionOnFirstUpdate = true - self.addSubnode(self.imageNode) - - if let image = media.media as? TelegramMediaImage { - self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image)) - self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) - } - } - - deinit { - self.fetchedDisposable.dispose() - } - - func updateIsVisible(_ isVisible: Bool) { - - } - - override func layout() { - super.layout() - - let size = self.bounds.size - - if self.currentSize != size { - self.currentSize = size - - self.imageNode.frame = CGRect(origin: CGPoint(), size: size) - - if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { - let imageSize = largest.dimensions.aspectFilled(size) - let boundingSize = size - var radius: CGFloat = 0.0 - - switch arguments { - case let .image(_, roundCorners, fit): - radius = roundCorners ? floor(min(size.width, size.height) / 2.0) : 0.0 - default: - break - } - let makeLayout = self.imageNode.asyncLayout() - let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) - apply() - } - } - } -} - -/*- (void)layoutSubviews { - [super layoutSubviews]; - - CGSize size = self.bounds.size; - _button.frame = self.bounds; - CGSize overlaySize = _overlayView.bounds.size; - _overlayView.frame = CGRectMake(CGFloor((size.width - overlaySize.width) / 2.0f), CGFloor((size.height - overlaySize.height) / 2.0f), overlaySize.width, overlaySize.height); - _imageView.frame = self.bounds; - - _videoView.frame = self.bounds; - - if (!CGSizeEqualToSize(_currentSize, size)) { - _currentSize = size; - - if ([_media.media isKindOfClass:[TGImageMediaAttachment class]]) { - TGImageMediaAttachment *image = _media.media; - CGSize imageSize = TGFillSize([image dimensions], size); - CGSize boundingSize = size; - - CGFloat radius = 0.0f; - if ([_arguments isKindOfClass:[TGInstantPageImageMediaArguments class]]) { - TGInstantPageImageMediaArguments *imageArguments = (TGInstantPageImageMediaArguments *)_arguments; - if (imageArguments.fit) { - _imageView.contentMode = UIViewContentModeScaleAspectFit; - imageSize = TGFitSize([image dimensions], size); - boundingSize = imageSize; - } - radius = imageArguments.roundCorners ? CGFloor(MIN(size.width, size.height) / 2.0f) : 0.0f; - } - [_imageView setArguments:[[TransformImageArguments alloc] initWithImageSize:imageSize boundingSize:boundingSize cornerRadius:radius]]; - } else if ([_media.media isKindOfClass:[TGVideoMediaAttachment class]]) { - TGVideoMediaAttachment *video = _media.media; - CGSize imageSize = TGFillSize([video dimensions], size); - [_imageView setArguments:[[TransformImageArguments alloc] initWithImageSize:imageSize boundingSize:size cornerRadius:0.0f]]; - } - } -}*/ diff --git a/TelegramUI/InstantPageNavigationBar.swift b/TelegramUI/InstantPageNavigationBar.swift index a3242f8121..206b47c0e4 100644 --- a/TelegramUI/InstantPageNavigationBar.swift +++ b/TelegramUI/InstantPageNavigationBar.swift @@ -2,41 +2,54 @@ import Foundation import Display import AsyncDisplayKit -private let backArrowImage = UIImage(bundleImageName: "Instant View/BackArrow")?.precomposed() -private let settingsImage = UIImage(bundleImageName: "Instant View/SettingsIcon")?.precomposed() +private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white) +private let moreImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/MoreIcon"), color: .white) +private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/ActionIcon"), color: .white) final class InstantPageNavigationBar: ASDisplayNode { private var strings: PresentationStrings - private var pageProgress: CGFloat = 0.0 + private let pageProgressNode: ASDisplayNode + private let backButton: HighlightableButtonNode + private let moreButton: HighlightableButtonNode + private let actionButton: HighlightableButtonNode + private let scrollToTopButton: HighlightableButtonNode + private let arrowNode: ASImageNode - let pageProgressNode: ASDisplayNode - let backButton: HighlightableButtonNode - let shareButton: HighlightableButtonNode - let settingsButton: HighlightableButtonNode - let scrollToTopButton: HighlightableButtonNode - let arrowNode: ASImageNode - let shareLabel: ASTextNode - var shareLabelSize: CGSize - var shareLabelSmallSize: CGSize + private let intrinsicMoreSize: CGSize + private let intrinsicSmallMoreSize: CGSize + private let intrinsicActionSize: CGSize + private let intrinsicSmallActionSize: CGSize + + private var dimmed: Bool = false + private var buttonsAlphaFactor: CGFloat = 1.0 var back: (() -> Void)? var share: (() -> Void)? var settings: (() -> Void)? + var scrollToTop: (() -> Void)? init(strings: PresentationStrings) { self.strings = strings self.pageProgressNode = ASDisplayNode() self.pageProgressNode.isLayerBacked = true + self.pageProgressNode.backgroundColor = UIColor(rgb: 0x242425) self.backButton = HighlightableButtonNode() - self.shareButton = HighlightableButtonNode() - self.settingsButton = HighlightableButtonNode() + self.moreButton = HighlightableButtonNode() + self.actionButton = HighlightableButtonNode() self.scrollToTopButton = HighlightableButtonNode() - self.settingsButton.setImage(settingsImage, for: []) - self.settingsButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0)) + self.actionButton.setImage(actionImage, for: []) + self.intrinsicActionSize = CGSize(width: 44.0, height: 44.0) + self.intrinsicSmallActionSize = CGSize(width: 20.0, height: 20.0) + self.actionButton.frame = CGRect(origin: CGPoint(), size: self.intrinsicActionSize) + + self.moreButton.setImage(moreImage, for: []) + self.intrinsicMoreSize = CGSize(width: 44.0, height: 44.0) + self.intrinsicSmallMoreSize = CGSize(width: 20.0, height: 20.0) + self.moreButton.frame = CGRect(origin: CGPoint(), size: self.intrinsicMoreSize) self.arrowNode = ASImageNode() self.arrowNode.image = backArrowImage @@ -44,50 +57,55 @@ final class InstantPageNavigationBar: ASDisplayNode { self.arrowNode.displayWithoutProcessing = true self.arrowNode.displaysAsynchronously = false - self.shareLabel = ASTextNode() - self.shareLabel.attributedText = NSAttributedString(string: strings.Channel_Share, font: Font.regular(17.0), textColor: UIColor(white: 1.0, alpha: 0.7)) - self.shareLabel.isLayerBacked = true - self.shareLabel.displaysAsynchronously = false - - let shareLabelSmall = ASTextNode() - shareLabelSmall.attributedText = NSAttributedString(string: strings.Channel_Share, font: Font.regular(12.0), textColor: UIColor(white: 1.0, alpha: 0.7)) - - self.shareLabelSize = self.shareLabel.measure(CGSize(width: 200.0, height: 100.0)) - self.shareLabelSmallSize = shareLabelSmall.measure(CGSize(width: 200.0, height: 100.0)) - - self.shareLabel.frame = CGRect(origin: CGPoint(), size: self.shareLabelSize) - super.init() self.backgroundColor = .black self.backButton.addSubnode(self.arrowNode) - self.shareButton.addSubnode(self.shareLabel) self.addSubnode(self.pageProgressNode) self.addSubnode(self.backButton) - self.addSubnode(self.shareButton) self.addSubnode(self.scrollToTopButton) - //self.addSubnode(self.settingsButton) + self.addSubnode(self.moreButton) + self.addSubnode(self.actionButton) self.backButton.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) - self.shareButton.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside) - self.settingsButton.addTarget(self, action: #selector(self.settingsPressed), forControlEvents: .touchUpInside) + self.actionButton.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) + self.moreButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside) + self.scrollToTopButton.addTarget(self, action: #selector(self.scrollToTopPressed), forControlEvents: .touchUpInside) } @objc func backPressed() { self.back?() } - @objc func sharePressed() { + @objc func actionPressed() { self.share?() } - @objc func settingsPressed() { + @objc func morePressed() { self.settings?() } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + @objc func scrollToTopPressed() { + self.scrollToTop?() + } + + func updateDimmed(_ dimmed: Bool, transition: ContainedViewLayoutTransition) { + if dimmed != self.dimmed { + self.dimmed = dimmed + transition.updateAlpha(node: self.arrowNode, alpha: dimmed ? 0.5 : 1.0) + var buttonsAlpha = self.buttonsAlphaFactor + if dimmed { + buttonsAlpha *= 0.5 + } + transition.updateAlpha(node: self.actionButton, alpha: buttonsAlpha) + } + } + + func updateLayout(size: CGSize, pageProgress: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.pageProgressNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * pageProgress), height: size.height))) + transition.updateFrame(node: self.backButton, frame: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: 100.0, height: size.height))) if let image = arrowNode.image { let arrowImageSize = image.size @@ -102,41 +120,39 @@ final class InstantPageNavigationBar: ASDisplayNode { transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: 8.0, y: max(0.0, size.height - 44.0) + floor((min(size.height, 44.0) - scaledArrowSize.height) / 2.0)), size: scaledArrowSize)) } - transition.updateFrame(node: shareButton, frame: CGRect(origin: CGPoint(x: size.width - 80.0, y: 0.0), size: CGSize(width: 80.0, height: size.height))) - - let shareImageSize = self.shareLabelSize - let shareSmallImageSize = self.shareLabelSmallSize - let shareHeight: CGFloat + let offsetScaleFactor: CGFloat + let buttonScaleFactor: CGFloat if size.height.isLess(than: 64.0) { - let k = (shareImageSize.height - shareSmallImageSize.height) / 44.0 - let b = shareSmallImageSize.height - k * 20.0; - shareHeight = k * size.height + b + offsetScaleFactor = max(size.height - 20.0, 0.0) / 44.0 + let k = (self.intrinsicMoreSize.height - self.intrinsicSmallMoreSize.height) / 44.0 + let b = self.intrinsicSmallMoreSize.height - k * 20.0; + buttonScaleFactor = (k * size.height + b) / self.intrinsicMoreSize.height } else { - shareHeight = shareImageSize.height; + offsetScaleFactor = 1.0 + buttonScaleFactor = 1.0 } - let shareHeightFactor = shareHeight / shareImageSize.height - transition.updateTransformScale(node: self.shareLabel, scale: shareHeightFactor) - let scaledShareSize = CGSize(width: shareImageSize.width * shareHeightFactor, height: shareImageSize.height * shareHeightFactor) - let shareLabelCenter = CGPoint(x: 80.0 - 8.0 - scaledShareSize.width / 2.0, y: max(0.0, size.height - 44.0) + min(size.height, 44.0) / 2.0) - transition.updatePosition(node: self.shareLabel, position: shareLabelCenter) + var alphaFactor = min(1.0, offsetScaleFactor * offsetScaleFactor) + self.buttonsAlphaFactor = alphaFactor + if self.dimmed { + alphaFactor *= 0.5 + } - let alpha = 1.0 - (shareImageSize.height - shareHeight) / (shareImageSize.height - shareSmallImageSize.height) - let diffFactor = shareSmallImageSize.height / shareImageSize.height - let smallSettingsWidth = 44.0 * diffFactor - let offset = smallSettingsWidth / 4.0 + transition.updateTransformScale(node: self.moreButton, scale: buttonScaleFactor) + transition.updatePosition(node: self.moreButton, position: CGPoint(x: size.width - buttonScaleFactor * self.intrinsicMoreSize.width / 2.0, y: offsetScaleFactor * 20.0 + buttonScaleFactor * self.intrinsicMoreSize.height / 2.0)) + transition.updateAlpha(node: self.moreButton, alpha: alphaFactor) + transition.updateTransformScale(node: self.actionButton, scale: buttonScaleFactor) + transition.updatePosition(node: self.actionButton, position: CGPoint(x: size.width - buttonScaleFactor * self.intrinsicMoreSize.width - buttonScaleFactor * self.intrinsicActionSize.width / 2.0, y: offsetScaleFactor * 20.0 + buttonScaleFactor * self.intrinsicActionSize.height / 2.0)) + transition.updateAlpha(node: self.actionButton, alpha: alphaFactor) - let spacing = max(4.0, (shareLabelCenter.x - scaledShareSize.width / 2.0) * -1.0 + 4.0) - - let xa = shareLabelCenter.x - scaledShareSize.width / 2.0 - let xb = spacing - (44.0 * shareHeightFactor) / 2.0 - let ya = max(0.0, size.height - 44.0) - let yb = min(size.height, 44.0) / 2.0 - 22.0 - 44.0 / 2.0 - transition.updatePosition(node: self.settingsButton, position: CGPoint(x: xa - xb, y: ya + yb)) - transition.updateTransformScale(node: self.settingsButton, scale: shareHeightFactor) - - transition.updateAlpha(node: self.settingsButton, alpha: alpha) - - transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: 100.0, y: 0.0), size: CGSize(width: size.width - 100.0 - 80.0 - 44.0, height: size.height))) + transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: 64.0, y: 0.0), size: CGSize(width: size.width - 64.0, height: size.height))) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.dimmed { + return nil + } else { + return super.hitTest(point, with: event) + } } } diff --git a/TelegramUI/InstantPageNode.swift b/TelegramUI/InstantPageNode.swift index a765a32968..1b0aeff187 100644 --- a/TelegramUI/InstantPageNode.swift +++ b/TelegramUI/InstantPageNode.swift @@ -3,6 +3,10 @@ import AsyncDisplayKit protocol InstantPageNode { func updateIsVisible(_ isVisible: Bool) + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? + func updateHiddenMedia(media: InstantPageMedia?) + func update(strings: PresentationStrings, theme: InstantPageTheme) } /*@class TGInstantPageMedia; diff --git a/TelegramUI/InstantPagePeerReferenceItem.swift b/TelegramUI/InstantPagePeerReferenceItem.swift new file mode 100644 index 0000000000..4c70086867 --- /dev/null +++ b/TelegramUI/InstantPagePeerReferenceItem.swift @@ -0,0 +1,53 @@ +import Foundation +import Postbox +import TelegramCore + +final class InstantPagePeerReferenceItem: InstantPageItem { + var frame: CGRect + let wantsNode: Bool = true + let medias: [InstantPageMedia] = [] + + let initialPeer: Peer + let rtl: Bool + + init(frame: CGRect, initialPeer: Peer, rtl: Bool) { + self.frame = frame + self.initialPeer = initialPeer + self.rtl = rtl + } + + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + return InstantPagePeerReferenceNode(account: account, strings: strings, theme: theme, initialPeer: self.initialPeer, rtl: self.rtl, openPeer: openPeer) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + if let node = node as? InstantPagePeerReferenceNode { + return self.initialPeer.id == node.initialPeer.id + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 4 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 1000.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func drawInTile(context: CGContext) { + } +} diff --git a/TelegramUI/InstantPagePeerReferenceNode.swift b/TelegramUI/InstantPagePeerReferenceNode.swift new file mode 100644 index 0000000000..0361a548ca --- /dev/null +++ b/TelegramUI/InstantPagePeerReferenceNode.swift @@ -0,0 +1,271 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import AsyncDisplayKit +import Display + +private enum JoinState: Equatable { + case none + case notJoined + case inProgress + case joined(justNow: Bool) + + static func ==(lhs: JoinState, rhs: JoinState) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case .notJoined: + if case .notJoined = rhs { + return true + } else { + return false + } + case .inProgress: + if case .inProgress = rhs { + return true + } else { + return false + } + case let .joined(justNow): + if case .joined(justNow) = rhs { + return true + } else { + return false + } + } + } +} + +final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { + private let account: Account + let initialPeer: Peer + private let rtl: Bool + private var strings: PresentationStrings + private var theme: InstantPageTheme + private let openPeer: (PeerId) -> Void + + private let highlightedBackgroundNode: ASDisplayNode + private let buttonNode: HighlightableButtonNode + private let nameNode: ASTextNode + private let joinNode: HighlightableButtonNode + private let activityIndicator: ActivityIndicator + private let checkNode: ASImageNode + + private var peer: Peer? + private var peerDisposable: Disposable? + + private let joinDisposable = MetaDisposable() + + private var joinState: JoinState = .none + + init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, initialPeer: Peer, rtl: Bool, openPeer: @escaping (PeerId) -> Void) { + self.account = account + self.strings = strings + self.theme = theme + self.initialPeer = initialPeer + self.rtl = rtl + self.openPeer = openPeer + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.buttonNode = HighlightableButtonNode() + + self.nameNode = ASTextNode() + self.nameNode.isLayerBacked = true + self.nameNode.maximumNumberOfLines = 1 + + 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)) + + self.checkNode = ASImageNode() + self.checkNode.isLayerBacked = true + self.checkNode.displayWithoutProcessing = true + self.checkNode.displaysAsynchronously = false + self.checkNode.isHidden = true + + super.init() + + self.backgroundColor = theme.panelBackgroundColor + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.buttonNode) + self.addSubnode(self.joinNode) + self.addSubnode(self.checkNode) + self.addSubnode(self.nameNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + + self.joinNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.joinNode.layer.removeAnimation(forKey: "opacity") + strongSelf.joinNode.alpha = 0.4 + } else { + strongSelf.joinNode.alpha = 1.0 + strongSelf.joinNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.joinNode.addTarget(self, action: #selector(self.joinPressed), forControlEvents: .touchUpInside) + + self.peerDisposable = (actualizedPeer(postbox: self.account.postbox, network: self.account.network, peer: self.initialPeer) |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self { + strongSelf.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: strongSelf.theme.panelPrimaryColor) + if let peer = peer as? TelegramChannel { + var joinState = strongSelf.joinState + if case .member = peer.participationStatus { + switch joinState { + case .none: + joinState = .joined(justNow: false) + case .inProgress, .notJoined: + joinState = .joined(justNow: true) + case .joined: + break + } + } else { + joinState = .notJoined + } + strongSelf.updateJoinState(joinState) + } + strongSelf.setNeedsLayout() + } + }) + + self.applyThemeAndStrings(themeUpdated: true) + } + + deinit { + self.peerDisposable?.dispose() + self.joinDisposable.dispose() + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + if self.strings !== strings || self.theme !== theme { + let themeUpdated = self.theme !== theme + self.strings = strings + self.theme = theme + self.applyThemeAndStrings(themeUpdated: themeUpdated) + } + } + + private func applyThemeAndStrings(themeUpdated: Bool) { + if let peer = self.peer { + self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.panelPrimaryColor) + } + self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: self.theme.panelAccentColor), for: []) + + if themeUpdated { + self.checkNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/PanelCheck"), color: self.theme.panelSecondaryColor) + self.activityIndicator.type = .custom(self.theme.panelAccentColor) + } + self.setNeedsLayout() + } + + private func updateJoinState(_ joinState: JoinState) { + if self.joinState != joinState { + self.joinState = joinState + + switch joinState { + case .none: + self.joinNode.isHidden = true + self.checkNode.isHidden = true + if self.activityIndicator.supernode != nil { + self.activityIndicator.removeFromSupernode() + } + case .notJoined: + self.joinNode.isHidden = false + self.checkNode.isHidden = true + if self.activityIndicator.supernode != nil { + self.activityIndicator.removeFromSupernode() + } + case .inProgress: + self.joinNode.isHidden = true + self.checkNode.isHidden = true + if self.activityIndicator.supernode == nil { + self.addSubnode(self.activityIndicator) + } + case let .joined(justNow): + self.joinNode.isHidden = true + self.checkNode.isHidden = !justNow + if self.activityIndicator.supernode != nil { + self.activityIndicator.removeFromSupernode() + } + } + } + } + + override func layout() { + super.layout() + + let size = self.bounds.size + let inset: CGFloat = 17.0 + + self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + + let joinSize = self.joinNode.measure(size) + let nameSize = self.nameNode.measure(CGSize(width: size.width - inset * 2.0 - joinSize.width, height: size.height)) + let checkSize = self.checkNode.measure(size) + let indicatorSize = self.activityIndicator.measure(size) + + if self.rtl { + self.nameNode.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - nameSize.width, y: floor((size.height - nameSize.height) / 2.0)), size: nameSize) + self.joinNode.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - joinSize.height) / 2.0)), size: joinSize) + self.checkNode.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - checkSize.height) / 2.0)), size: checkSize) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize) + } else { + self.nameNode.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - nameSize.height) / 2.0)), size: nameSize) + self.joinNode.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - joinSize.width, y: floor((size.height - joinSize.height) / 2.0)), size: joinSize) + self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - checkSize.width, y: floor((size.height - checkSize.height) / 2.0)), size: checkSize) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - indicatorSize.width, y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize) + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + } + + func updateIsVisible(_ isVisible: Bool) { + } + + @objc func buttonPressed() { + self.openPeer(self.initialPeer.id) + } + + @objc func joinPressed() { + if case .notJoined = self.joinState { + self.updateJoinState(.inProgress) + self.joinDisposable.set((joinChannel(account: self.account, peerId: self.initialPeer.id) |> deliverOnMainQueue).start(error: { [weak self] _ in + if let strongSelf = self { + if case .inProgress = strongSelf.joinState { + strongSelf.updateJoinState(.notJoined) + } + } + })) + } + } +} diff --git a/TelegramUI/InstantPageMediaItem.swift b/TelegramUI/InstantPagePlayableVideoItem.swift similarity index 54% rename from TelegramUI/InstantPageMediaItem.swift rename to TelegramUI/InstantPagePlayableVideoItem.swift index aa24c324d3..f0acb02b92 100644 --- a/TelegramUI/InstantPageMediaItem.swift +++ b/TelegramUI/InstantPagePlayableVideoItem.swift @@ -1,12 +1,8 @@ import Foundation +import Postbox import TelegramCore -enum InstantPageMediaArguments { - case image(interactive: Bool, roundCorners: Bool, fit: Bool) - case video(interactive: Bool, autoplay: Bool) -} - -final class InstantPageMediaItem: InstantPageItem { +final class InstantPagePlayableVideoItem: InstantPageItem { var frame: CGRect let media: InstantPageMedia @@ -14,19 +10,18 @@ final class InstantPageMediaItem: InstantPageItem { return [self.media] } - let arguments: InstantPageMediaArguments + let interactive: Bool let wantsNode: Bool = true - let hasLinks: Bool = false - init(frame: CGRect, media: InstantPageMedia, arguments: InstantPageMediaArguments) { + init(frame: CGRect, media: InstantPageMedia, interactive: Bool) { self.frame = frame self.media = media - self.arguments = arguments + self.interactive = interactive } - func node(account: Account) -> InstantPageNode? { - return InstantPageMediaNode(account: account, media: self.media, arguments: self.arguments) + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + return InstantPagePlayableVideoNode(account: account, media: self.media, interactive: self.interactive, openMedia: openMedia) } func matchesAnchor(_ anchor: String) -> Bool { @@ -34,7 +29,7 @@ final class InstantPageMediaItem: InstantPageItem { } func matchesNode(_ node: InstantPageNode) -> Bool { - if let node = node as? InstantPageMediaNode { + if let node = node as? InstantPagePlayableVideoNode { return node.media == self.media } else { return false @@ -42,12 +37,12 @@ final class InstantPageMediaItem: InstantPageItem { } func distanceThresholdGroup() -> Int? { - return 1 + return 2 } func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { if count > 3 { - return 400.0 + return 200.0 } else { return CGFloat.greatestFiniteMagnitude } @@ -56,7 +51,8 @@ final class InstantPageMediaItem: InstantPageItem { func drawInTile(context: CGContext) { } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + func linkSelectionRects(at point: CGPoint) -> [CGRect] { return [] } } + diff --git a/TelegramUI/InstantPagePlayableVideoNode.swift b/TelegramUI/InstantPagePlayableVideoNode.swift new file mode 100644 index 0000000000..fb91356a26 --- /dev/null +++ b/TelegramUI/InstantPagePlayableVideoNode.swift @@ -0,0 +1,116 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { + private let account: Account + let media: InstantPageMedia + private let interactive: Bool + private let openMedia: (InstantPageMedia) -> Void + + private let imageNode: TransformImageNode + private let videoNode: ManagedVideoNode + + private var currentSize: CGSize? + + private var fetchedDisposable = MetaDisposable() + + private var localIsVisible = false + + init(account: Account, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { + self.account = account + self.media = media + self.interactive = interactive + self.openMedia = openMedia + + self.imageNode = TransformImageNode() + self.videoNode = ManagedVideoNode(preferSoftwareDecoding: false, backgroundThread: false) + + super.init() + + self.imageNode.alphaTransitionOnFirstUpdate = true + self.addSubnode(self.imageNode) + self.addSubnode(self.videoNode) + + if let file = media.media as? TelegramMediaFile { + self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: file)) + self.fetchedDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start()) + } + } + + deinit { + self.fetchedDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + if self.interactive { + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + } + + func updateIsVisible(_ isVisible: Bool) { + if self.localIsVisible != isVisible { + self.localIsVisible = isVisible + + if isVisible { + if let file = media.media as? TelegramMediaFile { + self.videoNode.acquireContext(account: self.account, mediaManager: account.telegramApplicationContext.mediaManager, id: InstantPageManagedMediaId(media: self.media), resource: file.resource, priority: 0) + } + } else { + self.videoNode.discardContext() + } + } + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + if self.currentSize != size { + self.currentSize = size + + self.imageNode.frame = CGRect(origin: CGPoint(), size: size) + self.videoNode.frame = CGRect(origin: CGPoint(), size: size) + + if let file = self.media.media as? TelegramMediaFile, let dimensions = file.dimensions { + let imageSize = dimensions.aspectFilled(size) + let boundingSize = size + + let makeLayout = self.imageNode.asyncLayout() + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) + let apply = makeLayout(arguments) + apply() + + self.videoNode.transformArguments = arguments + } + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + if media == self.media { + return self.videoNode + } else { + return nil + } + } + + func updateHiddenMedia(media: InstantPageMedia?) { + self.imageNode.isHidden = self.media == media + self.videoNode.isHidden = self.media == media + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.openMedia(self.media) + } + } +} diff --git a/TelegramUI/InstantPagePresentationSettings.swift b/TelegramUI/InstantPagePresentationSettings.swift new file mode 100644 index 0000000000..77a199bf8d --- /dev/null +++ b/TelegramUI/InstantPagePresentationSettings.swift @@ -0,0 +1,102 @@ +import Foundation +import Postbox +import SwiftSignalKit + +enum InstantPageThemeType: Int32 { + case light = 0 + case dark = 1 + case sepia = 2 + case gray = 3 +} + +enum InstantPagePresentationFontSize: Int32 { + case small = 0 + case standard = 1 + case large = 2 + case xlarge = 3 + case xxlarge = 4 +} + +final class InstantPagePresentationSettings: PreferencesEntry, Equatable { + static var defaultSettings = InstantPagePresentationSettings(themeType: .light, fontSize: .standard, forceSerif: false, autoNightMode: true) + + let themeType: InstantPageThemeType + let fontSize: InstantPagePresentationFontSize + let forceSerif: Bool + let autoNightMode: Bool + + init(themeType: InstantPageThemeType, fontSize: InstantPagePresentationFontSize, forceSerif: Bool, autoNightMode: Bool) { + self.themeType = themeType + self.fontSize = fontSize + self.forceSerif = forceSerif + self.autoNightMode = autoNightMode + } + + init(decoder: PostboxDecoder) { + self.themeType = InstantPageThemeType(rawValue: decoder.decodeInt32ForKey("themeType", orElse: 0))! + self.fontSize = InstantPagePresentationFontSize(rawValue: decoder.decodeInt32ForKey("fontSize", orElse: 0))! + self.forceSerif = decoder.decodeInt32ForKey("forceSerif", orElse: 0) != 0 + self.autoNightMode = decoder.decodeInt32ForKey("autoNightMode", orElse: 0) != 0 + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.themeType.rawValue, forKey: "themeType") + encoder.encodeInt32(self.fontSize.rawValue, forKey: "fontSize") + encoder.encodeInt32(self.forceSerif ? 1 : 0, forKey: "forceSerif") + encoder.encodeInt32(self.autoNightMode ? 1 : 0, forKey: "autoNightMode") + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? InstantPagePresentationSettings { + return self == to + } else { + return false + } + } + + static func ==(lhs: InstantPagePresentationSettings, rhs: InstantPagePresentationSettings) -> Bool { + if lhs.themeType != rhs.themeType { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.forceSerif != rhs.forceSerif { + return false + } + if lhs.autoNightMode != rhs.autoNightMode { + return false + } + return true + } + + func withUpdatedThemeType(_ themeType: InstantPageThemeType) -> InstantPagePresentationSettings { + return InstantPagePresentationSettings(themeType: themeType, fontSize: self.fontSize, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode) + } + + func withUpdatedFontSize(_ fontSize: InstantPagePresentationFontSize) -> InstantPagePresentationSettings { + return InstantPagePresentationSettings(themeType: self.themeType, fontSize: fontSize, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode) + } + + func withUpdatedForceSerif(_ forceSerif: Bool) -> InstantPagePresentationSettings { + return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, forceSerif: forceSerif, autoNightMode: self.autoNightMode) + } + + func withUpdatedAutoNightMode(_ autoNightMode: Bool) -> InstantPagePresentationSettings { + return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, forceSerif: self.forceSerif, autoNightMode: autoNightMode) + } +} + +func updateInstantPagePresentationSettingsInteractively(postbox: Postbox, _ f: @escaping (InstantPagePresentationSettings) -> InstantPagePresentationSettings) -> Signal { + return postbox.modify { modifier -> Void in + modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.instantPagePresentationSettings, { entry in + let currentSettings: InstantPagePresentationSettings + if let entry = entry as? InstantPagePresentationSettings { + currentSettings = entry + } else { + currentSettings = InstantPagePresentationSettings.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/InstantPageSettingsBacklightItemNode.swift b/TelegramUI/InstantPageSettingsBacklightItemNode.swift new file mode 100644 index 0000000000..0b5955701a --- /dev/null +++ b/TelegramUI/InstantPageSettingsBacklightItemNode.swift @@ -0,0 +1,76 @@ +import Foundation +import AsyncDisplayKit +import Display + +import LegacyComponents + +private func generateKnobImage() -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) +} + +final class InstantPageSettingsBacklightItemNode: InstantPageSettingsItemNode { + private let sliderView: TGPhotoEditorSliderView + private let leftIconNode: ASImageNode + private let rightIconNode: ASImageNode + + init(theme: InstantPageSettingsItemTheme) { + self.sliderView = TGPhotoEditorSliderView() + self.sliderView.trackCornerRadius = 1.0 + self.sliderView.lineSize = 2.0 + self.sliderView.minimumValue = 0.0 + self.sliderView.startValue = 0.0 + self.sliderView.maximumValue = 100.0 + self.sliderView.disablesInteractiveTransitionGestureRecognizer = true + + self.leftIconNode = ASImageNode() + self.leftIconNode.displaysAsynchronously = false + self.leftIconNode.displayWithoutProcessing = true + + self.rightIconNode = ASImageNode() + self.rightIconNode.displaysAsynchronously = false + self.rightIconNode.displayWithoutProcessing = true + + super.init(theme: theme, selectable: false) + + self.updateTheme(theme) + + self.sliderView.value = UIScreen.main.brightness * 100.0 + self.sliderView.addTarget(self, action: #selector(self.sliderChanged), for: .valueChanged) + self.view.addSubview(self.sliderView) + + self.addSubnode(self.leftIconNode) + self.addSubnode(self.rightIconNode) + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + + self.sliderView.backgroundColor = theme.itemBackgroundColor + self.sliderView.backColor = theme.secondaryColor + self.sliderView.trackColor = theme.accentColor + self.sliderView.knobImage = generateKnobImage() + + self.leftIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMinIcon"), color: theme.primaryColor) + self.rightIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMaxIcon"), color: theme.primaryColor) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + self.sliderView.frame = CGRect(origin: CGPoint(x: 38.0, y: 8.0), size: CGSize(width: width - 38.0 * 2.0, height: 44.0)) + if let image = self.leftIconNode.image { + self.leftIconNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 24.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + if let image = self.rightIconNode.image { + self.rightIconNode.frame = CGRect(origin: CGPoint(x: width - 13.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + return (62.0 + insets.top + insets.bottom, nil) + } + + @objc func sliderChanged() { + UIScreen.main.brightness = self.sliderView.value / 100.0 + } +} diff --git a/TelegramUI/InstantPageSettingsFontFamilyItemNode.swift b/TelegramUI/InstantPageSettingsFontFamilyItemNode.swift new file mode 100644 index 0000000000..fc3a80d5e4 --- /dev/null +++ b/TelegramUI/InstantPageSettingsFontFamilyItemNode.swift @@ -0,0 +1,89 @@ +import Foundation +import AsyncDisplayKit +import Display + +private func generateCheckIcon(_ color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(2.0) + context.move(to: CGPoint(x: 12.0, y: 1.0)) + context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) + context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) + context.strokePath() + }) +} + +final class InstantPageSettingsFontFamilyNode: InstantPageSettingsItemNode { + private let title: String + private let family: String? + private let tapped: () -> Void + + private let labelNode: ASTextNode + private let checkNode: ASImageNode + + var _checked: Bool + var checked: Bool { + get { + return self._checked + } set(value) { + self._checked = value + self.checkNode.isHidden = !value + } + } + + init(theme: InstantPageSettingsItemTheme, title: String, family: String?, checked: Bool, tapped: @escaping () -> Void) { + self.title = title + self.family = family + self._checked = checked + self.tapped = tapped + + self.labelNode = ASTextNode() + + self.checkNode = ASImageNode() + self.checkNode.displayWithoutProcessing = true + self.checkNode.displaysAsynchronously = false + self.checkNode.isHidden = !checked + + super.init(theme: theme, selectable: true) + + self.addSubnode(self.labelNode) + self.addSubnode(self.checkNode) + + self.updateTheme(theme) + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + + let font: UIFont + if let family = self.family { + if let familyFont = UIFont(name: family, size: 17.0) { + font = familyFont + } else { + font = UIFont.systemFont(ofSize: 17.0) + } + } else { + font = UIFont.systemFont(ofSize: 17.0) + } + self.labelNode.attributedText = NSAttributedString(string: self.title, font: font, textColor: theme.primaryColor) + self.checkNode.image = generateCheckIcon(theme.accentColor) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + var separatorInset: CGFloat? + if case .sameSection = previousItem.0, let previousNode = previousItem.1, previousNode is InstantPageSettingsFontFamilyNode { + separatorInset = 46.0 + } + let labelSize = self.labelNode.measure(CGSize(width: width - 46.0 - 5.0, height: 44.0)) + self.labelNode.frame = CGRect(origin: CGPoint(x: 46.0, y: insets.top + floor((44.0 - labelSize.height) / 2.0)), size: labelSize) + if let image = self.checkNode.image { + self.checkNode.frame = CGRect(origin: CGPoint(x: 16.0, y: insets.top + floor((44.0 - image.size.height) / 2.0)), size: image.size) + } + return (44.0 + insets.top + insets.bottom, separatorInset) + } + + override func pressed() { + self.tapped() + } +} diff --git a/TelegramUI/InstantPageSettingsFontSizeItemNode.swift b/TelegramUI/InstantPageSettingsFontSizeItemNode.swift new file mode 100644 index 0000000000..86307d881f --- /dev/null +++ b/TelegramUI/InstantPageSettingsFontSizeItemNode.swift @@ -0,0 +1,82 @@ +import Foundation +import AsyncDisplayKit +import Display + +import LegacyComponents + +private func generateKnobImage() -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) +} + +final class InstantPageSettingsFontSizeItemNode: InstantPageSettingsItemNode { + private let updated: (Int) -> Void + + private let sliderView: TGPhotoEditorSliderView + private let leftIconNode: ASImageNode + private let rightIconNode: ASImageNode + + init(theme: InstantPageSettingsItemTheme, fontSizeVariant: Int, updated: @escaping (Int) -> Void) { + self.updated = updated + + self.sliderView = TGPhotoEditorSliderView() + self.sliderView.trackCornerRadius = 1.0 + self.sliderView.lineSize = 2.0 + self.sliderView.dotSize = 5.0 + self.sliderView.minimumValue = 0.0 + self.sliderView.maximumValue = 4.0 + self.sliderView.startValue = 0.0 + self.sliderView.value = CGFloat(fontSizeVariant) + self.sliderView.positionsCount = 5 + self.sliderView.disablesInteractiveTransitionGestureRecognizer = true + + self.leftIconNode = ASImageNode() + self.leftIconNode.displaysAsynchronously = false + self.leftIconNode.displayWithoutProcessing = true + + self.rightIconNode = ASImageNode() + self.rightIconNode.displaysAsynchronously = false + self.rightIconNode.displayWithoutProcessing = true + + super.init(theme: theme, selectable: false) + + self.updateTheme(theme) + + self.sliderView.addTarget(self, action: #selector(self.sliderChanged), for: .valueChanged) + self.view.addSubview(self.sliderView) + + self.addSubnode(self.leftIconNode) + self.addSubnode(self.rightIconNode) + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + + self.sliderView.backgroundColor = theme.itemBackgroundColor + self.sliderView.backColor = theme.secondaryColor + self.sliderView.trackColor = theme.accentColor + self.sliderView.knobImage = generateKnobImage() + + self.leftIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMinIcon"), color: theme.primaryColor) + self.rightIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMaxIcon"), color: theme.primaryColor) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + self.sliderView.frame = CGRect(origin: CGPoint(x: 38.0, y: 8.0), size: CGSize(width: width - 38.0 * 2.0, height: 44.0)) + if let image = self.leftIconNode.image { + self.leftIconNode.frame = CGRect(origin: CGPoint(x: 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + if let image = self.rightIconNode.image { + self.rightIconNode.frame = CGRect(origin: CGPoint(x: width - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + return (62.0 + insets.top + insets.bottom, nil) + } + + @objc func sliderChanged() { + self.updated(max(0, min(4, Int(self.sliderView.value)))) + } +} diff --git a/TelegramUI/InstantPageSettingsItemNode.swift b/TelegramUI/InstantPageSettingsItemNode.swift new file mode 100644 index 0000000000..c191276679 --- /dev/null +++ b/TelegramUI/InstantPageSettingsItemNode.swift @@ -0,0 +1,125 @@ +import Foundation +import AsyncDisplayKit +import Display + +enum InstantPageSettingsItemNodeStatus { + case none + case sameSection + case otherSection +} + +class InstantPageSettingsItemNode: ASDisplayNode { + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode? + private let highlightButtonNode: HighlightTrackingButtonNode? + + init(theme: InstantPageSettingsItemTheme, selectable: Bool) { + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + self.topSeparatorNode.isHidden = true + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + self.bottomSeparatorNode.isHidden = true + + if selectable { + let highlightedBackgroundNode = ASDisplayNode() + highlightedBackgroundNode.isLayerBacked = true + highlightedBackgroundNode.alpha = 0.0 + self.highlightedBackgroundNode = highlightedBackgroundNode + self.highlightButtonNode = HighlightTrackingButtonNode() + } else { + self.highlightedBackgroundNode = nil + self.highlightButtonNode = nil + } + + super.init() + + self.backgroundColor = theme.itemBackgroundColor + + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + if let highlightedBackgroundNode = self.highlightedBackgroundNode { + self.addSubnode(highlightedBackgroundNode) + } + if let highlightButtonNode = self.highlightButtonNode { + self.addSubnode(highlightButtonNode) + highlightButtonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + highlightButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self, let highlightedBackgroundNode = strongSelf.highlightedBackgroundNode { + if highlighted { + strongSelf.supernode?.view.bringSubview(toFront: strongSelf.view) + highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + highlightedBackgroundNode.alpha = 1.0 + } else { + highlightedBackgroundNode.alpha = 0.0 + highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + } + + self.updateTheme(theme) + } + + func updateTheme(_ theme: InstantPageSettingsItemTheme) { + self.backgroundColor = theme.itemBackgroundColor + self.highlightedBackgroundNode?.backgroundColor = theme.itemHighlightedBackgroundColor + self.topSeparatorNode.backgroundColor = theme.separatorColor + self.bottomSeparatorNode.backgroundColor = theme.separatorColor + } + + func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + return (44.0 + insets.top + insets.bottom, nil) + } + + final func updateLayout(width: CGFloat, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> CGFloat { + let separatorHeight = UIScreenPixel + + let separatorInset: CGFloat = 0.0 + var highlightExtension: CGFloat = 0.0 + switch previousItem.0 { + case .none: + self.topSeparatorNode.isHidden = true + case .sameSection: + self.topSeparatorNode.isHidden = false + case .otherSection: + self.topSeparatorNode.isHidden = false + } + + switch nextItem.0 { + case .none: + self.bottomSeparatorNode.isHidden = true + case .sameSection: + self.bottomSeparatorNode.isHidden = true + highlightExtension = separatorHeight + case .otherSection: + self.bottomSeparatorNode.isHidden = false + } + + let (internalHeight, internalSeparatorInset) = self.updateInternalLayout(width: width, insets: UIEdgeInsets(top: self.topSeparatorNode.isHidden ? 0.0 : separatorHeight, left: 0.0, bottom: self.bottomSeparatorNode.isHidden ? 0.0 : separatorHeight, right: 0.0), previousItem: previousItem, nextItem: nextItem) + + let finalSeparatorInset = internalSeparatorInset ?? separatorInset + + self.topSeparatorNode.frame = CGRect(origin: CGPoint(x: finalSeparatorInset, y: 0.0), size: CGSize(width: width - finalSeparatorInset, height: separatorHeight)) + + self.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: internalHeight - separatorHeight), size: CGSize(width: width, height: separatorHeight)) + + if let highlightButtonNode = self.highlightButtonNode { + highlightButtonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -highlightExtension), size: CGSize(width: width, height: internalHeight + highlightExtension)) + } + if let highlightedBackgroundNode = self.highlightedBackgroundNode { + highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: internalHeight + highlightExtension)) + } + + return internalHeight + } + + @objc func buttonPressed() { + self.pressed() + } + + func pressed() { + } +} diff --git a/TelegramUI/InstantPageSettingsItemTheme.swift b/TelegramUI/InstantPageSettingsItemTheme.swift new file mode 100644 index 0000000000..7aec03baf8 --- /dev/null +++ b/TelegramUI/InstantPageSettingsItemTheme.swift @@ -0,0 +1,101 @@ +import Foundation +import UIKit +import Display + +final class InstantPageSettingsItemTheme: Equatable { + let listBackgroundColor: UIColor + let itemBackgroundColor: UIColor + let itemHighlightedBackgroundColor: UIColor + let separatorColor: UIColor + let primaryColor: UIColor + let secondaryColor: UIColor + let accentColor: UIColor + + init(listBackgroundColor: UIColor, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, separatorColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor) { + self.listBackgroundColor = listBackgroundColor + self.itemBackgroundColor = itemBackgroundColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.separatorColor = separatorColor + self.primaryColor = primaryColor + self.secondaryColor = secondaryColor + self.accentColor = accentColor + } + + static func ==(lhs: InstantPageSettingsItemTheme, rhs: InstantPageSettingsItemTheme) -> Bool { + if !lhs.listBackgroundColor.isEqual(rhs.listBackgroundColor) { + return false + } + if !lhs.itemBackgroundColor.isEqual(rhs.itemBackgroundColor) { + return false + } + if !lhs.itemHighlightedBackgroundColor.isEqual(rhs.itemHighlightedBackgroundColor) { + return false + } + if !lhs.separatorColor.isEqual(rhs.separatorColor) { + return false + } + if !lhs.primaryColor.isEqual(rhs.primaryColor) { + return false + } + if !lhs.secondaryColor.isEqual(rhs.secondaryColor) { + return false + } + if !lhs.accentColor.isEqual(rhs.accentColor) { + return false + } + return true + } + + static func themeFor(_ settings: InstantPagePresentationSettings) -> InstantPageSettingsItemTheme { + switch settings.themeType { + case .light: + return lightTheme + case .sepia: + return sepiaTheme + case .gray: + return grayTheme + case .dark: + return darkTheme + } + } +} + +private let lightTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0xefeff4), + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + separatorColor: UIColor(rgb: 0xc8c7cc), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0xa8a8a8), + accentColor: UIColor(rgb: 0x007ee5) +) + +private let sepiaTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0xefeff4), + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + separatorColor: UIColor(rgb: 0xc8c7cc), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0xb7b7b7), + accentColor: UIColor(rgb: 0xb06900) +) + +private let grayTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0xefeff4), + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + separatorColor: UIColor(rgb: 0xc8c7cc), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0xb6b6b6), + accentColor: UIColor(rgb: 0xc7c7c7) +) + +private let darkTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0x232323), + itemBackgroundColor: UIColor(rgb: 0x1a1a1a), + itemHighlightedBackgroundColor: UIColor(rgb: 0x4c4c4c), + separatorColor: UIColor(rgb: 0x151515), + primaryColor: UIColor(rgb: 0x878787), + secondaryColor: UIColor(rgb: 0xa6a6a6), + accentColor: UIColor(rgb: 0xbfc0c2) +) diff --git a/TelegramUI/InstantPageSettingsNode.swift b/TelegramUI/InstantPageSettingsNode.swift new file mode 100644 index 0000000000..34427ca7bc --- /dev/null +++ b/TelegramUI/InstantPageSettingsNode.swift @@ -0,0 +1,241 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import SwiftSignalKit + +private func generateArrowImage(color: UIColor) -> UIImage? { + let smallRadius: CGFloat = 5.0 + let largeRadius: CGFloat = 14.0 + return generateImage(CGSize(width: smallRadius + largeRadius, height: smallRadius + largeRadius + 16.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsArrow"), color: color), let cgImage = image.cgImage { + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - image.size.height - 16.0), size: CGSize(width: size.width, height: 16.0))) + context.draw(cgImage, in: CGRect(origin: CGPoint(x: size.width - image.size.width, y: size.height - image.size.height), size: image.size)) + } + }) +} + +final class InstantPageSettingsNode: ASDisplayNode { + private var settings: InstantPagePresentationSettings + private var theme: InstantPageSettingsItemTheme + + private let applySettings: (InstantPagePresentationSettings) -> Void + + private var sections: [[InstantPageSettingsItemNode]] = [] + private let sansFamilyNode: InstantPageSettingsFontFamilyNode + private let serifFamilyNode: InstantPageSettingsFontFamilyNode + private let themeItemNode: InstantPageSettingsThemeItemNode + private let autoNightItemNode: InstantPageSettingsSwitchNode + + private let arrowNode: ASImageNode + private let itemContainerNode: ASDisplayNode + + init(strings: PresentationStrings, settings: InstantPagePresentationSettings, applySettings: @escaping (InstantPagePresentationSettings) -> Void) { + self.settings = settings + self.theme = InstantPageSettingsItemTheme.themeFor(settings) + + self.applySettings = applySettings + + self.arrowNode = ASImageNode() + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.image = generateArrowImage(color: self.theme.itemBackgroundColor) + + self.itemContainerNode = ASDisplayNode() + self.itemContainerNode.layer.masksToBounds = true + self.itemContainerNode.layer.cornerRadius = 16.0 + self.itemContainerNode.backgroundColor = self.theme.listBackgroundColor + + var updateSerifImpl: ((Bool) -> Void)? + var updateThemeTypeImpl: ((InstantPageThemeType) -> Void)? + var updateAutoNightImpl: ((Bool) -> Void)? + + self.sansFamilyNode = InstantPageSettingsFontFamilyNode(theme: self.theme, title: "San Francisco", family: nil, checked: !settings.forceSerif, tapped: { + updateSerifImpl?(false) + }) + self.serifFamilyNode = InstantPageSettingsFontFamilyNode(theme: self.theme, title: "Georgia", family: "Georgia", checked: settings.forceSerif, tapped: { + updateSerifImpl?(true) + }) + self.themeItemNode = InstantPageSettingsThemeItemNode(theme: theme, themeType: settings.themeType, update: { value in + updateThemeTypeImpl?(value) + }) + self.autoNightItemNode = InstantPageSettingsSwitchNode(theme: theme, title: strings.InstantPage_AutoNightTheme, isOn: settings.autoNightMode, isEnabled: settings.themeType != .dark, toggled: { value in + updateAutoNightImpl?(value) + }) + + super.init() + + self.addSubnode(self.arrowNode) + self.addSubnode(self.itemContainerNode) + + self.sections = [ + [ + InstantPageSettingsBacklightItemNode(theme: self.theme) + ], + [ + InstantPageSettingsFontSizeItemNode(theme: self.theme, fontSizeVariant: Int(settings.fontSize.rawValue), updated: { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + let size: InstantPagePresentationFontSize = InstantPagePresentationFontSize(rawValue: Int32(value)) ?? .standard + return $0.withUpdatedFontSize(size) + } + } + }), + self.sansFamilyNode, + self.serifFamilyNode + ], + [ + self.themeItemNode, + self.autoNightItemNode + ] + ] + + for section in self.sections { + for item in section { + self.itemContainerNode.addSubnode(item) + } + } + + updateSerifImpl = { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + return $0.withUpdatedForceSerif(value) + } + } + } + + updateThemeTypeImpl = { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + return $0.withUpdatedThemeType(value) + } + } + } + + updateAutoNightImpl = { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + return $0.withUpdatedAutoNightMode(value) + } + } + } + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let fixedWidth: CGFloat = 295.0 + let sectionSpacing: CGFloat = 4.0 + let sideInset: CGFloat = 11.0 + let topInset: CGFloat = layout.insets(options: [.statusBar]).top + 44.0 + 6.0 + + var contentHeight: CGFloat = 0.0 + var itemSizes: [[CGFloat]] = [] + for sectionIndex in 0 ..< self.sections.count { + itemSizes.append([]) + if sectionIndex != 0 { + contentHeight += sectionSpacing + } + for itemIndex in 0 ..< self.sections[sectionIndex].count { + let previousItem: InstantPageSettingsItemNodeStatus + var previousItemNode: InstantPageSettingsItemNode? + let nextItem: InstantPageSettingsItemNodeStatus + var nextItemNode: InstantPageSettingsItemNode? + if itemIndex == 0 { + if sectionIndex == 0 { + previousItem = .none + } else { + previousItem = .otherSection + } + } else { + previousItem = .sameSection + previousItemNode = self.sections[sectionIndex][itemIndex - 1] + } + if itemIndex == self.sections[sectionIndex].count - 1 { + if sectionIndex == self.sections.count - 1 { + nextItem = .none + } else { + nextItem = .otherSection + } + } else { + nextItem = .sameSection + nextItemNode = self.sections[sectionIndex][itemIndex + 1] + } + let itemHeight = self.sections[sectionIndex][itemIndex].updateLayout(width: fixedWidth, previousItem: (previousItem, previousItemNode), nextItem: (nextItem, nextItemNode)) + itemSizes[sectionIndex].append(itemHeight) + contentHeight += itemHeight + } + } + + if let image = self.arrowNode.image { + transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width, y: topInset - image.size.height + 16.0 + 8.0), size: image.size)) + } + + transition.updateFrame(node: self.itemContainerNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideInset - fixedWidth, y: topInset), size: CGSize(width: fixedWidth, height: contentHeight))) + var nextItemOffset: CGFloat = 0.0 + for sectionIndex in 0 ..< self.sections.count { + if sectionIndex != 0 { + nextItemOffset += sectionSpacing + } + for itemIndex in 0 ..< self.sections[sectionIndex].count { + let itemHeight = itemSizes[sectionIndex][itemIndex] + transition.updateFrame(node: self.sections[sectionIndex][itemIndex], frame: CGRect(origin: CGPoint(x: 0.0, y: nextItemOffset), size: CGSize(width: fixedWidth, height: itemHeight))) + nextItemOffset += itemHeight + } + } + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + private func updateSettings(_ f: (InstantPagePresentationSettings) -> InstantPagePresentationSettings) { + let updated = f(self.settings) + if updated != self.settings { + self.settings = updated + + self.sansFamilyNode.checked = !self.settings.forceSerif + self.serifFamilyNode.checked = self.settings.forceSerif + self.themeItemNode.themeType = self.settings.themeType + self.autoNightItemNode.isEnabled = self.settings.themeType != .dark + + let theme = InstantPageSettingsItemTheme.themeFor(self.settings) + if theme != self.theme { + self.theme = theme + + if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + self.arrowNode.image = generateArrowImage(color: self.theme.itemBackgroundColor) + self.itemContainerNode.backgroundColor = self.theme.listBackgroundColor + for section in self.sections { + for item in section { + item.updateTheme(self.theme) + } + } + + + } + + self.applySettings(settings) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.itemContainerNode.frame.contains(point) { + return super.hitTest(point, with: event) + } else { + return nil + } + } +} diff --git a/TelegramUI/InstantPageSettingsSwitchItemNode.swift b/TelegramUI/InstantPageSettingsSwitchItemNode.swift new file mode 100644 index 0000000000..c345c8ec91 --- /dev/null +++ b/TelegramUI/InstantPageSettingsSwitchItemNode.swift @@ -0,0 +1,86 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class InstantPageSettingsSwitchNode: InstantPageSettingsItemNode { + private let title: String + private let toggled: (Bool) -> Void + + private let labelNode: ASTextNode + private let switchNode: SwitchNode + + var isOn: Bool { + didSet { + if self.isEnabled && self.isOn != self.switchNode.isOn { + self.switchNode.setOn(self.isOn, animated: true) + } + } + } + + var isEnabled: Bool { + didSet { + if self.isEnabled { + self.switchNode.setOn(self.isOn, animated: true) + self.switchNode.allowsGroupOpacity = false + self.switchNode.alpha = 1.0 + } else { + self.switchNode.setOn(false, animated: true) + self.switchNode.allowsGroupOpacity = true + self.switchNode.alpha = 0.6 + } + self.switchNode.isUserInteractionEnabled = self.isEnabled + } + } + + init(theme: InstantPageSettingsItemTheme, title: String, isOn: Bool, isEnabled: Bool, toggled: @escaping (Bool) -> Void) { + self.title = title + self.toggled = toggled + + self.labelNode = ASTextNode() + + self.switchNode = SwitchNode() + if isEnabled { + self.switchNode.isOn = isOn + } else { + self.switchNode.isOn = false + self.switchNode.allowsGroupOpacity = true + self.switchNode.alpha = 0.6 + } + + self.isOn = isOn + self.isEnabled = isEnabled + + super.init(theme: theme, selectable: false) + + self.addSubnode(self.labelNode) + self.addSubnode(self.switchNode) + + self.switchNode.valueUpdated = { [weak self] value in + if let strongSelf = self { + strongSelf.isOn = value + toggled(value) + } + } + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + + self.labelNode.attributedText = NSAttributedString(string: self.title, font: Font.regular(17.0), textColor: theme.primaryColor) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + + let labelSize = self.labelNode.measure(CGSize(width: width - 46.0 - 5.0, height: 44.0)) + self.labelNode.frame = CGRect(origin: CGPoint(x: 15.0, y: insets.top + floor((44.0 - labelSize.height) / 2.0)), size: labelSize) + if let switchView = self.switchNode.view as? UISwitch { + if self.switchNode.bounds.size.width.isZero { + switchView.sizeToFit() + } + let switchSize = switchView.bounds.size + + self.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0, y: insets.top + 6.0), size: switchSize) + } + return (44.0 + insets.top + insets.bottom, nil) + } +} diff --git a/TelegramUI/InstantPageSettingsThemeItemNode.swift b/TelegramUI/InstantPageSettingsThemeItemNode.swift new file mode 100644 index 0000000000..a2f32fd93c --- /dev/null +++ b/TelegramUI/InstantPageSettingsThemeItemNode.swift @@ -0,0 +1,167 @@ +import Foundation +import AsyncDisplayKit +import Display + +private final class InstantPageSettingsThemeSelectorNode: ASDisplayNode { + private let selectionNode: ASImageNode + private let colorNode: ASImageNode + + private let color: UIColor + + var selected: Bool = false { + didSet { + self.selectionNode.isHidden = !self.selected + } + } + + var selectionColor: UIColor { + didSet { + if !self.selectionColor.isEqual(oldValue) { + self.selectionNode.image = generateFilledCircleImage(diameter: 46.0, color: nil, strokeColor: self.selectionColor, strokeWidth: 2.0, backgroundColor: nil) + } + } + } + + var edgeColor: UIColor { + didSet { + if !self.edgeColor.isEqual(oldValue) { + self.colorNode.image = generateFilledCircleImage(diameter: 46.0, color: self.color, strokeColor: self.edgeColor, strokeWidth: 1.0, backgroundColor: nil) + } + } + } + + init(color: UIColor, edgeColor: UIColor, selectionColor: UIColor) { + self.color = color + self.edgeColor = edgeColor + self.selectionColor = selectionColor + + self.selectionNode = ASImageNode() + self.selectionNode.isLayerBacked = true + self.selectionNode.displayWithoutProcessing = true + self.selectionNode.displaysAsynchronously = false + self.selectionNode.image = generateFilledCircleImage(diameter: 46.0, color: nil, strokeColor: self.selectionColor, strokeWidth: 2.0, backgroundColor: nil) + self.selectionNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 46.0, height: 46.0)) + + self.colorNode = ASImageNode() + self.colorNode.isLayerBacked = true + self.colorNode.displayWithoutProcessing = true + self.colorNode.displaysAsynchronously = false + self.colorNode.image = generateFilledCircleImage(diameter: 46.0, color: self.color, strokeColor: self.edgeColor, strokeWidth: 1.0, backgroundColor: nil) + self.colorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 46.0, height: 46.0)) + + super.init() + + self.addSubnode(self.colorNode) + self.addSubnode(self.selectionNode) + } +} + +final class InstantPageSettingsThemeItemNode: InstantPageSettingsItemNode { + private let update: (InstantPageThemeType) -> Void + + private let themeNodes: [InstantPageSettingsThemeSelectorNode] + + var themeType: InstantPageThemeType { + didSet { + let selectedIndex: Int + switch self.themeType { + case .light: + selectedIndex = 0 + case .sepia: + selectedIndex = 1 + case .gray: + selectedIndex = 2 + case .dark: + selectedIndex = 3 + } + + self.themeNodes[0].edgeColor = (selectedIndex == 1 || selectedIndex == 2) ? UIColor.lightGray : UIColor.white + + for i in 0 ..< self.themeNodes.count { + self.themeNodes[i].selected = i == selectedIndex + } + } + } + + init(theme: InstantPageSettingsItemTheme, themeType: InstantPageThemeType, update: @escaping (InstantPageThemeType) -> Void) { + self.themeType = themeType + self.update = update + + let selectedIndex: Int + switch themeType { + case .light: + selectedIndex = 0 + case .sepia: + selectedIndex = 1 + case .gray: + selectedIndex = 2 + case .dark: + selectedIndex = 3 + } + + let selectionColor = UIColor(rgb: 0x007ee5) + self.themeNodes = [ + InstantPageSettingsThemeSelectorNode(color: .white, edgeColor: (selectedIndex == 1 || selectedIndex == 2) ? UIColor.lightGray : UIColor.white, selectionColor: selectionColor), + InstantPageSettingsThemeSelectorNode(color: UIColor(rgb: 0xcbb98e), edgeColor: UIColor(rgb: 0xcbb98e), selectionColor: selectionColor), + InstantPageSettingsThemeSelectorNode(color: UIColor(rgb: 0x48484a), edgeColor: UIColor(rgb: 0x48484a), selectionColor: selectionColor), + InstantPageSettingsThemeSelectorNode(color: UIColor(rgb: 0x48484a), edgeColor: UIColor(rgb: 0x48484a), selectionColor: selectionColor) + ] + + super.init(theme: theme, selectable: false) + + for i in 0 ..< self.themeNodes.count { + self.themeNodes[i].selected = i == selectedIndex + self.addSubnode(self.themeNodes[i]) + } + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + + let sideInset: CGFloat = 26.0 + let topInset: CGFloat = 12.0 + let itemSize = CGSize(width: 46.0, height: 46.0) + let spacing: CGFloat = floor((width - CGFloat(self.themeNodes.count) * itemSize.width - sideInset * 2.0) / CGFloat(self.themeNodes.count - 1)) + + for i in 0 ..< self.themeNodes.count { + self.themeNodes[i].frame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + spacing), y: insets.top + topInset), size: itemSize) + } + + return (70.0 + insets.top + insets.bottom, nil) + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let location = recognizer.location(in: self.view) + for i in 0 ..< self.themeNodes.count { + if self.themeNodes[i].frame.contains(location) { + let themeType: InstantPageThemeType + switch i { + case 0: + themeType = .light + case 1: + themeType = .sepia + case 2: + themeType = .gray + case 3: + themeType = .dark + default: + themeType = .light + } + self.update(themeType) + break + } + } + } + } +} + diff --git a/TelegramUI/InstantPageShapeItem.swift b/TelegramUI/InstantPageShapeItem.swift index 472534a801..0fb3e40477 100644 --- a/TelegramUI/InstantPageShapeItem.swift +++ b/TelegramUI/InstantPageShapeItem.swift @@ -1,4 +1,5 @@ import Foundation +import Postbox import TelegramCore enum InstantPageShape { @@ -15,7 +16,6 @@ final class InstantPageShapeItem: InstantPageItem { let medias: [InstantPageMedia] = [] let wantsNode: Bool = false - let hasLinks: Bool = false init(frame: CGRect, shapeFrame: CGRect, shape: InstantPageShape, color: UIColor) { self.frame = frame @@ -55,11 +55,11 @@ final class InstantPageShapeItem: InstantPageItem { return false } - func node(account: Account) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { return nil } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + func linkSelectionRects(at point: CGPoint) -> [CGRect] { return [] } diff --git a/TelegramUI/InstantPageSlideshowItem.swift b/TelegramUI/InstantPageSlideshowItem.swift new file mode 100644 index 0000000000..4c0937b49d --- /dev/null +++ b/TelegramUI/InstantPageSlideshowItem.swift @@ -0,0 +1,50 @@ +import Foundation +import Postbox +import TelegramCore + +final class InstantPageSlideshowItem: InstantPageItem { + var frame: CGRect + let wantsNode: Bool = true + let medias: [InstantPageMedia] + + init(frame: CGRect, medias: [InstantPageMedia]) { + self.frame = frame + self.medias = medias + } + + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + return InstantPageSlideshowNode(account: account, medias: self.medias, openMedia: openMedia) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + if let node = node as? InstantPageSlideshowNode { + return self.medias == node.medias + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 3 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 1000.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func drawInTile(context: CGContext) { + } +} + diff --git a/TelegramUI/InstantPageSlideshowItemNode.swift b/TelegramUI/InstantPageSlideshowItemNode.swift new file mode 100644 index 0000000000..c2ef6dbea4 --- /dev/null +++ b/TelegramUI/InstantPageSlideshowItemNode.swift @@ -0,0 +1,417 @@ +import Foundation +import TelegramCore +import AsyncDisplayKit +import Display + +private final class InstantPageSlideshowItemNode: ASDisplayNode { + private var _index: Int? + var index: Int { + get { + return self._index! + } set(value) { + self._index = value + } + } + private let contentNode: ASDisplayNode + + var internalIsVisible: Bool = false { + didSet { + if self.internalParentVisible && oldValue != self.internalIsVisible && self.internalParentVisible { + (self.contentNode as? InstantPageNode)?.updateIsVisible(self.internalIsVisible && self.internalParentVisible) + } + } + } + + var internalParentVisible: Bool = false { + didSet { + if self.internalIsVisible && oldValue != self.internalIsVisible && self.internalParentVisible { + (self.contentNode as? InstantPageNode)?.updateIsVisible(self.internalIsVisible && self.internalParentVisible) + } + } + } + + init(contentNode: ASDisplayNode) { + self.contentNode = contentNode + + super.init() + + self.addSubnode(self.contentNode) + } + + override func layout() { + super.layout() + + self.contentNode.frame = self.bounds + } + + func updateHiddenMedia(_ media: InstantPageMedia?) { + if let node = self.contentNode as? InstantPageNode { + node.updateHiddenMedia(media: media) + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + if let node = self.contentNode as? InstantPageNode { + return node.transitionNode(media: media) + } + return nil + } +} + +private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDelegate { + private let account: Account + private let openMedia: (InstantPageMedia) -> Void + private let pageGap: CGFloat + + private let scrollView: UIScrollView + + private var items: [InstantPageMedia] = [] + private var itemNodes: [InstantPageSlideshowItemNode] = [] + private var ignoreCentralItemIndexUpdate = false + private var centralItemIndex: Int? { + didSet { + if oldValue != self.centralItemIndex && !self.ignoreCentralItemIndexUpdate { + //self.centralItemIndexUpdated(self.centralItemIndex) + } + } + } + + private var containerLayout: ContainerViewLayout? + + var centralItemIndexUpdated: (Int?) -> Void = { _ in } + + var internalIsVisible: Bool = false { + didSet { + if self.internalIsVisible != oldValue { + for node in self.itemNodes { + node.internalParentVisible = self.internalIsVisible + } + } + } + } + + init(account: Account, openMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { + self.account = account + self.openMedia = openMedia + self.pageGap = pageGap + self.scrollView = UIScrollView() + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + + super.init() + + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = !pageGap.isZero + self.scrollView.bounces = !pageGap.isZero + self.scrollView.isPagingEnabled = true + self.scrollView.delegate = self + self.scrollView.clipsToBounds = false + self.scrollView.scrollsToTop = false + self.view.addSubview(self.scrollView) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.containerLayout = layout + + var previousCentralNodeHorizontalOffset: CGFloat? + if let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) { + previousCentralNodeHorizontalOffset = self.scrollView.contentOffset.x - centralNode.frame.minX + } + + self.scrollView.frame = CGRect(origin: CGPoint(x: -self.pageGap, y: 0.0), size: CGSize(width: layout.size.width + self.pageGap * 2.0, height: layout.size.height)) + + for i in 0 ..< self.itemNodes.count { + self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + //self.itemNodes[i].containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + if let previousCentralNodeHorizontalOffset = previousCentralNodeHorizontalOffset, let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) { + self.scrollView.contentOffset = CGPoint(x: centralNode.frame.minX + previousCentralNodeHorizontalOffset, y: 0.0) + } + + self.updateItemNodes() + } + + func centralItemNode() -> InstantPageSlideshowItemNode? { + if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { + return centralItemNode + } else { + return nil + } + } + + func replaceItems(_ items: [InstantPageMedia], centralItemIndex: Int?, keepFirst: Bool = false) { + var keptItemNode: InstantPageSlideshowItemNode? + for itemNode in self.itemNodes { + if keepFirst && itemNode.index == 0 { + keptItemNode = itemNode + } else { + itemNode.removeFromSupernode() + } + } + self.itemNodes.removeAll() + if let keptItemNode = keptItemNode { + self.itemNodes.append(keptItemNode) + } + if let centralItemIndex = centralItemIndex, centralItemIndex >= 0 && centralItemIndex < items.count { + self.centralItemIndex = centralItemIndex + } else { + self.centralItemIndex = nil + } + self.items = items + + self.updateItemNodes() + } + + private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode { + let media = self.items[index] + let contentNode: ASDisplayNode + if let _ = media.media as? TelegramMediaImage { + contentNode = InstantPageImageNode(account: self.account, media: media, interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia) + } else if let file = media.media as? TelegramMediaFile { + contentNode = ASDisplayNode() + } else { + contentNode = ASDisplayNode() + } + + let node = InstantPageSlideshowItemNode(contentNode: contentNode) + + node.index = index + return node + } + + private func visibleItemNode(at index: Int) -> InstantPageSlideshowItemNode? { + for itemNode in self.itemNodes { + if itemNode.index == index { + return itemNode + } + } + return nil + } + + private func addVisibleItemNode(_ node: InstantPageSlideshowItemNode) { + var added = false + for i in 0 ..< self.itemNodes.count { + if node.index < self.itemNodes[i].index { + self.itemNodes.insert(node, at: i) + added = true + break + } + } + if !added { + self.itemNodes.append(node) + } + self.scrollView.addSubview(node.view) + } + + private func removeVisibleItemNode(internalIndex: Int) { + self.itemNodes[internalIndex].view.removeFromSuperview() + self.itemNodes.remove(at: internalIndex) + } + + private func updateItemNodes() { + if self.items.isEmpty || self.containerLayout == nil { + return + } + + var resetOffsetToCentralItem = false + if self.itemNodes.isEmpty { + let node = self.makeNodeForItem(at: self.centralItemIndex ?? 0) + node.frame = CGRect(origin: CGPoint(), size: scrollView.bounds.size) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + self.centralItemIndex = node.index + resetOffsetToCentralItem = true + } + + var notifyCentralItemUpdated = false + + if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { + if centralItemIndex != 0 { + if self.visibleItemNode(at: centralItemIndex - 1) == nil { + let node = self.makeNodeForItem(at: centralItemIndex - 1) + node.frame = centralItemNode.frame.offsetBy(dx: -centralItemNode.frame.size.width - self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + if centralItemIndex != items.count - 1 { + if self.visibleItemNode(at: centralItemIndex + 1) == nil { + let node = self.makeNodeForItem(at: centralItemIndex + 1) + node.frame = centralItemNode.frame.offsetBy(dx: centralItemNode.frame.size.width + self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + for i in 0 ..< self.itemNodes.count { + self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + } + + if resetOffsetToCentralItem { + self.scrollView.contentOffset = CGPoint(x: centralItemNode.frame.minX - self.pageGap, y: 0.0) + } + + if let centralItemCandidateNode = self.centralItemCandidate(), centralItemCandidateNode.index != centralItemIndex { + for i in (0 ..< self.itemNodes.count).reversed() { + let node = self.itemNodes[i] + if node.index < centralItemCandidateNode.index - 1 || node.index > centralItemCandidateNode.index + 1 { + self.removeVisibleItemNode(internalIndex: i) + } + } + + self.ignoreCentralItemIndexUpdate = true + self.centralItemIndex = centralItemCandidateNode.index + self.ignoreCentralItemIndexUpdate = false + notifyCentralItemUpdated = true + + if centralItemCandidateNode.index != 0 { + if self.visibleItemNode(at: centralItemCandidateNode.index - 1) == nil { + let node = self.makeNodeForItem(at: centralItemCandidateNode.index - 1) + node.frame = centralItemCandidateNode.frame.offsetBy(dx: -centralItemCandidateNode.frame.size.width - self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + if centralItemCandidateNode.index != items.count - 1 { + if self.visibleItemNode(at: centralItemCandidateNode.index + 1) == nil { + let node = self.makeNodeForItem(at: centralItemCandidateNode.index + 1) + node.frame = centralItemCandidateNode.frame.offsetBy(dx: centralItemCandidateNode.frame.size.width + self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + let previousCentralCandidateHorizontalOffset = self.scrollView.contentOffset.x - centralItemCandidateNode.frame.minX + + for i in 0 ..< self.itemNodes.count { + self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + } + + self.scrollView.contentOffset = CGPoint(x: centralItemCandidateNode.frame.minX + previousCentralCandidateHorizontalOffset, y: 0.0) + } + + self.scrollView.contentSize = CGSize(width: CGFloat(self.itemNodes.count) * self.scrollView.bounds.size.width, height: self.scrollView.bounds.size.height) + } else { + assertionFailure() + } + + for itemNode in self.itemNodes { + //itemNode.centralityUpdated(isCentral: itemNode.index == self.centralItemIndex) + //itemNode.visibilityUpdated(isVisible: self.scrollView.bounds.intersects(itemNode.frame)) + itemNode.internalIsVisible = self.scrollView.bounds.intersects(itemNode.frame) + } + + if notifyCentralItemUpdated { + self.centralItemIndexUpdated(self.centralItemIndex) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateItemNodes() + } + + private func centralItemCandidate() -> InstantPageSlideshowItemNode? { + let hotizontlOffset = self.scrollView.contentOffset.x + self.pageGap + var closestNodeAndDistance: (Int, CGFloat)? + for i in 0 ..< self.itemNodes.count { + let node = self.itemNodes[i] + let distance = abs(node.frame.minX - hotizontlOffset) + if let currentClosestNodeAndDistance = closestNodeAndDistance { + if distance < currentClosestNodeAndDistance.1 { + closestNodeAndDistance = (node.index, distance) + } + } else { + closestNodeAndDistance = (node.index, distance) + } + } + if let closestNodeAndDistance = closestNodeAndDistance { + return self.visibleItemNode(at: closestNodeAndDistance.0) + } else { + return nil + } + } + + func updateHiddenMedia(_ media: InstantPageMedia?) { + for node in self.itemNodes { + node.updateHiddenMedia(media) + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + for node in self.itemNodes { + if let transitionNode = node.transitionNode(media: media) { + return transitionNode + } + } + return nil + } +} + +final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { + var medias: [InstantPageMedia] = [] + + private let pagerNode: InstantPageSlideshowPagerNode + private let pageControlNode: PageControlNode + + init(account: Account, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void) { + self.medias = medias + + self.pagerNode = InstantPageSlideshowPagerNode(account: account, openMedia: openMedia) + self.pagerNode.replaceItems(medias, centralItemIndex: nil) + + self.pageControlNode = PageControlNode(dotColor: .white) + self.pageControlNode.isUserInteractionEnabled = false + + super.init() + + self.backgroundColor = .black + + self.addSubnode(self.pagerNode) + self.addSubnode(self.pageControlNode) + self.pageControlNode.pagesCount = medias.count + self.pagerNode.centralItemIndexUpdated = { [weak self] index in + if let strongSelf = self, let index = index { + strongSelf.pageControlNode.setPage(CGFloat(index)) + } + } + } + + override func layout() { + super.layout() + + self.pagerNode.frame = self.bounds + self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), 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? { + return self.pagerNode.transitionNode(media: media) + } + + func updateHiddenMedia(media: InstantPageMedia?) { + self.pagerNode.updateHiddenMedia(media) + } + + func updateIsVisible(_ isVisible: Bool) { + self.pagerNode.internalIsVisible = isVisible + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + } +} diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 001eb1c6a4..21f89e7fcc 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -1,9 +1,15 @@ import Foundation import TelegramCore +import Postbox -struct InstantPageTextUrlItem { - let frame: CGRect - let item: AnyObject +final class InstantPageUrlItem { + let url: String + let webpageId: MediaId? + + init(url: String, webpageId: MediaId?) { + self.url = url + self.webpageId = webpageId + } } struct InstantPageTextStrikethroughItem { @@ -12,22 +18,22 @@ struct InstantPageTextStrikethroughItem { final class InstantPageTextLine { let line: CTLine + let range: NSRange let frame: CGRect - let urlItems: [InstantPageTextUrlItem] let strikethroughItems: [InstantPageTextStrikethroughItem] - init(line: CTLine, frame: CGRect, urlItems: [InstantPageTextUrlItem], strikethroughItems: [InstantPageTextStrikethroughItem]) { + init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem]) { self.line = line + self.range = range self.frame = frame - self.urlItems = urlItems self.strikethroughItems = strikethroughItems } } final class InstantPageTextItem: InstantPageItem { + let attributedString: NSAttributedString let lines: [InstantPageTextLine] let rtlLineIndices: Set - let hasLinks: Bool var frame: CGRect var alignment: NSTextAlignment = .left let medias: [InstantPageMedia] = [] @@ -37,17 +43,13 @@ final class InstantPageTextItem: InstantPageItem { return !self.rtlLineIndices.isEmpty } - init(frame: CGRect, lines: [InstantPageTextLine]) { + init(frame: CGRect, attributedString: NSAttributedString, lines: [InstantPageTextLine]) { + self.attributedString = attributedString self.frame = frame self.lines = lines - var hasLinks = false var index = 0 var rtlLineIndices = Set() for line in lines { - if !line.urlItems.isEmpty { - hasLinks = true - } - let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray if glyphRuns.count != 0 { inner: for i in 0 ..< glyphRuns.count { @@ -61,7 +63,6 @@ final class InstantPageTextItem: InstantPageItem { } index += 1 } - self.hasLinks = hasLinks self.rtlLineIndices = rtlLineIndices } @@ -106,15 +107,140 @@ final class InstantPageTextItem: InstantPageItem { context.restoreGState() } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + private func attributesAtPoint(_ point: CGPoint) -> (Int, [String: Any])? { + let transformedPoint = CGPoint(x: point.x, y: point.y) + for line in self.lines { + let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if lineFrame.contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } + if index >= 0 && index < attributedString.length { + return (index, attributedString.attributes(at: index, effectiveRange: nil)) + } + break + } + } + for line in self.lines { + let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if lineFrame.insetBy(dx: -5.0, dy: -5.0).contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } + if index >= 0 && index < attributedString.length { + return (index, attributedString.attributes(at: index, effectiveRange: nil)) + } + break + } + } + return nil + } + + private func attributeRects(name: String, at index: Int) -> [CGRect]? { + var range = NSRange() + let _ = self.attributedString.attribute(name, at: index, effectiveRange: &range) + if range.length != 0 { + let boundsWidth = self.frame.width + var rects: [CGRect] = [] + for i in 0 ..< self.lines.count { + let line = self.lines[i] + let lineRange = NSIntersectionRange(range, line.range) + if lineRange.length != 0 { + var leftOffset: CGFloat = 0.0 + if lineRange.location != line.range.location { + leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) + } + var rightOffset: CGFloat = line.frame.width + if lineRange.location + lineRange.length != line.range.length { + rightOffset = ceil(CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, nil)) + } + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if self.alignment == .center { + lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } + + rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset, y: lineFrame.minY), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height))) + } + } + if !rects.isEmpty { + return rects + } + } + + return nil + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + if let (index, dict) = self.attributesAtPoint(point) { + if let _ = dict[TextNode.UrlAttribute] { + if let rects = self.attributeRects(name: TextNode.UrlAttribute, at: index) { + return rects + } + } + } + return [] } + func urlAttribute(at point: CGPoint) -> InstantPageUrlItem? { + if let (_, dict) = self.attributesAtPoint(point) { + if let url = dict[TextNode.UrlAttribute] as? InstantPageUrlItem { + return url + } + } + return nil + } + + func lineRects() -> [CGRect] { + let boundsWidth = self.frame.width + var rects: [CGRect] = [] + for i in 0 ..< self.lines.count { + let line = self.lines[i] + + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if self.alignment == .center { + lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } + + rects.append(lineFrame) + } + return rects + } + + func plainText() -> String { + if let first = self.lines.first, let last = self.lines.last { + return self.attributedString.attributedSubstring(from: NSMakeRange(first.range.location, last.range.location + last.range.length - first.range.location)).string + } + return "" + } + func matchesAnchor(_ anchor: String) -> Bool { return false } - func node(account: Account) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { return nil } @@ -131,119 +257,57 @@ final class InstantPageTextItem: InstantPageItem { } } -/* - - -static TGInstantPageLinkSelectionView *selectionViewFromFrames(NSArray *frames, CGPoint origin, id urlItem) { - CGRect frame = CGRectMake(0.0f, 0.0f, 0.0f, 0.0f); - bool first = true; - for (NSValue *rectValue in frames) { - CGRect rect = [rectValue CGRectValue]; - if (first) { - first = false; - frame = rect; - } else { - frame = CGRectUnion(rect, frame); - } - } - NSMutableArray *adjustedFrames = [[NSMutableArray alloc] init]; - for (NSValue *rectValue in frames) { - CGRect rect = [rectValue CGRectValue]; - rect.origin.x -= frame.origin.x; - rect.origin.y -= frame.origin.y; - [adjustedFrames addObject:[NSValue valueWithCGRect:rect]]; - } - return [[TGInstantPageLinkSelectionView alloc] initWithFrame:CGRectOffset(frame, origin.x, origin.y) rects:adjustedFrames urlItem:urlItem]; - } - - - (NSArray *)linkSelectionViews { - if (_hasLinks) { - NSMutableArray *views = [[NSMutableArray alloc] init]; - NSMutableArray *currentLinkFrames = [[NSMutableArray alloc] init]; - id currentUrlItem = nil; - for (TGInstantPageTextLine *line in _lines) { - if (line.urlItems != nil) { - for (TGInstantPageTextUrlItem *urlItem in line.urlItems) { - if (currentUrlItem == urlItem.item) { - } else { - if (currentLinkFrames.count != 0) { - [views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)]; - } - [currentLinkFrames removeAllObjects]; - currentUrlItem = urlItem.item; - } - CGPoint lineOrigin = line.frame.origin; - if (_alignment == NSTextAlignmentCenter) { - lineOrigin.x = CGFloor((self.frame.size.width - line.frame.size.width) / 2.0f); - } - [currentLinkFrames addObject:[NSValue valueWithCGRect:CGRectOffset(urlItem.frame, lineOrigin.x, 0.0)]]; - } - } else if (currentUrlItem != nil) { - if (currentLinkFrames.count != 0) { - [views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)]; - } - [currentLinkFrames removeAllObjects]; - currentUrlItem = nil; - } - } - if (currentLinkFrames.count != 0 && currentUrlItem != nil) { - [views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)]; - } - return views; - } - return nil; -} - -@end*/ - -func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack) -> NSAttributedString { +func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack, url: InstantPageUrlItem? = nil) -> NSAttributedString { switch text { case .empty: return NSAttributedString(string: "", attributes: styleStack.textAttributes()) case let .plain(string): - return NSAttributedString(string: string, attributes: styleStack.textAttributes()) + var attributes = styleStack.textAttributes() + if let url = url { + attributes[TextNode.UrlAttribute] = url + } + return NSAttributedString(string: string, attributes: attributes) case let .bold(text): styleStack.push(.bold) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .italic(text): styleStack.push(.italic) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .underline(text): styleStack.push(.underline) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .strikethrough(text): styleStack.push(.strikethrough) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .fixed(text): styleStack.push(.fontFixed(true)) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result - case let .url(text, url, _): - styleStack.push(.textColor(UIColor(rgb: 0x007BE8))) - let result = attributedStringForRichText(text, styleStack: styleStack) - styleStack.pop() + case let .url(text, url, webpageId): + styleStack.push(.underline) + let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: url, webpageId: webpageId)) styleStack.pop() return result - case let .email(text, _): + case let .email(text, email): styleStack.push(.bold) - styleStack.push(.textColor(UIColor(rgb: 0x007BE8))) - let result = attributedStringForRichText(text, styleStack: styleStack) + styleStack.push(.underline) + let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(email)", webpageId: nil)) styleStack.pop() styleStack.pop() return result case let .concat(texts): let string = NSMutableAttributedString() for text in texts { - let substring = attributedStringForRichText(text, styleStack: styleStack) + let substring = attributedStringForRichText(text, styleStack: styleStack, url: url) string.append(substring) } return string @@ -252,12 +316,12 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat) -> InstantPageTextItem { if string.length == 0 { - return InstantPageTextItem(frame: CGRect(), lines: []) + return InstantPageTextItem(frame: CGRect(), attributedString: string, lines: []) } var lines: [InstantPageTextLine] = [] guard let font = string.attribute(NSFontAttributeName, at: 0, effectiveRange: nil) as? UIFont else { - return InstantPageTextItem(frame: CGRect(), lines: []) + return InstantPageTextItem(frame: CGRect(), attributedString: string, lines: []) } var lineSpacingFactor: CGFloat = 1.12 @@ -287,10 +351,11 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo let trailingWhitespace = CGFloat(CTLineGetTrailingWhitespaceWidth(line)) let lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil) + Double(currentLineInset)) - var urlItems: [InstantPageTextUrlItem] = [] var strikethroughItems: [InstantPageTextStrikethroughItem] = [] - string.enumerateAttribute(NSStrikethroughStyleAttributeName, in: NSMakeRange(lastIndex, lineCharacterCount), options: [], using: { item, range, _ in + let lineRange = NSMakeRange(lastIndex, lineCharacterCount) + + string.enumerateAttribute(NSStrikethroughStyleAttributeName, in: lineRange, options: [], using: { item, range, _ in if let item = item { let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil)) @@ -311,7 +376,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo } }];*/ - let textLine = InstantPageTextLine(line: line, frame: CGRect(x: currentLineOrigin.x, y: currentLineOrigin.y, width: lineWidth, height: fontLineHeight), urlItems: urlItems, strikethroughItems: strikethroughItems) + let textLine = InstantPageTextLine(line: line, range: lineRange, frame: CGRect(x: currentLineOrigin.x, y: currentLineOrigin.y, width: lineWidth, height: fontLineHeight), strikethroughItems: strikethroughItems) lines.append(textLine) @@ -332,5 +397,5 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo height = lines.last!.frame.maxY } - return InstantPageTextItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: height), lines: lines) + return InstantPageTextItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: height), attributedString: string, lines: lines) } diff --git a/TelegramUI/InstantPageTheme.swift b/TelegramUI/InstantPageTheme.swift index 86e2e8824c..f87385ff85 100644 --- a/TelegramUI/InstantPageTheme.swift +++ b/TelegramUI/InstantPageTheme.swift @@ -1,7 +1,191 @@ import Foundation +import Postbox -final class InstantPageTheme { - init() { - +enum InstantPageFontStyle { + case sans + case serif +} + +struct InstantPageFont { + let style: InstantPageFontStyle + let size: CGFloat + let lineSpacingFactor: CGFloat +} + +struct InstantPageTextAttributes { + let font: InstantPageFont + let color: UIColor + let underline: Bool + + init(font: InstantPageFont, color: UIColor, underline: Bool = false) { + self.font = font + self.color = color + self.underline = underline + } + + func withUnderline(_ underline: Bool) -> InstantPageTextAttributes { + return InstantPageTextAttributes(font: self.font, color: self.color, underline: underline) + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextAttributes { + return InstantPageTextAttributes(font: InstantPageFont(style: forceSerif ? .serif : self.font.style, size: floor(self.font.size * sizeMultiplier), lineSpacingFactor: self.font.lineSpacingFactor), color: self.color, underline: self.underline) + } +} + +enum InstantPageTextCategoryType { + case header + case subheader + case paragraph + case caption +} + +struct InstantPageTextCategories { + let header: InstantPageTextAttributes + let subheader: InstantPageTextAttributes + let paragraph: InstantPageTextAttributes + let caption: InstantPageTextAttributes + + func attributes(type: InstantPageTextCategoryType, link: Bool) -> InstantPageTextAttributes { + switch type { + case .header: + return self.header.withUnderline(link) + case .subheader: + return self.subheader.withUnderline(link) + case .paragraph: + return self.paragraph.withUnderline(link) + case .caption: + return self.caption.withUnderline(link) + } + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextCategories { + return InstantPageTextCategories(header: self.header.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), subheader: self.subheader.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), paragraph: self.paragraph.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif)) + } +} + +final class InstantPageTheme { + let pageBackgroundColor: UIColor + + let textCategories: InstantPageTextCategories + + let textHighlightColor: UIColor + let linkHighlightColor: UIColor + + let panelBackgroundColor: UIColor + let panelHighlightedBackgroundColor: UIColor + let panelPrimaryColor: UIColor + 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) { + self.pageBackgroundColor = pageBackgroundColor + self.textCategories = textCategories + self.textHighlightColor = textHighlightColor + self.linkHighlightColor = linkHighlightColor + self.panelBackgroundColor = panelBackgroundColor + self.panelHighlightedBackgroundColor = panelHighlightedBackgroundColor + self.panelPrimaryColor = panelPrimaryColor + self.panelSecondaryColor = panelSecondaryColor + self.panelAccentColor = panelAccentColor + } + + 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) + } +} + +private let lightTheme = InstantPageTheme( + pageBackgroundColor: .white, + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: .black), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: .black), + 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)) + ), + textHighlightColor: UIColor(rgb: 0, alpha: 0.12), + linkHighlightColor: UIColor(rgb: 0, alpha: 0.12), + panelBackgroundColor: UIColor(rgb: 0xf3f4f5), + panelHighlightedBackgroundColor: UIColor(rgb: 0xe7e7e7), + panelPrimaryColor: .black, + panelSecondaryColor: UIColor(rgb: 0x79828b), + panelAccentColor: UIColor(rgb: 0x007ee5) +) + +private let sepiaTheme = InstantPageTheme( + pageBackgroundColor: UIColor(rgb: 0xf8f1e2), + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0x4f321d)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0x4f321d)), + 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)) + ), + textHighlightColor: UIColor(rgb: 0, alpha: 0.1), + linkHighlightColor: UIColor(rgb: 0, alpha: 0.1), + panelBackgroundColor: UIColor(rgb: 0xefe7d6), + panelHighlightedBackgroundColor: UIColor(rgb: 0xe3dccb), + panelPrimaryColor: .black, + panelSecondaryColor: UIColor(rgb: 0x927e6b), + panelAccentColor: UIColor(rgb: 0xd19601) +) + +private let grayTheme = InstantPageTheme( + pageBackgroundColor: UIColor(rgb: 0x5a5a5c), + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xcecece)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xcecece)), + 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)) + ), + textHighlightColor: UIColor(rgb: 0, alpha: 0.16), + linkHighlightColor: UIColor(rgb: 0, alpha: 0.16), + panelBackgroundColor: UIColor(rgb: 0x555556), + panelHighlightedBackgroundColor: UIColor(rgb: 0x505051), + panelPrimaryColor: UIColor(rgb: 0xcecece), + panelSecondaryColor: UIColor(rgb: 0xa0a0a0), + panelAccentColor: UIColor(rgb: 0x54b9f8) +) + +private let darkTheme = InstantPageTheme( + pageBackgroundColor: UIColor(rgb: 0x000000), + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xb0b0b0)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xb0b0b0)), + 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)) + ), + textHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), + linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), + panelBackgroundColor: UIColor(rgb: 0x131313), + panelHighlightedBackgroundColor: UIColor(rgb: 0x1f1f1f), + panelPrimaryColor: UIColor(rgb: 0xb0b0b0), + panelSecondaryColor: UIColor(rgb: 0x6a6a6a), + panelAccentColor: UIColor(rgb: 0x50b6f3) +) + +private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFontSize) -> CGFloat { + switch variant { + case .small: + return 0.85 + case .standard: + return 1.0 + case .large: + return 1.15 + case .xlarge: + return 1.3 + case .xxlarge: + return 1.5 + } +} + +func instantPageThemeForSettings(_ settings: InstantPagePresentationSettings) -> InstantPageTheme { + switch settings.themeType { + case .light: + return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + case .sepia: + return sepiaTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + case .gray: + return grayTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + case .dark: + return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) } } diff --git a/TelegramUI/InstantPageTileNode.swift b/TelegramUI/InstantPageTileNode.swift index 0a41c26767..3d35a943be 100644 --- a/TelegramUI/InstantPageTileNode.swift +++ b/TelegramUI/InstantPageTileNode.swift @@ -3,9 +3,11 @@ import AsyncDisplayKit private final class InstantPageTileNodeParameters: NSObject { let tile: InstantPageTile + let backgroundColor: UIColor - init(tile: InstantPageTile) { + init(tile: InstantPageTile, backgroundColor: UIColor) { self.tile = tile + self.backgroundColor = backgroundColor super.init() } @@ -14,31 +16,31 @@ private final class InstantPageTileNodeParameters: NSObject { final class InstantPageTileNode: ASDisplayNode { private let tile: InstantPageTile - init(tile: InstantPageTile) { + init(tile: InstantPageTile, backgroundColor: UIColor) { self.tile = tile super.init() self.isLayerBacked = true self.isOpaque = true - self.backgroundColor = UIColor.white + self.backgroundColor = backgroundColor } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return InstantPageTileNodeParameters(tile: self.tile) + return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.backgroundColor ?? UIColor.white) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! - if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(UIColor.white.cgColor) - context.fill(bounds) - } - if let parameters = parameters as? InstantPageTileNodeParameters { + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(parameters.backgroundColor.cgColor) + context.fill(bounds) + } + parameters.tile.draw(context: context) } } diff --git a/TelegramUI/InstantPageWebEmbedItem.swift b/TelegramUI/InstantPageWebEmbedItem.swift index d942b186f2..ff998c2d74 100644 --- a/TelegramUI/InstantPageWebEmbedItem.swift +++ b/TelegramUI/InstantPageWebEmbedItem.swift @@ -1,9 +1,9 @@ import Foundation +import Postbox import TelegramCore final class InstantPageWebEmbedItem: InstantPageItem { var frame: CGRect - let hasLinks: Bool = false let wantsNode: Bool = true let medias: [InstantPageMedia] = [] @@ -18,7 +18,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - func node(account: Account) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { return instantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling) } @@ -46,7 +46,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { } } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + func linkSelectionRects(at point: CGPoint) -> [CGRect] { return [] } diff --git a/TelegramUI/InstantPageWebEmbedNode.swift b/TelegramUI/InstantPageWebEmbedNode.swift index 1e93ea2e35..2f2de22d9d 100644 --- a/TelegramUI/InstantPageWebEmbedNode.swift +++ b/TelegramUI/InstantPageWebEmbedNode.swift @@ -33,6 +33,16 @@ final class instantPageWebEmbedNode: ASDisplayNode, InstantPageNode { self.webView.frame = self.bounds } + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + } + func updateIsVisible(_ isVisible: Bool) { } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + } } diff --git a/TelegramUI/InstantVideoNode.swift b/TelegramUI/InstantVideoNode.swift index 6a5256b1b0..aabaab8921 100644 --- a/TelegramUI/InstantVideoNode.swift +++ b/TelegramUI/InstantVideoNode.swift @@ -168,6 +168,16 @@ final class InstantVideoNode: OverlayMediaItemNode { } }) + + self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { [weak self] context in + if let strongSelf = self, let context = context as? SharedInstantVideoContext { + context.addPlaybackCompleted { + if let strongSelf = self { + strongSelf.playbackEnded?() + } + } + } + }) } deinit { diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index a515165b69..6e1fb31853 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -189,6 +189,14 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV self.transitionDisposable.dispose() } + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + func animateIn() { self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } @@ -352,7 +360,7 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV } func scrollToTop() { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 8adde9434b..7fdb3e0b7a 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -31,6 +31,13 @@ private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyCo self.controller = LegacyController(presentation: .custom) super.init() + + if let parentController = parentController { + if parentController.statusBar.statusBarStyle == .Hide { + self.controller?.statusBar.statusBarStyle = parentController.statusBar.statusBarStyle + } + self.controller?.view.frame = parentController.view.bounds + } } func managesWindow() -> Bool { @@ -275,10 +282,13 @@ public class LegacyController: ViewController { self.controllerNode.animateModalIn(completion: { [weak self] in self?.presentationCompleted?() }) + } else { + self.presentationCompleted?() } self.legacyController.viewDidAppear(animated && animateIn) case .custom: self.legacyController.viewDidAppear(animated) + self.presentationCompleted?() } } diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift new file mode 100644 index 0000000000..8c6e285010 --- /dev/null +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -0,0 +1,156 @@ +import Foundation +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +import LegacyComponents + +final class InstantVideoControllerRecordingStatus { + let micLevel: Signal + + init(micLevel: Signal) { + self.micLevel = micLevel + } +} + +final class InstantVideoController: LegacyController { + private var captureController: TGVideoMessageCaptureController? + + var onDismiss: (() -> Void)? + + private let micLevelValue = ValuePromise(0.0) + let audioStatus: InstantVideoControllerRecordingStatus + + private var dismissedVideo = false + + override init(presentation: LegacyControllerPresentation) { + self.audioStatus = InstantVideoControllerRecordingStatus(micLevel: self.micLevelValue.get()) + + super.init(presentation: presentation) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bindCaptureController(_ captureController: TGVideoMessageCaptureController?) { + self.captureController = captureController + if let captureController = captureController { + captureController.micLevel = { [weak self] (level: CGFloat) -> Void in + self?.micLevelValue.set(Float(level)) + } + captureController.onDismiss = { [weak self] _ in + if let strongSelf = self { + strongSelf.onDismiss?() + } + } + } + } + + func dismissVideo() { + if let captureController = self.captureController, !self.dismissedVideo { + self.dismissedVideo = true + captureController.dismiss() + } + } + + func completeVideo() { + if let captureController = self.captureController, !self.dismissedVideo { + self.dismissedVideo = true + captureController.complete() + } + } + + func stopVideo() -> Bool { + if let captureController = self.captureController { + return captureController.stop() + } + return false + } + + func lockVideo() { + if let captureController = self.captureController { + return captureController.setLocked() + } + } + + func updateRecordButtonInteraction(_ value: CGFloat) { + if let captureController = self.captureController { + captureController.buttonInteractionUpdate(CGPoint(x: value, y: 0.0)) + } + } +} + +func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, account: Account, peerId: PeerId) -> InstantVideoController { + let legacyController = InstantVideoController(presentation: .custom) + legacyController.statusBar.statusBarStyle = .Hide + let baseController = TGViewController(context: legacyController.context)! + legacyController.bind(controller: baseController) + legacyController.presentationCompleted = { [weak legacyController, weak baseController] in + if let legacyController = legacyController, let baseController = baseController { + let controller = TGVideoMessageCaptureController(context: legacyController.context, assets: TGVideoMessageCaptureControllerAssets(send: PresentationResourcesChat.chatInputPanelSendButtonImage(theme)!, slideToCancel:PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)!, actionDelete: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor))!, transitionInView: { + return nil + }, parentController: baseController, controlsFrame: panelFrame, isAlreadyLocked: { + return false + }, liveUploadInterface: nil)! + /*controller.finishedWithVideo = ^(NSURL *videoURL, UIImage *previewImage, __unused NSUInteger fileSize, NSTimeInterval duration, CGSize dimensions, TGLiveUploadActorData *liveUploadData, TGVideoEditAdjustments *adjustments) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf != nil) + { + NSDictionary *desc = [strongSelf->_companion videoDescriptionFromVideoURL:videoURL previewImage:previewImage dimensions:dimensions duration:duration adjustments:adjustments stickers:nil caption:nil roundMessage:true liveUploadData:liveUploadData timer:0]; + [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:@[ desc ] asReplyToMessageId:[strongSelf currentReplyMessageId] botReplyMarkup:nil]; + } + }*/ + controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments in + guard let videoUrl = videoUrl else { + return + } + + var finalDimensions: CGSize = dimensions + var finalDuration: Double = duration + + var previewRepresentations: [TelegramMediaImageRepresentation] = [] + if let previewImage = previewImage { + let resource = LocalFileMediaResource(fileId: arc4random64()) + let thumbnailSize = finalDimensions.aspectFitted(CGSize(width: 90.0, height: 90.0)) + let thumbnailImage = TGScaleImageToPixelSize(previewImage, thumbnailSize)! + if let thumbnailData = UIImageJPEGRepresentation(thumbnailImage, 0.4) { + account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnailSize, resource: resource)) + } + } + + finalDimensions = TGMediaVideoConverter.dimensions(for: finalDimensions, adjustments: adjustments, preset: TGMediaVideoConversionPresetVideoMessage) + + var resourceAdjustments: VideoMediaResourceAdjustments? + if let adjustments = adjustments { + if adjustments.trimApplied() { + finalDuration = adjustments.trimEndValue - adjustments.trimStartValue + } + + let adjustmentsData = MemoryBuffer(data: NSKeyedArchiver.archivedData(withRootObject: adjustments.dictionary())) + let digest = MemoryBuffer(data: adjustmentsData.md5Digest()) + resourceAdjustments = VideoMediaResourceAdjustments(data: adjustmentsData, digest: digest) + } + + let resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: videoUrl.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: [.instantRoundVideo])]) + var attributes: [MessageAttribute] = [] + /*if let timer = item.timer, timer > 0 && timer <= 60 { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) + }*/ + let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: attributes, media: media, replyToMessageId: nil)]).start() + } + controller.didDismiss = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss() + } + } + legacyController.bindCaptureController(controller) + } + } + return legacyController +} diff --git a/TelegramUI/MapResources.swift b/TelegramUI/MapResources.swift index 0ebd14f662..593defb554 100644 --- a/TelegramUI/MapResources.swift +++ b/TelegramUI/MapResources.swift @@ -40,14 +40,14 @@ public class MapSnapshotMediaResource: TelegramMediaResource { self.height = height } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.latitude = decoder.decodeDoubleForKey("lt", orElse: 0.0) self.longitude = decoder.decodeDoubleForKey("ln", orElse: 0.0) self.width = decoder.decodeInt32ForKey("w", orElse: 0) self.height = decoder.decodeInt32ForKey("h", orElse: 0) } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeDouble(self.latitude, forKey: "lt") encoder.encodeDouble(self.longitude, forKey: "ln") encoder.encodeInt32(self.width, forKey: "w") diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index cf5b45efc6..fc6c0c4c5c 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -85,6 +85,7 @@ private final class MediaPlayerScrubbingForegroundNode: ASDisplayNode { } final class MediaPlayerScrubbingNode: ASDisplayNode { + private let lineCap: MediaPlayerScrubbingNodeCap private let lineHeight: CGFloat private let backgroundNode: ASImageNode @@ -120,20 +121,21 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } private var statusDisposable: Disposable? - private var statusValuePromise = Promise() + private var statusValuePromise = Promise() var status: Signal? { didSet { if let status = self.status { - self.statusValuePromise.set(status) + self.statusValuePromise.set(status |> map { $0 }) } else { - self.statusValuePromise.set(.never()) + self.statusValuePromise.set(.single(nil)) } } } init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: Bool, backgroundColor: UIColor, foregroundColor: UIColor) { self.lineHeight = lineHeight + self.lineCap = lineCap self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -234,6 +236,20 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } } + func updateColors(backgroundColor: UIColor, foregroundColor: UIColor) { + switch lineCap { + case .round: + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) + self.foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) + case .square: + self.backgroundNode.backgroundColor = backgroundColor + self.foregroundContentNode.backgroundColor = foregroundColor + } + if let handleNode = self.handleNode as? ASImageNode { + handleNode.image = generateHandleBackground(color: foregroundColor) + } + } + private func preparedAnimation(keyPath: String, from: NSValue, to: NSValue, duration: Double, beginTime: Double?, offset: Double, speed: Float, repeatForever: Bool = false) -> CAAnimation { let animation = CABasicAnimation(keyPath: keyPath) animation.fromValue = from @@ -244,7 +260,7 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { animation.speed = speed animation.timeOffset = offset animation.isAdditive = false - animation.repeatCount = Float.infinity + //animation.repeatCount = Float.infinity if let beginTime = beginTime { animation.beginTime = beginTime } diff --git a/TelegramUI/MediaResources.swift b/TelegramUI/MediaResources.swift index afb13d2d58..a199c71b49 100644 --- a/TelegramUI/MediaResources.swift +++ b/TelegramUI/MediaResources.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import TelegramCore -public final class VideoMediaResourceAdjustments: Coding, Equatable { +public final class VideoMediaResourceAdjustments: PostboxCoding, Equatable { let data: MemoryBuffer let digest: MemoryBuffer @@ -11,12 +11,12 @@ public final class VideoMediaResourceAdjustments: Coding, Equatable { self.digest = digest } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.data = decoder.decodeBytesForKey("d")! self.digest = decoder.decodeBytesForKey("h")! } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeBytes(self.data, forKey: "d") encoder.encodeBytes(self.digest, forKey: "h") } @@ -64,12 +64,12 @@ public final class VideoLibraryMediaResource: TelegramMediaResource { self.adjustments = adjustments } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.localIdentifier = decoder.decodeStringForKey("i", orElse: "") self.adjustments = decoder.decodeObjectForKey("a", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.localIdentifier, forKey: "i") if let adjustments = self.adjustments { encoder.encodeObject(adjustments, forKey: "a") @@ -126,13 +126,13 @@ public final class LocalFileVideoMediaResource: TelegramMediaResource { self.adjustments = adjustments } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.randomId = decoder.decodeInt64ForKey("i", orElse: 0) self.path = decoder.decodeStringForKey("p", orElse: "") self.adjustments = decoder.decodeObjectForKey("a", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.randomId, forKey: "i") encoder.encodeString(self.path, forKey: "p") if let adjustments = self.adjustments { @@ -182,11 +182,11 @@ public class PhotoLibraryMediaResource: TelegramMediaResource { self.localIdentifier = localIdentifier } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.localIdentifier = decoder.decodeStringForKey("i", orElse: "") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.localIdentifier, forKey: "i") } diff --git a/TelegramUI/MultiplexedSoftwareVideoNode.swift b/TelegramUI/MultiplexedSoftwareVideoNode.swift index 15858d3fba..45f8c53d5b 100644 --- a/TelegramUI/MultiplexedSoftwareVideoNode.swift +++ b/TelegramUI/MultiplexedSoftwareVideoNode.swift @@ -109,6 +109,9 @@ final class MultiplexedSoftwareVideoNode: UIView { init(account: Account, scrollView: UIScrollView) { self.account = account self.scrollView = scrollView + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } self.trackingNode = MultiplexedSoftwareVideoTrackingNode() self.sourceManager = MultiplexedSoftwareVideoSourceManager(queue: self.videoSourceQueue, account: account) diff --git a/TelegramUI/Notices.swift b/TelegramUI/Notices.swift index 0d66096b1c..34e26bc141 100644 --- a/TelegramUI/Notices.swift +++ b/TelegramUI/Notices.swift @@ -2,14 +2,14 @@ import Foundation import Postbox import SwiftSignalKit -final class ApplicationSpecificBoolNotice: Coding { +final class ApplicationSpecificBoolNotice: PostboxCoding { init() { } - init(decoder: Decoder) { + init(decoder: PostboxDecoder) { } - func encode(_ encoder: Encoder) { + func encode(_ encoder: PostboxEncoder) { } } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 972051c164..1e4c5dc899 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -317,8 +317,11 @@ public class PeerMediaCollectionController: ViewController { }, sendBotCommand: { _ in }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _ in - }, beginAudioRecording: { - }, finishAudioRecording: { _ in + }, beginMediaRecording: { _ in + }, finishMediaRecording: { _ in + }, stopMediaRecording: { + }, lockMediaRecording: { + }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _ in }, unblockPeer: { @@ -329,6 +332,7 @@ public class PeerMediaCollectionController: ViewController { }, deleteChat: { }, beginCall: { }, toggleMessageStickerStarred: { _ in + }, presentController: { _ in }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 21b48d2e19..7767f633a9 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -1620,10 +1620,6 @@ func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(T c.draw(fullSizeImage, in: fittedRect) c.setBlendMode(.normal) - - if let locationPinImage = locationPinImage { - c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) - } } } else { context.withFlippedContext { c in @@ -1632,10 +1628,6 @@ func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(T c.fill(arguments.drawingRect) c.setBlendMode(.normal) - - if let locationPinImage = locationPinImage { - c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) - } } } } diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index b6c2612ac9..b7662fafd0 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -9,6 +9,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { case generatedMediaStoreSettings = 3 case voiceCallSettings = 4 case presentationThemeSettings = 5 + case instantPagePresentationSettings = 6 } public struct ApplicationSpecificPreferencesKeys { @@ -18,4 +19,5 @@ public struct ApplicationSpecificPreferencesKeys { public static let generatedMediaStoreSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.generatedMediaStoreSettings.rawValue) public static let voiceCallSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voiceCallSettings.rawValue) public static let presentationThemeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.presentationThemeSettings.rawValue) + public static let instantPagePresentationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.instantPagePresentationSettings.rawValue) } diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index 6d0296dc86..1a2bd3f780 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -4,7 +4,7 @@ import Postbox import TelegramCore import Display -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?) -> Signal { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, readStateData: ChatHistoryCombinedInitialReadStateData?) -> Signal { return Signal { subscriber in let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) @@ -106,64 +106,84 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie if let scrollPosition = scrollPosition { switch scrollPosition { - case let .Unread(unreadIndex): - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if case .UnreadEntry = entry { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) - break - } - index -= 1 - } - - if scrollToItem == nil { + case let .unread(unreadIndex): var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { - if entry.index >= unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) + if case .UnreadEntry = entry { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) break } index -= 1 } - } - - if scrollToItem == nil { - var index = 0 - for entry in toView.filteredEntries.reversed() { - if entry.index < unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) + + if scrollToItem == nil { + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= unreadIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) + break + } + index -= 1 + } + } + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < unreadIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) + break + } + index += 1 + } + } + case let .positionRestoration(scrollIndex, relativeOffset): + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .top(relativeOffset), animated: false, curve: .Default, directionHint: .Down) break } - index += 1 + index -= 1 } - } - case let .Index(scrollIndex, position, directionHint, animated): - if case .Center = position { - scrolledToIndex = scrollIndex - } - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if entry.index >= scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) - break + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default, directionHint: .Down) + break + } + index += 1 + } } - index -= 1 - } - - if scrollToItem == nil { - var index = 0 - for entry in toView.filteredEntries.reversed() { - if entry.index < scrollIndex { + case let .index(scrollIndex, position, directionHint, animated): + if case .center = position { + scrolledToIndex = scrollIndex + } + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= scrollIndex { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) break } - index += 1 + index -= 1 + } + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + break + } + index += 1 + } } - } } } - subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, scrolledToIndex: scrolledToIndex)) + subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, readStateData: readStateData, scrolledToIndex: scrolledToIndex)) subscriber.putCompletion() return EmptyDisposable diff --git a/TelegramUI/PresentationPasscodeSettings.swift b/TelegramUI/PresentationPasscodeSettings.swift index f1bb88a00a..51cf7e2305 100644 --- a/TelegramUI/PresentationPasscodeSettings.swift +++ b/TelegramUI/PresentationPasscodeSettings.swift @@ -15,12 +15,12 @@ public struct PresentationPasscodeSettings: PreferencesEntry, Equatable { self.autolockTimeout = autolockTimeout } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.enableBiometrics = decoder.decodeInt32ForKey("b", orElse: 0) != 0 self.autolockTimeout = decoder.decodeOptionalInt32ForKey("al") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.enableBiometrics ? 1 : 0, forKey: "s") if let autolockTimeout = self.autolockTimeout { encoder.encodeInt32(autolockTimeout, forKey: "al") diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 43821e4c52..72cae11e18 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -98,6 +98,9 @@ enum PresentationResourceKey: Int32 { case chatInputTextFieldClearImage case chatInputPanelSendButtonImage case chatInputPanelVoiceButtonImage + case chatInputPanelVideoButtonImage + case chatInputPanelVoiceActiveButtonImage + case chatInputPanelVideoActiveButtonImage case chatInputPanelAttachmentButtonImage case chatInputPanelMediaRecordingDotImage case chatInputPanelMediaRecordingCancelArrowImage diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 18a079502b..a315d38367 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -339,6 +339,24 @@ struct PresentationResourcesChat { }) } + static func chatInputPanelVideoButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVideoButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: theme.chat.inputPanel.panelControlColor) + }) + } + + static func chatInputPanelVoiceActiveButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVoiceActiveButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: .white) + }) + } + + static func chatInputPanelVideoActiveButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVideoActiveButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: .white) + }) + } + static func chatInputPanelAttachmentButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelAttachmentButtonImage.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: theme.chat.inputPanel.panelControlColor) diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index ae57ac3b64..a0c2d8fc74 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -7,7 +7,7 @@ public enum PresentationThemeParsingError: Error { case generic } -private func parseColor(_ decoder: Decoder, _ key: String) throws -> UIColor { +private func parseColor(_ decoder: PostboxDecoder, _ key: String) throws -> UIColor { if let value = decoder.decodeOptionalInt32ForKey(key) { return UIColor(argb: UInt32(bitPattern: value)) } else { @@ -36,7 +36,7 @@ public final class PresentationThemeRootTabBar { self.badgeTextColor = badgeTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.backgroundColor = try parseColor(decoder, "backgroundColor") self.separatorColor = try parseColor(decoder, "separatorColor") self.iconColor = try parseColor(decoder, "iconColor") @@ -47,7 +47,7 @@ public final class PresentationThemeRootTabBar { self.badgeTextColor = try parseColor(decoder, "badgeTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -81,7 +81,7 @@ public final class PresentationThemeRootNavigationStatusBar { self.style = style } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let styleValue = decoder.decodeOptionalInt32ForKey("style"), let style = PresentationThemeStatusBarStyle(rawValue: styleValue) { self.style = style } else { @@ -89,7 +89,7 @@ public final class PresentationThemeRootNavigationStatusBar { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.style.rawValue, forKey: "style") } } @@ -102,8 +102,10 @@ public final class PresentationThemeRootNavigationBar { public let accentTextColor: UIColor public let backgroundColor: UIColor public let separatorColor: UIColor + public let badgeBackgroundColor: UIColor + public let badgeTextColor: UIColor - public init(buttonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor) { + public init(buttonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { self.buttonColor = buttonColor self.primaryTextColor = primaryTextColor self.secondaryTextColor = secondaryTextColor @@ -111,9 +113,11 @@ public final class PresentationThemeRootNavigationBar { self.accentTextColor = accentTextColor self.backgroundColor = backgroundColor self.separatorColor = separatorColor + self.badgeBackgroundColor = badgeBackgroundColor + self.badgeTextColor = badgeTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.buttonColor = try parseColor(decoder, "buttonColor") self.primaryTextColor = try parseColor(decoder, "primaryTextColor") self.secondaryTextColor = try parseColor(decoder, "secondaryTextColor") @@ -121,9 +125,11 @@ public final class PresentationThemeRootNavigationBar { self.accentTextColor = try parseColor(decoder, "accentTextColor") self.backgroundColor = try parseColor(decoder, "backgroundColor") self.separatorColor = try parseColor(decoder, "separatorColor") + self.badgeBackgroundColor = try parseColor(decoder, "badgeBackgroundColor") + self.badgeTextColor = try parseColor(decoder, "badgeTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -155,7 +161,7 @@ public final class PresentationThemeActiveNavigationSearchBar { self.separatorColor = separatorColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.backgroundColor = try parseColor(decoder, "backgroundColor") self.accentColor = try parseColor(decoder, "accentColor") self.inputFillColor = try parseColor(decoder, "inputFillColor") @@ -165,7 +171,7 @@ public final class PresentationThemeActiveNavigationSearchBar { self.separatorColor = try parseColor(decoder, "separatorColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -191,7 +197,7 @@ public final class PresentationThemeRootController { self.activeNavigationSearchBar = activeNavigationSearchBar } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let statusBar = (try? decoder.decodeObjectForKeyThrowing("statusBar", decoder: { try PresentationThemeRootNavigationStatusBar(decoder: $0) })) as? PresentationThemeRootNavigationStatusBar { self.statusBar = statusBar } else { @@ -214,7 +220,7 @@ public final class PresentationThemeRootController { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectWithEncoder(self.statusBar, encoder: { self.statusBar.encode($0) }, forKey: "statusBar") encoder.encodeObjectWithEncoder(self.tabBar, encoder: { self.tabBar.encode($0) }, forKey: "tabBar") encoder.encodeObjectWithEncoder(self.navigationBar, encoder: { self.navigationBar.encode($0) }, forKey: "navigationBar") @@ -233,13 +239,13 @@ public final class PresentationThemeSwitch { self.contentColor = contentColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.frameColor = try parseColor(decoder, "frameColor") self.handleColor = try parseColor(decoder, "handleColor") self.contentColor = try parseColor(decoder, "contentColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -291,7 +297,7 @@ public final class PresentationThemeList { self.itemSwitchColors = itemSwitchColors } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.blocksBackgroundColor = try parseColor(decoder, "blocksBackgroundColor") self.plainBackgroundColor = try parseColor(decoder, "plainBackgroundColor") self.itemPrimaryTextColor = try parseColor(decoder, "itemPrimaryTextColor") @@ -315,7 +321,7 @@ public final class PresentationThemeList { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -379,7 +385,7 @@ public final class PresentationThemeChatList { self.searchBarKeyboardColor = searchBarKeyboardColor } - init(decoder: Decoder) throws { + init(decoder: PostboxDecoder) throws { self.backgroundColor = try parseColor(decoder, "backgroundColor") self.itemSeparatorColor = try parseColor(decoder, "itemSeparatorColor") self.itemBackgroundColor = try parseColor(decoder, "itemBackgroundColor") @@ -408,7 +414,7 @@ public final class PresentationThemeChatList { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -528,7 +534,7 @@ public final class PresentationThemeChatBubble { self.actionButtonsTextColor = actionButtonsTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.incomingFillColor = try parseColor(decoder, "incomingFillColor") self.incomingFillHighlightedColor = try parseColor(decoder, "incomingFillHighlightedColor") self.incomingStrokeColor = try parseColor(decoder, "incomingStrokeColor") @@ -578,7 +584,7 @@ public final class PresentationThemeChatBubble { self.actionButtonsTextColor = try parseColor(decoder, "actionButtonsTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -616,7 +622,7 @@ public final class PresentationThemeServiceMessage { self.dateTextColor = dateTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.serviceMessageFillColor = try parseColor(decoder, "serviceMessageFillColor") self.serviceMessagePrimaryTextColor = try parseColor(decoder, "serviceMessagePrimaryTextColor") self.unreadBarFillColor = try parseColor(decoder, "unreadBarFillColor") @@ -627,7 +633,7 @@ public final class PresentationThemeServiceMessage { self.dateTextColor = try parseColor(decoder, "dateTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -689,7 +695,7 @@ public final class PresentationThemeChatInputPanel { self.keyboardColor = keyboardColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") self.panelStrokeColor = try parseColor(decoder, "panelStrokeColor") self.panelControlAccentColor = try parseColor(decoder, "panelControlAccentColor") @@ -710,7 +716,7 @@ public final class PresentationThemeChatInputPanel { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -744,7 +750,7 @@ public final class PresentationThemeInputMediaPanel { self.gifsBackgroundColor = gifsBackgroundColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") self.panelIconColor = try parseColor(decoder, "panelIconColor") self.panelHighlightedIconBackgroundColor = try parseColor(decoder, "panelHighlightedIconBackgroundColor") @@ -753,7 +759,7 @@ public final class PresentationThemeInputMediaPanel { self.gifsBackgroundColor = try parseColor(decoder, "gifsBackgroundColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -787,7 +793,7 @@ public final class PresentationThemeInputButtonPanel { self.buttonTextColor = buttonTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") self.buttonFillColor = try parseColor(decoder, "buttonFillColor") @@ -797,7 +803,7 @@ public final class PresentationThemeInputButtonPanel { self.buttonTextColor = try parseColor(decoder, "buttonTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -827,7 +833,7 @@ public final class PresentationThemeChatHistoryNavigation { self.badgeTextColor = badgeTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.fillColor = try parseColor(decoder, "fillColor") self.strokeColor = try parseColor(decoder, "strokeColor") self.foregroundColor = try parseColor(decoder, "foregroundColor") @@ -835,7 +841,7 @@ public final class PresentationThemeChatHistoryNavigation { self.badgeTextColor = try parseColor(decoder, "badgeTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -869,7 +875,7 @@ public final class PresentationThemeChat { self.historyNavigation = historyNavigation } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let bubble = (try? decoder.decodeObjectForKeyThrowing("bubble", decoder: { try PresentationThemeChatBubble(decoder: $0) })) as? PresentationThemeChatBubble { self.bubble = bubble } else { @@ -902,7 +908,7 @@ public final class PresentationThemeChat { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectWithEncoder(self.bubble, encoder: { self.bubble.encode($0) }, forKey: "bubble") encoder.encodeObjectWithEncoder(self.serviceMessage, encoder: { self.serviceMessage.encode($0) }, forKey: "serviceMessage") encoder.encodeObjectWithEncoder(self.inputPanel, encoder: { self.inputPanel.encode($0) }, forKey: "inputPanel") @@ -927,7 +933,7 @@ public final class PresentationTheme: Equatable { self.chat = chat } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let rootController = (try? decoder.decodeObjectForKeyThrowing("rootController", decoder: { try PresentationThemeRootController(decoder: $0) })) as? PresentationThemeRootController { self.rootController = rootController } else { @@ -950,7 +956,7 @@ public final class PresentationTheme: Equatable { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectWithEncoder(self.rootController, encoder: { self.rootController.encode($0) }, forKey: "list") encoder.encodeObjectWithEncoder(self.list, encoder: { self.list.encode($0) }, forKey: "list") encoder.encodeObjectWithEncoder(self.chatList, encoder: { self.chatList.encode($0) }, forKey: "chatList") diff --git a/TelegramUI/PresentationThemeSettings.swift b/TelegramUI/PresentationThemeSettings.swift index c9f66e0c94..d266130cf4 100644 --- a/TelegramUI/PresentationThemeSettings.swift +++ b/TelegramUI/PresentationThemeSettings.swift @@ -7,10 +7,10 @@ public enum PresentationBuilinThemeReference: Int32 { case dark } -public enum PresentationThemeReference: Coding, Equatable { +public enum PresentationThemeReference: PostboxCoding, Equatable { case builtin(PresentationBuilinThemeReference) - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { case 0: self = .builtin(PresentationBuilinThemeReference(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))!) @@ -20,7 +20,7 @@ public enum PresentationThemeReference: Coding, Equatable { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { switch self { case let .builtin(reference): encoder.encodeInt32(0, forKey: "v") @@ -53,12 +53,12 @@ public struct PresentationThemeSettings: PreferencesEntry { self.theme = theme } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.chatWallpaper = (decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper) ?? .builtin self.theme = decoder.decodeObjectForKey("t", decoder: { PresentationThemeReference(decoder: $0) }) as! PresentationThemeReference } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObject(self.chatWallpaper, forKey: "w") encoder.encodeObject(self.theme, forKey: "t") } diff --git a/TelegramUI/RadialStatusNode.swift b/TelegramUI/RadialStatusNode.swift index 6f051f59f3..f2482496b8 100644 --- a/TelegramUI/RadialStatusNode.swift +++ b/TelegramUI/RadialStatusNode.swift @@ -102,35 +102,50 @@ final class RadialStatusNode: ASControlNode { super.init() } - func transitionToState(_ state: RadialStatusNodeState, completion: @escaping () -> Void) { + func transitionToState(_ state: RadialStatusNodeState, animated: Bool = true, completion: @escaping () -> Void) { if self.state != state { self.state = state let contentNode = state.contentNode(current: self.contentNode) if contentNode !== self.contentNode { - self.transitionToContentNode(contentNode, backgroundColor: state.backgroundColor(color: self.backgroundNodeColor), completion: completion) + self.transitionToContentNode(contentNode, backgroundColor: state.backgroundColor(color: self.backgroundNodeColor), animated: animated, completion: completion) } else { - self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), completion: completion) + self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), animated: animated, completion: completion) } } } - private func transitionToContentNode(_ node: RadialStatusContentNode?, backgroundColor: UIColor?, completion: @escaping () -> Void) { + private func transitionToContentNode(_ node: RadialStatusContentNode?, backgroundColor: UIColor?, animated: Bool, completion: @escaping () -> Void) { if let contentNode = self.contentNode { self.nextContentNode = node contentNode.enqueueReadyForTransition { [weak contentNode, weak self] in if let strongSelf = self, let contentNode = contentNode, strongSelf.contentNode === contentNode { - contentNode.animateOut { [weak contentNode] in - contentNode?.removeFromSupernode() + if animated { + contentNode.animateOut { [weak contentNode] in + contentNode?.removeFromSupernode() + } + strongSelf.contentNode = strongSelf.nextContentNode + if let contentNode = strongSelf.contentNode { + strongSelf.addSubnode(contentNode) + contentNode.frame = strongSelf.bounds + if strongSelf.isNodeLoaded { + contentNode.layout() + contentNode.animateIn() + } + } + strongSelf.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion) + } else { + contentNode.removeFromSupernode() + strongSelf.contentNode = strongSelf.nextContentNode + if let contentNode = strongSelf.contentNode { + strongSelf.addSubnode(contentNode) + contentNode.frame = strongSelf.bounds + if strongSelf.isNodeLoaded { + contentNode.layout() + } + } + strongSelf.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion) } - strongSelf.contentNode = strongSelf.nextContentNode - if let contentNode = strongSelf.contentNode { - strongSelf.addSubnode(contentNode) - contentNode.frame = strongSelf.bounds - contentNode.layout() - contentNode.animateIn() - } - strongSelf.transitionToBackgroundColor(backgroundColor, completion: completion) } } } else { @@ -139,11 +154,11 @@ final class RadialStatusNode: ASControlNode { contentNode.frame = self.bounds self.addSubnode(contentNode) } - self.transitionToBackgroundColor(backgroundColor, completion: completion) + self.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion) } } - private func transitionToBackgroundColor(_ color: UIColor?, completion: @escaping () -> Void) { + private func transitionToBackgroundColor(_ color: UIColor?, animated: Bool, completion: @escaping () -> Void) { let currentColor = self.backgroundNode?.color var updated = false @@ -167,10 +182,15 @@ final class RadialStatusNode: ASControlNode { } } else if let backgroundNode = self.backgroundNode { self.backgroundNode = nil - backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in - backgroundNode?.removeFromSupernode() + if animated { + backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in + backgroundNode?.removeFromSupernode() + completion() + }) + } else { + backgroundNode.removeFromSupernode() completion() - }) + } } } else { completion() diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index c3638d1216..27bf5d16a7 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -223,6 +223,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 1d04de5800..ee38910aa1 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -188,6 +188,14 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.contentGridNode.view.addGestureRecognizer(longTapRecognizer) } + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index 6d8d672a32..6359485b50 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -39,6 +39,22 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent } } +private func encodeText(_ string: String, _ key: Int) -> String { + var result = "" + for c in string.unicodeScalars { + result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!)) + } + return result +} + +private let keyboardWindowClass: AnyClass? = { + if #available(iOS 9.0, *) { + return NSClassFromString(encodeText("VJSfnpufLfzcpbseXjoepx", -1)) + } else { + return NSClassFromString(encodeText("VJUfyuFggfdutXjoepx", -1)) + } +}() + private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyComponentsGlobalsProvider { func log(_ string: String!) { print(string) @@ -57,6 +73,15 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone } public func applicationKeyboardWindow() -> UIWindow! { + guard let keyboardWindowClass = keyboardWindowClass else { + return nil + } + + for window in legacyComponentsApplication.windows { + if window.isKind(of: keyboardWindowClass) { + return window + } + } return nil } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 0ce80a8099..aedecda1f3 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -355,7 +355,7 @@ private func stringForBlockAction(strings: PresentationStrings, action: Destruct } } -private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: Coding?) -> [UserInfoEntry] { +private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: PostboxCoding?) -> [UserInfoEntry] { var entries: [UserInfoEntry] = [] guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { diff --git a/TelegramUI/VoiceCallSettings.swift b/TelegramUI/VoiceCallSettings.swift index 6733407f59..ac5383dc5d 100644 --- a/TelegramUI/VoiceCallSettings.swift +++ b/TelegramUI/VoiceCallSettings.swift @@ -19,11 +19,11 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable { self.dataSaving = dataSaving } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))! } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.dataSaving.rawValue, forKey: "ds") } diff --git a/TelegramUI/Wallpapers.swift b/TelegramUI/Wallpapers.swift index 5edebc8e06..4b05ce1c4c 100644 --- a/TelegramUI/Wallpapers.swift +++ b/TelegramUI/Wallpapers.swift @@ -8,7 +8,7 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { case color(Int32) case image([TelegramMediaImageRepresentation]) - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { case 0: self = .builtin @@ -22,7 +22,7 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { switch self { case .builtin: encoder.encodeInt32(0, forKey: "v") diff --git a/TelegramUI/ZoomableContentGalleryItemNode.swift b/TelegramUI/ZoomableContentGalleryItemNode.swift index 53b98918e2..e7ec7fb5a7 100644 --- a/TelegramUI/ZoomableContentGalleryItemNode.swift +++ b/TelegramUI/ZoomableContentGalleryItemNode.swift @@ -23,6 +23,9 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { override init() { self.scrollView = UIScrollView() + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } super.init() diff --git a/third-party/opus/include/opus/opus.h b/third-party/opus/include/opus/opus.h index b0bdf6f2df..027e09935b 100644 --- a/third-party/opus/include/opus/opus.h +++ b/third-party/opus/include/opus/opus.h @@ -198,7 +198,7 @@ OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_encoder_get_size(int channels); * This must be one of 8000, 12000, 16000, * 24000, or 48000. * @param [in] channels int: Number of channels (1 or 2) in input signal - * @param [in] application int: Coding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @param [in] application int: PostboxCoding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) * @param [out] error int*: @ref opus_errorcodes * @note Regardless of the sampling rate and number channels selected, the Opus encoder * can switch to a lower audio bandwidth or number of channels if the bitrate @@ -222,7 +222,7 @@ OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create( * This must be one of 8000, 12000, 16000, * 24000, or 48000. * @param [in] channels int: Number of channels (1 or 2) in input signal - * @param [in] application int: Coding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @param [in] application int: PostboxCoding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY) * @retval #OPUS_OK Success or @ref opus_errorcodes */ OPUS_EXPORT int opus_encoder_init(